能寫多少篇我就不确定了,可能就這一篇就太監了,也有可能會寫不少。
OpenXml SDK 相信很多人都不陌生,這個就是管Office一家的文檔格式,Word, Excel, PowerPoint等都用到這個。并且,這個格式主要是給Word 2007以上使用的。如果是用到其中Excel部分,那建議直接使用NPOI這樣的成品類庫就行。
但是,NPOI FOR WORD真的是太難受了。當然,也不是說一定要用NPOI,現在成品的WORD操作庫也不是沒有,比如DocX。這個庫基本算是 Xceed Words for .NET 的簡化版。GitHub https://github.com/xceedsoftware/DocX 。而且,這個庫非常牛逼的地方在于他是直接操作XML的,效率是上去了,可讀性就下去了啊。要讀懂這個,恐怕得對 ISO/IEC 29500 這個标準非常熟悉。那沒救了,我是專精資訊系統開發的,對這個标準的了解非常一般。而且說實話,這個标準裡 95% 以上的内容我根本用不到,我又不用做一個Word,我隻需要把我系統裡的東西生成為一個Word顯示出來就行了。
那于是,我痛定思痛,自己讀文檔吧:https://docs.microsoft.com/zh-cn/office/open-xml/open-xml-sdk (當然,英文的品質比中文可高多了,不過懶的看英文)。這文檔寫的可真是太專業了,想讀懂它恐怕得要點技術水準。是以呢,我打算把這個文檔給拆一下,做一個筆記。能寫多少就随緣了,反正我把需求實作完了就不寫了。恐怕兩三篇就完事了。那第一篇講的就是Word的基本結構。
一、WordprocessingML的了解
在看文檔和使用的時候,就可以發現這樣的一個命名空間:Wordprocessing。也可以看到這樣的名詞WordprocessingML。什麼意思呢,Office家的這個産品叫Word,其作用是處理文字。是以,Wordprocessing翻譯成 文字處理 就行了。對于OpenXml結構的docx檔案那就是一個壓縮包。你把字尾名從docx改成zip就可以用解壓軟體打開了。在其中,可以看到這樣的結構:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL0gjNzIjN1MTMtITOxcTNzIzMxATMxETMyAjMtUTMxMDMy8CXxETMyAjMvwVNxEzMwIzLcd2bsJ2Lc12bj5ycn9Gbi52YuAjMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
這個結構裡,第一個檔案夾word就是我們要關注的内容,這個檔案夾裡是這樣的:
有圖檔的話會更複雜一點,再多一個media檔案夾,裡面存着圖檔。不過這個無關緊要,本次我的需求隻是簡單的輸出一個純文檔的證明檔案。是以,不要管圖檔了。
在這裡,重點需要注意注意的xml有兩個,document.xml和styles.xml。他們分别對應着docx檔案的樣式部分和正文部分,大緻就是這樣的:
也就是說,如果我們需要通過代碼編輯一個純文字的Word,那就是修改這兩個xml就可以了。甚至于,如果不需要搞樣式的話,隻要改docment.xml就行了。這兩個Xml适用的标準就是 ISO/IEC 29500,并且這種Xml就稱為:WordprocessingML。
但是,手寫xml可太刑了。把整個 ISO/IEC 29500:2016 讀完怕不是半條命就要去掉了。再等你把代碼寫完,恐怕你的工作就已經涼涼了。是以呢,微軟自己出了個 OpenXml SDK 幫助開發者編輯這種Xml檔案。不過呢,這玩意也是真的難用。而且說實話,裡面一大片功能是根本用不着。說實話,日常使用的時候,也就是搞個樣式,然後向裡面添加文字,設定一下字型和段落樣式,頂多插入點圖檔和表格。
對于我這種做普通的資訊管理系統的人來說,圖檔和表格裡都有大把的功能是完全用不着的。而且對于MIS的絕大部分用例而言,我都是隻需要生成Word,然後由Word程式讀取,而不需要由我來讀一個Word模闆,然後再向其中修改。當然,如果你會寫了,讀也不是什麼太難的問題,隻不過Word那個鬼程式裡的Run真的是看不懂生成規律,經常會有亂七八糟的東西。是以,如果有碰到讀模闆再向裡寫的需求,我另外寫一個筆記。
二、建立一個Word檔案
打開VS,然後建立一個指令行程式,向裡面添加一個名為“DocumentFormat.OpenXml”的Nuget包,這樣項目的引用關系就做完了。然後添加以下代碼:
1 if (File.Exists("newDocx.docx"))
2 {
3 File.Delete("newDocx.docx");
4 }
5
6 using (WordprocessingDocument doc = WordprocessingDocument.Create("newDocx.docx", DocumentFormat.OpenXml.WordprocessingDocumentType.Document))
7 {
8 var main = doc.MainDocumentPart;
9 if (doc.MainDocumentPart == null)
10 {
11 doc.AddMainDocumentPart();
12 }
13
14 if (doc.MainDocumentPart.Document == null)
15 {
16 doc.MainDocumentPart.Document = new Document();
17 }
18 var body = doc.MainDocumentPart.Document;
19
20 Paragraph para = new Paragraph();
21 Run r = new Run();
22 Text t = new Text();
23 t.Text = "Hello World";
24 r.Append(t);
25 para.Append(r);
26 body.Append(para);
27
28 doc.Save();
29 }
運作一下,就可以發現在運作目錄下,出現了一個名為 newDocx.docx 的檔案。這個檔案打開,裡面就一行 Hello World 文本。雖然,這個Hello World程式簡單,但是要了解這個東西就 特!别!麻!煩!
首先,using語句塊裡,“WordprocessingDocument”對象 就是一個docx文檔對象,也就是上文所述的那個壓縮包。聲明這個對象通常使用兩種方法:Open和Create。非常好差別,一個是打開,一個是建立。
之後,“MainDocumentPart”這個屬性就相當于壓縮包裡的“word”檔案夾,非常的真實。
接着,“MainDocumentPart.Document”這個屬性就相當于“word”檔案夾下的“document.xml”檔案,更真實了。
再下,“Paragraph”是一個段落。一份Word是由多個段落或者表格組成的。是以,在Word文檔裡,看到回車符,就可以認為是一個“Paragraph”對象的結束。
在“段落”裡,有多個連續文本,也就是“Run”。一個Run就相當于Html裡的span标簽。比如,上文中,Hello World整個文本都是一樣的格式。是以,就應當在一個Run裡。寫成Html大緻是這樣的感覺:
1 <p>
2 <span>Hello World</span>
3 </p>
但是,并不是所有情況都是這樣的。在很多時候,一個段落的文本也是有不同的格式的。比如說,中文和英文的字型不一樣,或者其它情況。比如,下文這樣:
那這時,就需要對段落内的文本再進行拆分。上圖的格式,寫成Html大緻是這樣的感覺:
1 <p>
2 <span>某</span>
3 <span>個</span>
4 <span>大</span>
5 <span>學</span>
6 <span>大</span>
7 <span>學</span>
8 <span>生</span>
9 <span>創</span>
10 </p>
是以呢,這裡每一個字都是一個Run。在Run裡,則是正式的文字,相當于span的#innerText。但是,WordprocessingML要求你将這些文字放在名為Text的段落裡。
至此,整個Word的基本結構就看懂了。Document裡面有若幹個Paragraph。每個Paragraph裡,前後格式完全一樣的文本放在一個Run裡。前後格式不一樣的文本放在不同的Run裡。每一個Run裡面再有若幹個Text。那麼,練習一下,下面的Word有幾個Paragraph,幾個Run?
這個就是我需求的一部分,我也沒有截全。但是看的出來,是有兩個段落。是以,兩個Paragraph安排上。第一個Paragraph裡,“茲證明”的格式是一樣的,但是後面的下劃線的文本是“空格符”,文本格式是“下劃線”。他們的格式與前面的“茲證明”不一樣。是以,哪怕再後續的“學院教師”與“茲證明”的格式相同,這裡也得分成三個Run。再之後,又是一個下劃線,再一個Run。再後,根據需求,這個括号也是三号宋體和文字的格式一樣,是以“(教工号:”是一個Run。下劃線再一個Run。“)指導項目如下:”一個Run。是以,第一段裡就有7個Run。
第二段,就留給大家做練習了。在我截出來的部分,所有數字元号都是三号宋體,和文字一樣。算一下多少個Run?具體過程我就略了,答案是5個。
三、封裝OpenXml SDK
如果說你對自己的技術有信心。那直接用OpenXml當然也是可以的。但是,我嫌他實在太煩了。于是,自己封裝一下這個SDK,讓他變的更加易用一些。對于一個文檔而言,他的操作基本就是打開,儲存,建立。需要注意的是,在建立的時候,直接“WordprocessingDocument.Create”出來的是一個空的壓縮包。必須要向其中添加“MainDocumentPart”和“Document”。甚至還有其它的東西,都是要自己加的。是以,在這個封裝操作裡,需要一個初始化函數。再者,同原來的WordprocessingDocument類一樣,這個構造函數肯定也是要私有化的。不然容易出問題。
于是,建立一個WordDocument類。就可以敲出這樣的代碼了:
1 using DocumentFormat.OpenXml.Packaging;
2 using DocumentFormat.OpenXml.Wordprocessing;
3 using System;
4 using System.IO;
5
6 namespace Ricebird.Wordprocessing
7 {
8 public class WordDocument : IDisposable
9 {
10 protected WordprocessingDocument InternalDocument
11 {
12 get; set;
13 } = null;
14
15 #region ctor
16 private WordDocument()
17 {
18
19 }
20 #endregion
21
22 #region 建立對象
23 /// <summary>
24 /// 建立一個Word對象
25 /// </summary>
26 /// <returns></returns>
27 public static WordDocument CreateDocument()
28 {
29 WordDocument doc = new WordDocument();
30 doc.InternalDocument = WordprocessingDocument.Create(new MemoryStream(), DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
31 doc.InitializeDocument();
32 return doc;
33 }
34
35 /// <summary>
36 /// 讀取一個Word文檔
37 /// </summary>
38 /// <param name="path">文檔路徑</param>
39 /// <param name="createNew">如果檔案已經存在,是否删除原檔案</param>
40 /// <returns></returns>
41 public static WordDocument LoadDocument(string path, bool createNew)
42 {
43 if (createNew && File.Exists(path))
44 {
45 File.Delete(path);
46 }
47
48 WordDocument doc = new WordDocument();
49 if (File.Exists(path))
50 {
51 doc.InternalDocument = WordprocessingDocument.Open(path, true);
52 }
53 else
54 {
55 doc.InternalDocument = WordprocessingDocument.Create(path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
56 }
57 doc.InitializeDocument();
58 return doc;
59 }
60 #endregion
61
62 #region 初始化文檔
63 protected void InitializeDocument()
64 {
65 var doc = InternalDocument;
66 if (doc.MainDocumentPart == null)
67 {
68 doc.AddMainDocumentPart();
69 }
70
71 if (doc.MainDocumentPart.Document == null)
72 {
73 doc.MainDocumentPart.Document = new Document();
74 }
75
76 }
77 #endregion
78
79 #region 儲存函數
80 /// <summary>
81 /// 儲存函數
82 /// </summary>
83 public void Save()
84 {
85 InternalDocument.Save();
86 }
87
88 /// <summary>
89 /// 另存為函數
90 /// </summary>
91 /// <param name="path"></param>
92 public void SaveAs(string path)
93 {
94 InternalDocument.SaveAs(path);
95 }
96 #endregion
97
98 public void Dispose()
99 {
100 InternalDocument?.Dispose();
101 }
102 }
103 }
那由于這個項目的運作環境是C#7.0。是以就不能用10.0的新文法啦,不然全局命名空間還是真的香。