正好,我今天下午就打算写这个工具类库了。那开发过程就随便写写哈。我个人开发是习惯先想最后如何调用,再按调用编写过程。那么,最后使用,我希望是这样的风格:
1 using (WordDocument doc = WordDocument.CreateDocument("newDocx.docx", true)) 2 { 3 doc.AppendParagraph(para => 4 { 5 para.Append("你好,世界") 6 .Append(" ") 7 .Append("你好,Word"); 8 }) 9 .AppendParagraph() 10 .Append("Hello World") 11 .Append(" ") 12 .Append("Hello World"); 13 doc.Save(); 14 }
大致可以看出两种风格。上面那种属于ASP.NET CORE里经常用到的Builder模式,下面一种则是经典的流式接口。那么,经过思考,由于这里之后还需要设置格式。所以,流式接口可能更适合一些。因为设置格式的时候,更需要Builder。而一个函数里写两个Builder就非常奇怪。所以,在这里,选择流式接口。所以,在刚才的项目里,新建两个类:WordParagraph和WordRun。这里需要注意,在WordprocessingML里,Run这个结构不一定是隶属于Paragraph的,Paragraph这个结构也不一定是直接隶属于Document的。比如在表格里,用来表示单元格的TableCell里,也会有Paragraph。所以呢,这里需要表示一下整个树型结构,否则以后要扩展起来就非常麻烦。那么,整个WordprocessingML基本所有元素都符合这个特性。而且在原本的代码里,Paragraph和Run都是OpenXmlCompositeElement的一个子类。
所以,新建一个CompositeElementBase,大致代码如下:
1 using System; 2 using System.Collections.Generic; 3 4 namespace Ricebird.Wordprocessing 5 { 6 public abstract class CompositeElementBase 7 { 8 #region ctor 9 protected CompositeElementBase BaseElement 10 { 11 get; 12 set; 13 } 14 15 protected WordDocument Document 16 { 17 get; 18 set; 19 } 20 21 protected List<CompositeElementBase> Children 22 { 23 get; set; 24 } = new List<CompositeElementBase>(); 25 26 public CompositeElementBase(CompositeElementBase @base) 27 { 28 if (@base == null) 29 { 30 throw new NullReferenceException("@base 不能为 null"); 31 } 32 BaseElement = @base; 33 Document = @base.Document; 34 } 35 36 public CompositeElementBase(WordDocument doc) 37 { 38 Document = doc; 39 BaseElement = null; 40 } 41 #endregion 42 43 #region 流式接口 44 public abstract CompositeElementBase AppendParagraph(); 45 public abstract CompositeElementBase Append(string text); 46 #endregion 47 } 48 }
这里注意,构造函数需要两个。因为一种情况是像顶级段落一样没有上级元素的。他们直接隶属于Document。接着,实现WordParagraph和WordRun。篇幅所限,就只放一个Run。
using DocumentFormat.OpenXml.Wordprocessing; namespace Ricebird.Wordprocessing { public class WordRun : CompositeElementBase { private Run InternalRun { get;set; } public WordRun(Run r, CompositeElementBase @base) : base(@base) { InternalRun = r; } #region 读取文本 public void SetText(string text) { Text t = InternalRun.GetFirstChild<Text>(); if (t == null) { t = new Text(); InternalRun.Append(t); } t.Text = text; } public string GetText() { Text t = InternalRun.GetFirstChild<Text>(); return t?.Text ?? string.Empty; } #endregion #region 流式接口 public override CompositeElementBase Append(string text) { return BaseElement.Append(text); } public override CompositeElementBase AppendParagraph() { return BaseElement.AppendParagraph(); } #endregion } }
这个都是非常简单的代码,然后运行这个程序。就会发现:
对比一下我放在下面的代码图,是不是有哪里不对?比如说,空格呢?于是,再去翻阅文档。就可以从Text这个对象的属性里翻出一个叫Space的属性(https://docs.microsoft.com/zh-cn/dotnet/api/documentformat.openxml.wordprocessing.texttype.space)。他说了,不设定这个属性值,那么空格是不会显示的!所以,改动一下SetText函数:
1 public void SetText(string text) 2 { 3 Text t = InternalRun.GetFirstChild<Text>(); 4 if (t == null) 5 { 6 t = new Text(); 7 InternalRun.Append(t); 8 } 9 t.Text = text; 10 11 if (text.Contains(" ")) 12 { 13 t.Space = new DocumentFormat.OpenXml.EnumValue<DocumentFormat.OpenXml.SpaceProcessingModeValues>(DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve); 14 } 15 }
对,就是为了设置一个空格可以用,要写辣么辣么长的代码!再运行一下程序。就不截图了哈,整个代码就正常了起来。然后,考虑到在很多时候,文章的一段就是一个连续文本。所以,再添加一个AppendParagraph(string text)函数。代码就不贴了,非常简单,自己加一下就行。然后整理一下流式接口里的调用关系,能用Document的就不要用BaseElement。全部整理完,再看一眼自己的需求,把正式文本填进去:
这图左上是代码的运行图,右上是最后的效果图,下面是代码。对比两边,不能说一模一样只能说完全不像。。。。。。为什么呢?原来是。。。少了格式。那么下一篇就讲如果给文字加上格式