OpenXmlSDK学习笔记(1):Word的基本结构
能写多少篇我就不确定了,可能就这⼀篇就太监了,也有可能会写不少。
OpenXml SDK 相信很多⼈都不陌⽣,这个就是管Office⼀家的⽂档格式,Word, Excel, PowerPoint等都⽤到这个。并且,这个格式主要是给Word 2007以上使⽤的。如果是⽤到其中Excel部分,那建议直接使⽤NPOI这样的成品类库就⾏。
⼀、WordprocessingML的理解
在看⽂档和使⽤的时候,就可以发现这样的⼀个命名空间:Wordprocessing。也可以看到这样的名词WordprocessingML。什么意思
呢,Office家的这个产品叫Word,其作⽤是处理⽂字。所以,Wordprocessing翻译成⽂字处理就⾏了。对于OpenXml结构的docx⽂件那就是⼀个压缩包。你把后缀名从docx改成zip就可以⽤解压软件打开了。在其中,可以看到这样的结构:
这个结构⾥,第⼀个⽂件夹word就是我们要关注的内容,这个⽂件夹⾥是这样的:
有图⽚的话会更复杂⼀点,再多⼀个media⽂件夹,⾥⾯存着图⽚。不过这个⽆关紧要,本次我的需求只是简单的输出⼀个纯⽂档的证明⽂件。所以,不要管图⽚了。
在这⾥,重点需要注意注意的xml有两个,l和l。他们分别对应着docx⽂件的样式部分和正⽂部分,⼤致就是这样的:也就是说,如果我们需要通过代码编辑⼀个纯⽂本的Word,那就是修改这两个xml就可以了。甚⾄于,如果不需要搞样式的话,只要改
但是,⼿写xml可太刑了。把整个 ISO/IEC 29500:2016 读完怕不是半条命就要去掉了。再等你把代码写完,恐怕你的⼯作就已经凉凉了。所以呢,微软⾃⼰出了个 OpenXml SDK 帮助开发者编辑这种Xml⽂件。不过呢,这玩意也是真的难⽤。⽽且说实话,⾥⾯⼀⼤⽚功能是根本⽤不着。说实话,⽇常使⽤的时候,也就是搞个样式,然后向⾥⾯添加⽂字,设置⼀下字体和段落样式,顶多插⼊点图⽚和表格。
对于我这种做普通的信息管理系统的⼈来说,图⽚和表格⾥都有⼤把的功能是完全⽤不着的。⽽且对于MIS的绝⼤部分⽤例⽽⾔,我都是只需要⽣成Word,然后由Word程序读取,⽽不需要由我来读⼀个Word模板,然后再向其中修改。当然,如果你会写了,读也不是什么太难的问题,只不过Word那个⿁程序⾥的Run真的是看不懂⽣成规律,经常会有乱七⼋糟的东西。所以,如果有碰到读模板再向⾥写的需求,我另外写⼀个笔记。
⼆、创建⼀个Word⽂件
打开VS,然后创建⼀个命令⾏程序,向⾥⾯添加⼀个名为“DocumentFormat.OpenXml”的Nuget包,这样项⽬的引⽤关系就做完了。然后添加以下代码:
1if (File.Exists("newDocx.docx"))
2 {
3    File.Delete("newDocx.docx");
4 }
5
6using (WordprocessingDocument doc = WordprocessingDocument.Create("newDocx.docx", DocumentFormat.OpenXml.WordprocessingDocumentType.Document)) 7 {
8var main = doc.MainDocumentPart;
9if (doc.MainDocumentPart == null)
10    {
11        doc.AddMainDocumentPart();
12    }
13
14if (doc.MainDocumentPart.Document == null)
15    {
16        doc.MainDocumentPart.Document = new Document();
17    }
18var 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”⽂件夹下的“l”⽂件,更真实了。
再下,“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;
sdk13        } = 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的新语法啦,不然全局命名空间还是真的⾹。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。