前两节已经大概了解了 OpenXML SDK 的一些主要类型,以及Excel文档内部的结构。接下来开始尝试第一个Excel文档导出的实现。其实操做OpenXML SDK, 大部分状况下,和操做XML是差很少的,大部分类型都是继承于OpenXmlElement这个元素,通常大体了解XML的结构,对照来操做,都不是很难。git
咱们来生成一个简单的文档,设置第一行为表头,共五列,分别为:序号,学生姓名,学生年龄,学生班级,辅导老师, 同时输出2行具体的数据。github
具体导出结果图 以下图所示:数组
经过 Visual Studio 这个开发工具来建立一个控制台项目,也能够直接用 Visual Studio Code ,或者终端命令行来建立等等。 网络
首先安装OpenXml SDK,经过 Visual Studio 开发工具的Nuget管理安装,也能够直接在终端经过nuget包管理命令安装:工具
Install-Package DocumentFormat.OpenXml -Version 2.11.3 开发工具
命名空间,通常会用到如下几个: 测试
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
从前面章节咱们了解到咱们要涉及的几个主要部件(Part)的类型,有SpreadsheetDocument , workbookPart, WorksheetPart 。这三个部件是必须的,同时咱们这里增长一个SharedStringTablePart 类型。字体
大体流程:spa
(1)建立 SpreadsheetDocument 对象,表示一个Excel文档包,经过它进行下一步操做。命令行
(2)SpreadsheetDocument 对象下,提供AddNewPart,增长 WorkbookPart对象,至关于给文档包插入一个工做簿。
(3)初始化 WorkbookPart 对象中表明其描述的XML根元素节点: Workbook 对象。
(4)经过WorkbookPart 对象,先建立 SharedStringTablePart 对象,表示这个工做簿中,统一共享字符串相关的部件。
(5)有了工做簿,则须要插入工做表了;经过WorkbookPart对象的AddNewPart,方法为其增长子部件,增长WorksheetPart 对象
(6)创建工做簿和工做表的关联,Workbook 下建立 Sheets, 再建立Sheet, 该对象,承接的任务就是创建工做簿和上一步建立的WorksheetPart 对象。
(7)接下来的核心就都在工做表了,初始化Worksheet对象
(8)建立表格表头信息
(9)建立表格数据内容的信息
(10)保存工做簿而且持久化到磁盘
如下代码是Main方法,其中 初始化Worksheet,建立表头,建立表格数据 单独放一个方法 。
1 public static void Main(string[] args) 2 { 3 //构建一个MemoryStream 4 var ms = new MemoryStream(); 5 6 //建立Workbook, 指定为Excel Workbook (*.xlsx). 7 var document = SpreadsheetDocument.Create(ms, SpreadsheetDocumentType.Workbook); 8 9 //建立WorkbookPart(工做簿) 10 var workbookPart = document.AddWorkbookPart(); 11 workbookPart.Workbook = new Workbook(); 12 13 //构建SharedStringTablePart 14 var shareStringPart = workbookPart.AddNewPart<SharedStringTablePart>(); 15 shareStringPart.SharedStringTable = new SharedStringTable(); //建立根元素 16 17 //建立WorksheetPart(工做簿中的工做表) 18 var worksheetPart = workbookPart.AddNewPart<WorksheetPart>(); 19 20 //Workbook 下建立Sheets节点, 创建一个子节点Sheet,关联工做表WorksheetPart 21 var sheets = document.WorkbookPart.Workbook.AppendChild<Sheets>( 22 new Sheets(new Sheet() 23 { 24 Id = document.WorkbookPart.GetIdOfPart(worksheetPart), 25 SheetId = 1, 26 Name = "myFirstSheet" 27 })); 28 29 //初始化Worksheet 30 InitWorksheet(worksheetPart); 31 32 //建立表头 (序号,学生姓名,学生年龄,学生班级,辅导老师) 33 CreateTableHeader(worksheetPart, shareStringPart); 34 35 //建立内容数据 36 CreateTableBody(worksheetPart, shareStringPart); 37 38 workbookPart.Workbook.Save(); 39 document.Close(); 40 41 //保存到文件 42 SaveToFile(ms); 43 Console.WriteLine("End."); 44 }
初始化 WorksheetPart 对象下的 Worksheet 对象,Worksheet 是对应描述工做表的XML的根元素。Worksheet 下的子对象中,SheetData 是表示单元格数据部分,SheetFormatProperties 是能够设置一些属性,Columns 是定义列的一些属性。
这里初始化工做表,设置默认行高度,宽度分别为15个单位长度, 而第一列宽度为 5个单位长度,而第2列~第4列为 30个单位长度。Excel里面,高度,宽度的单位是什么,没有说明,网络上查阅的资料是:
Excel 行高所使用单位为磅( 1厘米 = 28.6磅),列宽使用单位为 1/10英寸(既 1个单位为 2.54毫米)
Excel 行高:1毫米(mm)=2.7682个单位长度,则 1厘米(cm)=27.682个单位长度;1个单位长度=0.3612 毫米(mm)
Excel 列宽:1毫米(mm)=0.4374个单位长度,则 1厘米(cm)=4.374 个单位长度;1个单位长度=2.2862 毫米(mm)
Column类型,其中属性 Min 和 Max 是一个区间,表示连续多列,因此设置 第2列~第4列为 30个单位长度,只须要实则Min 为2, Max为 4, 要注意的是这里是从1开始计算,不是从0开始。
具体初始化代码以下:
1 /// <summary> 2 /// 初始化工做表 3 /// </summary> 4 /// <param name="worksheetPart"></param> 5 public static void InitWorksheet(WorksheetPart worksheetPart) 6 { 7 //构建Worksheet根节点,同时追加子节点SheetData 8 worksheetPart.Worksheet = new Worksheet(new SheetData()); 9 //获取Worksheet对象 10 var worksheet = worksheetPart.Worksheet; 11 12 //SheetFormatProperties, 设置默认行高度,宽度, 值类型是Double类型。 13 var sheetFormatProperties = new SheetFormatProperties() 14 { 15 DefaultColumnWidth = 15d, 16 DefaultRowHeight = 15d 17 }; 18 19 //插入SheetFormatProperties,插入到SheetData的前面。经过InsertBefore方法,而不是Append 20 //顺序不能错误,不然会致使office打开提示错误,因此通常最好提早在一个列表或者数组,放好顺序再一次性加入 21 worksheet.InsertBefore(sheetFormatProperties, worksheet.GetFirstChild<SheetData>()); 22 23 //初始化列宽 第一列 5个单位, 第二列~第四列 30个单位 24 var columns = new Columns(); 25 //列,从1开始算起。 26 var column1 = new Column 27 { 28 Min = 1, Max = 1, Width = 5d, CustomWidth = true 29 }; 30 var column2 = new Column 31 { 32 Min = 2, Max =3, Width = 30d, CustomWidth = true 33 }; 34 35 columns.Append(column1, column2); 36 37 //插入Column1对象, 它的位置是在SheetFormatProperties 的后面,可是在SheetData的前面。 38 //worksheet.Append(columns); 直接追加在后面,office打开提示错误 39 worksheet.InsertAfter(columns, worksheet.GetFirstChild<SheetFormatProperties>()); 40 41 //最好是前面弄好对象,再一次性插入,或者初始化时先建立对象,用的时候直接拿出来。 42 //worksheet.Append(new OpenXmlElement[] 43 //{ 44 // new SheetFormatProperties(), 45 // new Columns(), 46 // new SheetData() 47 //}); 48 }
这里须要注意的一点 SheetData,SheetFormatProperties ,Columns 三个类型在Worksheet 下的顺序,是有要求的。自己XML上,经过XSD 是能够约束 子元素 出现的顺序。按照这三个的顺序是: SheetFormatProperties最前, 中间Columns , 最后SheetData。
若是说不按顺序的话,会怎么样呢? 执行代码,导出成功了,可是经过office excel 打开文件的时候,会提示有问题,提示以下图所示:
能够点击是,尝试恢复。 修复后会提示 是sheet1.xml 文件有问题,已经删除或者修复了不可读取的内容,以下图所示:
可是咱们发现,修复成功以后,里面的数据内容不见,表头也没有了,表格数据也没有了。因此经过OpenXML SDK 来操做Excel,是须要很当心的。
这里另一个有趣的地方是,若是你用WPS office 来打开那个错误的Excel(直接导出,没有通过office修复的), 它其实是能够打开的, 也就是说WPS对这种有错误格式的Excel文件,是有必定的兼容性的,打开以下图所示:
可是咱们导出,作测试的时候,仍是须要以office 软件打得开为准,毕竟不知道使用者的软件安装的是office 仍是 wps。
初始化工做表部分,接下来就是开始导出数据了,首先是录入表头数据, 来看CreateTableHeader这个方法的实现。
单元格的数据,都是存放在SheetData 下面的,从结构上很容易理解,一个Row对象,表示一行, Row对象下面的每一个 Cell对象,表示一行中的一格, CellValue 表示单元格的值。
这里同时使用了 SharedStringTablePart 对象,这个部件表明共享字符串信息,属于WorkbookPart下面的,表示整个工做簿下的工做表均可以用这个来共享字符串,以便于减小整个文档的大小。SharedStringTablePart 对象下表明其对应XML文件的根元素,是 SharedStringTable对象(xml根节点元素是 sst ), 而其对象下的 SharedStringItem(xml根节点元素是 si )表示一个要用于共享的字符串项,new SharedStringItem(new Text("文本信息")) 就表示一个字符串项,这个对象也不是只用于普通文档,像一个单元格文本里面附带多种字体,多种颜色,也是能够的,可是相对来讲构建起来会很复杂。而单元格的值 CellValue 对象,是经过 这个共享字符串SharedStringItem 在 SharedStringTable下面的第几个元素来引用的,用索引值(0开始计算的)。 好比是第二个子元素,则其对应的索引值就是 1(0开始计算的), 因此就是构建对象的时候就是 new CellValue("1") , 同时 Cell对象下的属性,DataType属性,值为枚举类CellValues 指定的 SharedString。
具体CreateTableHeader这个方法代码以下,同时建立表头单元格的方法,也抽了一个CreateTableHeaderCell方法,具体以下:
1 /// <summary> 2 /// 建立表头。 (序号,学生姓名,学生年龄,学生班级,辅导老师) 3 /// </summary> 4 /// <param name="worksheetPart">WorksheetPart 对象</param> 5 /// <param name="shareStringPart">SharedStringTablePart 对象</param> 6 public static void CreateTableHeader(WorksheetPart worksheetPart, SharedStringTablePart shareStringPart) 7 { 8 //获取Worksheet对象 9 var worksheet = worksheetPart.Worksheet; 10 11 //获取表格的数据对象,SheetData 12 var sheetData = worksheet.GetFirstChild<SheetData>(); 13 14 //插入第一行数据,做为表头数据 建立 Row 对象,表示一行 15 var row = new Row 16 { 17 //设置行号,从1开始,不是从0 18 RowIndex = 1 19 }; 20 21 //Row下面,追加Cell对象 22 row.AppendChild(CreateTableHeaderCell("序号", shareStringPart)); 23 row.AppendChild(CreateTableHeaderCell("学生姓名", shareStringPart)); 24 row.AppendChild(CreateTableHeaderCell("学生年龄", shareStringPart)); 25 row.AppendChild(CreateTableHeaderCell("学生班级", shareStringPart)); 26 row.AppendChild(CreateTableHeaderCell("辅导老师", shareStringPart)); 27 28 sheetData.AppendChild(row); 29 } 30 31 /// <summary> 32 /// 建立表头的单元格 33 /// </summary> 34 public static Cell CreateTableHeaderCell(string headerStr, SharedStringTablePart shareStringPart) 35 { 36 //共享字符串表 37 var sharedStringTable = shareStringPart.SharedStringTable; 38 39 //把字符串追加到共享 40 sharedStringTable.AppendChild(new SharedStringItem(new Text(headerStr))); 41 var index = sharedStringTable.ChildElements.Count - 1; //获取索引 42 43 var cell = new Cell 44 { 45 //设置值,这里的值是引用 共享字符串里面的对应的索引,就是上面添加的SharedStringItem的子元素的位置。 46 CellValue = new CellValue(index.ToString()), 47 //设置值类型是共享字符串 48 DataType = new EnumValue<CellValues>(CellValues.SharedString) 49 }; 50 51 return cell; 52 }
建立了表头部分,接下来就是开始建立表格内容数据了,其实方法和建立表头是同样,只是这里不使用 SharedStringTablePart 对象,换另一种方式尝试下和对比,就是直接输出字符串, Cell对象下的属性,DataType属性,值为枚举类CellValues 指定的 String。 CellValue 对象构建的时候,输入的就不是索引值,而是具体的内容字符串了。
具体代码以下:
1 public static void CreateTableBody(WorksheetPart worksheetPart) 2 { 3 //获取Worksheet对象 4 var worksheet = worksheetPart.Worksheet; 5 6 //获取表格的数据对象,SheetData 7 var sheetData = worksheet.GetFirstChild<SheetData>(); 8 9 //插入第一行数据,做为表头数据 建立 Row 对象,表示一行 10 var row1 = new Row 11 { 12 RowIndex = 2 13 }; 14 15 row1.Append(new OpenXmlElement[] 16 { 17 new Cell() 18 { 19 CellValue = new CellValue("1"), 20 DataType = new EnumValue<CellValues>(CellValues.String) 21 }, 22 new Cell() 23 { 24 CellValue = new CellValue("王同窗"), 25 DataType = new EnumValue<CellValues>(CellValues.String) 26 }, 27 new Cell() 28 { 29 CellValue = new CellValue("18岁"), 30 DataType = new EnumValue<CellValues>(CellValues.String) 31 }, 32 new Cell() 33 { 34 CellValue = new CellValue("一班"), 35 DataType = new EnumValue<CellValues>(CellValues.String) 36 }, 37 new Cell() 38 { 39 CellValue = new CellValue("林老师"), 40 DataType = new EnumValue<CellValues>(CellValues.String) 41 } 42 }); 43 44 sheetData.AppendChild(row1); 45 46 var row2 = new Row 47 { 48 RowIndex = 3 49 }; 50 51 row2.Append(new OpenXmlElement[] 52 { 53 new Cell() 54 { 55 CellValue = new CellValue("2"), 56 DataType = new EnumValue<CellValues>(CellValues.String) 57 }, 58 new Cell() 59 { 60 CellValue = new CellValue("李同窗"), 61 DataType = new EnumValue<CellValues>(CellValues.String) 62 }, 63 new Cell() 64 { 65 CellValue = new CellValue("19岁"), 66 DataType = new EnumValue<CellValues>(CellValues.String) 67 }, 68 new Cell() 69 { 70 CellValue = new CellValue("二班"), 71 DataType = new EnumValue<CellValues>(CellValues.String) 72 }, 73 new Cell() 74 { 75 CellValue = new CellValue("林老师"), 76 DataType = new EnumValue<CellValues>(CellValues.String) 77 } 78 }); 79 80 sheetData.AppendChild(row2); 81 }
调用Workbook的save方法,和关闭文档对象(document.Close()方法)。因为建立文档的时候,并非指定一个文件路径,而是经过一个Stream流, 因此还须要将流转换为文件流持久化,经过SaveToFile方法。
如下SaveToFile方法代码:
1 /// <summary> 2 /// 保存到文件 3 /// </summary> 4 public static void SaveToFile(MemoryStream ms) 5 { 6 //当前运行时路径 7 var directoryInfo = new DirectoryInfo(Directory.GetCurrentDirectory()); 8 var fileName = $@"PracticePart1-{DateTime.Now:yyyyMMddHHmmss}.xlsx"; 9 10 //文件路径,保存在运行时路径下 11 var filepath = Path.Combine(directoryInfo.ToString(), fileName); 12 13 var bytes = ms.ToArray(); 14 var fileStream = new FileStream(filepath, FileMode.Create, FileAccess.Write, FileShare.Read); 15 fileStream.Write(bytes, 0, bytes.Length); 16 fileStream.Flush(); 17 18 Console.WriteLine($"Save Path: {filepath}"); 19 }
执行代码生成了Excel文件以后,打开展现就如文章第一图所展现的是同样的。 修改后缀名为zip解压后,打开文件夹,包含了workbook.xml 和 sharedStrings.xml 两个xml文件,和worksheets文件。跟上一节解压的文件对比, 没有style.xml文件, 由于咱们在代码中,尚未涉及到 样式类型。
打开worksheets文件,有一个sheet1.xml文件,工做表文件。
打开sheet1.xml文件来看看,咱们导出的数据,生成是怎么样的。
如下是sheet1.xml文件代码:
1 <?xml version="1.0" encoding="utf-8"?> 2 <x:worksheet xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> 3 <x:sheetFormatPr defaultColWidth="15" defaultRowHeight="15" /> 4 <x:cols> 5 <x:col min="1" max="1" width="5" customWidth="1" /> 6 <x:col min="2" max="3" width="30" customWidth="1" /> 7 </x:cols> 8 <x:sheetData> 9 <x:row r="1"> 10 <x:c t="s"> 11 <x:v>0</x:v> 12 </x:c> 13 <x:c t="s"> 14 <x:v>1</x:v> 15 </x:c> 16 <x:c t="s"> 17 <x:v>2</x:v> 18 </x:c> 19 <x:c t="s"> 20 <x:v>3</x:v> 21 </x:c> 22 <x:c t="s"> 23 <x:v>4</x:v> 24 </x:c> 25 </x:row> 26 <x:row r="2"> 27 <x:c t="str"> 28 <x:v>1</x:v> 29 </x:c> 30 <x:c t="str"> 31 <x:v>王同窗</x:v> 32 </x:c> 33 <x:c t="str"> 34 <x:v>18岁</x:v> 35 </x:c> 36 <x:c t="str"> 37 <x:v>一班</x:v> 38 </x:c> 39 <x:c t="str"> 40 <x:v>林老师</x:v> 41 </x:c> 42 </x:row> 43 <x:row r="3"> 44 <x:c t="str"> 45 <x:v>2</x:v> 46 </x:c> 47 <x:c t="str"> 48 <x:v>李同窗</x:v> 49 </x:c> 50 <x:c t="str"> 51 <x:v>19岁</x:v> 52 </x:c> 53 <x:c t="str"> 54 <x:v>二班</x:v> 55 </x:c> 56 <x:c t="str"> 57 <x:v>林老师</x:v> 58 </x:c> 59 </x:row> 60 </x:sheetData> 61 </x:worksheet>
从上面代码看,和前一节对比展现的对比,有一点不同,就是元素节点前面加上了命名空间。 XML命名空间 主要是为了避免命名冲突而起,因此这里加上了命名空间则是更为严谨了一些而已。 从上面的代码能够看出,除了表头一行用的是共享字符串(<x:c t="s">),表格数据内容则是直接字符串内容(<x:c t="str">)。
对比看下sharedStrings.xml 文件的代码:
1 <?xml version="1.0" encoding="utf-8"?> 2 <x:sst xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> 3 <x:si> 4 <x:t>序号</x:t> 5 </x:si> 6 <x:si> 7 <x:t>学生姓名</x:t> 8 </x:si> 9 <x:si> 10 <x:t>学生年龄</x:t> 11 </x:si> 12 <x:si> 13 <x:t>学生班级</x:t> 14 </x:si> 15 <x:si> 16 <x:t>辅导老师</x:t> 17 </x:si> 18 </x:sst>
针对一些经常使用,而且可能重复性出现不少次的文本,是能够经过共享字符串来统一存储和访问的,其它部分能够直接放在各自的工做表里面。一方面主要是在代码逻辑处理上会比较麻烦,由于值的引用,是经过共享字符串在其子元素的位置索引。若是通常不是预先设置好,很难知道其要插入的字符串,是否在共享字符串列表里面存在了。 除非每次插入的时候,都去判断一下,不存在则插入,返回索引,若存在则直接返回索引。
文中源代码能够查阅Github: https://github.com/QingGuangWang/OpenXMLForExcelPractices/tree/master/PracticePart2
以上即是本节的内容,其中须要再次强调的是操做各个子元素的时候,须要注意其顺序,如有时候不知道什么顺序,或者要用什么元素,简单的状况下,就是先用office软件建立一个excel,设置你要的格式和数据,而后解压出来看看其中的XML文件,这个时候你大体能够了解到你想要的信息。