基於 DocumentFormat.OpenXml 操作 Excel (3)-- 導出數據


  前兩節已經大概了解了 OpenXML SDK 的一些主要類型,以及Excel文檔內部的結構。接下來開始嘗試第一個Excel文檔導出的實現。其實操作OpenXML SDK, 大部分情況下,和操作XML是差不多的,大部分類型都是繼承於OpenXmlElement這個元素,一般大致了解XML的結構,對照來操作,都不是很難。

  我們來生成一個簡單的文檔,設置第一行為表頭,共五列,分別為:序號,學生姓名,學生年齡,學生班級,輔導老師, 同時輸出2行具體的數據。

  具體導出結果圖 如下圖所示:

  

  

 --》項目准備

  通過 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;

  

 --》創建Excel文檔,工作簿部件 WorkbookPart

  從前面章節我們了解到我們要涉及的幾個主要部件(Part)的類型,有SpreadsheetDocument workbookPartWorksheetPart 。這三個部件是必須的,同時我們這里增加一個SharedStringTablePart 類型。

  大致流程:

  (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 }

  

 --》初始化Worksheet

  初始化 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 }

  這里需要注意的一點 SheetDataSheetFormatProperties 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,解壓文件對比

  執行代碼生成了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文件,這個時候你大致可以了解到你想要的信息。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM