前兩節已經大概瞭解了 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文件,這個時候你大體能夠了解到你想要的信息。