zoukankan      html  css  js  c++  java
  • 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的新语法啦,不然全局命名空间还是真的香。

  • 相关阅读:
    Swift和OC混编
    Swift逃逸闭包之见解
    百度地图集成
    hitTest和pointInside和CGRectContainsPoint
    Bitcode问题
    ReactiveCocoa常用方法
    iOS之图文混排
    tableview cell添加3D动画
    ReactiveCocoa总结
    Math类常用方法(Java)
  • 原文地址:https://www.cnblogs.com/Pray4U/p/15533139.html
Copyright © 2011-2022 走看看