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的新语法啦,不然全局命名空间还是真的香。

posted @ 2021-11-10 14:00  bluesky234  阅读(1849)  评论(0编辑  收藏  举报