OpenXml SDK學習筆記(1):Word的基本結構


能寫多少篇我就不確定了,可能就這一篇就太監了,也有可能會寫不少。

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就可以用解壓軟件打開了。在其中,可以看到這樣的結構:

 

 

 

 這個結構里,第一個文件夾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的新語法啦,不然全局命名空間還是真的香。


免責聲明!

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



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