OpenXml编程--去除自动生成的word文档中由分页符和换行符产生的空白页
前言
前置知识:OpenXml
首先描述下问题产生的场景。我们的业务需求是根据用户的在线作答(或导入的作答结果)数据批量产生报告。产生报告的方式是把通过工作流控制的复杂业务逻辑的产出--分析结果--和Word模板进行匹配产生新的word文档。接下来根据word文档按需生成Pdf、flash或者Html等其他文档。图-1是我们的word模板的截图。
图1 word模板截图
图中所示为一个模板的一部分,其中的自己定义标签会被报告生成程序替换为指定的数据,然后生成新的docx文档。不同的报告会由专业的设计人员设计word的排版和样式,其中为了分隔报告的不同部分,会经常使用分页符,如图2所示。
图2 分页符
分页符的作用我这里就不细说了,各位应该比我更熟悉,不论当前页剩余多少空白,分页符后面的内容都会被强制在下一页显示。分页符本身没有什么问题,但是当分页符上面的内容正好满一页 之后,分页符就会被挤压到下一页,那么分页符所在的页就不会有任何文字,形成一个空白页,如图3所示。
图3 由分页符形成的空白页
因为我们事先无法判断要绑定内容的多少,分页符没办法和自定义标签和谐的相处,每部分都可能产生空白页。
表格和有些自定义标签会产生一个换行符,而且无法有效的在制作模板阶段删除,那么如果最后一页的内容正好满一页的话,就会把这个换行符挤压下来,形成一个空白页,如图4所示。
图4 由换行符产生的空白页
在某些特殊的情况下,我们可以这样来处理由分页符产生空白页问题。
图5 让分页符跟在最后一行文字之后
如果让分页符跟在文字之后,可以很好的解决问题,似乎可以手工的解决这个问题,最终我们放弃了手工修改的计划,原因如下:
1) 特殊的自定义标签无法和分页符如图5那样和谐共处;
2) 已经开发好的大量模板都要重新修改,测试人员要重新测试,工作量很大;
3)无法解决由换行符带来的空白页问题
如果能在数据完全替换模板中的标签生成新的 word之后,我们再来在程序中将分页符转移到它上面的文字末尾,然后再删除最后一个换行符是不是就解决问题了呢?
正文
为了研究上诉问题,我们首先建立一个简单的word文档,内容如图6所示。
图6 分页符示例文档
图7 查看文档的内容
打开body,WordML的内容如代码清单1-1.
1: <w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2: <w:p w:rsidR="00373B8D" w:rsidP="004B265D" w:rsidRDefault="004B265D">
3: <w:pPr>
4: <w:rPr>
5: <w:rFonts w:hint="eastAsia" />
6: </w:rPr>
7: </w:pPr>
8: <w:r>
9: <w:rPr>
10: <w:rFonts w:hint="eastAsia" />
11: </w:rPr>
12: <w:t>你好</w:t>
13: </w:r>
14: </w:p>
15: <w:p w:rsidR="004B265D" w:rsidP="004B265D" w:rsidRDefault="004B265D">
16: <w:r>
17: <w:rPr>
18: <w:rFonts w:hint="eastAsia" />
19: </w:rPr>
20: <w:t>我是分页符</w:t>
21: </w:r>
22: </w:p>
23: <w:p w:rsidR="004B265D" w:rsidRDefault="004B265D">
24: <w:pPr>
25: <w:widowControl />
26: <w:jc w:val="left" />
27: </w:pPr>
28: <w:r>
29: <w:br w:type="page" />
30: </w:r>
31: </w:p>
32: <w:p w:rsidR="004B265D" w:rsidP="004B265D" w:rsidRDefault="004B265D">
33: <w:pPr>
34: <w:rPr>
35: <w:rFonts w:hint="eastAsia" />
36: </w:rPr>
37: </w:pPr>
38: </w:p>
39: <w:sectPr w:rsidR="004B265D" w:rsidSect="00E556CD">
40: <w:footerReference w:type="default" r:id="rId8" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" />
41: <w:pgSz w:w="11907" w:h="16839" w:code="9" />
42: <w:pgMar w:top="964" w:right="737" w:bottom="1021" w:left="737" w:header="567" w:footer="442" w:gutter="0" />
43: <w:pgNumType w:start="1" />
44: <w:cols w:space="425" />
45: <w:docGrid w:type="linesAndChars" w:linePitch="312" />
46: </w:sectPr>
47: </w:body>
“我是分页符”对应的WordML为:
1: <w:p w:rsidR="004B265D" w:rsidP="004B265D" w:rsidRDefault="004B265D">
2: <w:r>
3: <w:rPr>
4: <w:rFonts w:hint="eastAsia" />
5: </w:rPr>
6: <w:t>我是分页符</w:t>
7: </w:r>
8: </w:p>
而分页符对应的WordML为 :
1:
2:
3: <w:p w:rsidR="004B265D" w:rsidRDefault="004B265D">
4: <w:pPr>
5: <w:widowControl />
6: <w:jc w:val="left" />
7: </w:pPr>
8: <w:r>
9: <w:br w:type="page" />
10: </w:r>
11: </w:p>
12:
1: 那么我们如果想把分页符放到它上一行的文字的后面,只需把上面两段代码合并为:
2:
3: <w:p w:rsidR="004B265D" w:rsidP="004B265D" w:rsidRDefault="004B265D">
4: <w:r>
5: <w:rPr>
6: <w:rFonts w:hint="eastAsia" />
7: </w:rPr>
8: <w:t>我是分页符</w:t>
9: </w:r>
10:
11: <w:r>
12: <w:br w:type="page" />
13: </w:r>
14: </w:p>
15:
那么如何实现上面的“乾坤大挪移”呢?我们先看代码再分析。
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using DocumentFormat.OpenXml.Packaging;
6: using System.IO;
7: using DocumentFormat.OpenXml.Wordprocessing;
8:
9: namespace FilterBlankPage
10: {
11: public class WordBlankPageFilter
12: {
13: public static void Filter(Stream word)
14: {
15:
16: using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(word, true))
17: {
18: Body body = wordDocument.MainDocumentPart.Document.Body;
20: var breaks = body.Descendants<Break>();
21: List<Paragraph> ps = new List<Paragraph>();
22: foreach (var br in breaks)
23: {
24: if (br.Type == null || br.Type != BreakValues.Page)
25: continue;
26: else if (br.Parent != null && br.Parent.Parent != null)
27: {
28: Paragraph paragraph = br.Parent.Parent as Paragraph;
29: if (paragraph.Descendants<Text>().Count() == 0&¶graph.Descendants<Drawing>().Count()==0)
30: {
31: var toAppend = paragraph.ElementsBefore().Last();
32: if (toAppend.GetType().Name == "Table")
33: {
34:
35: Table t = toAppend as Table;
36:
37: TableProperties oldProperty = t.GetFirstChild<TableProperties>();
38: //TableProperties newProperty = oldProperty.CloneNode(true);
39: TablePositionProperties oldPositionProp = oldProperty.GetFirstChild<TablePositionProperties>();
40: if (oldPositionProp == null)//设置文字环绕
41: {
42: TablePositionProperties tablePositionProperties1 = new TablePositionProperties()
43: {
44: LeftFromText = 180,
45: RightFromText = 180,
46: VerticalAnchor = VerticalAnchorValues.Text,
47: //TablePositionXAlignment = HorizontalAlignmentValues.Center,
48: TablePositionY = 1
49: };
50: oldProperty.Append(tablePositionProperties1);
51: }
52: TableWidth oldWidth = oldProperty.GetFirstChild<TableWidth>();
53: if (oldWidth == null)
54: {
55: TableWidth tableWidth1 = new TableWidth() { Width = "9180", Type = TableWidthUnitValues.Dxa };
56: oldProperty.Append(tableWidth1);
57: }
58: else
59: {
60: if (oldWidth.Type == TableWidthUnitValues.Dxa)
61: {
62: oldWidth.Width = int.Parse(oldWidth.Width.Value) > 9180 ? "9180" : oldWidth.Width.Value;
63: }
64: else if (oldWidth.Type == TableWidthUnitValues.Auto)
65: {
66: oldWidth.Type = TableWidthUnitValues.Dxa;
67: oldWidth.Width = "9180";
68: }
69: }
70: }
71: else
72: {
73: Run r = new Run();
74: Break b = new Break();
75: b.Type = BreakValues.Page;
76: r.Append(b);
77: toAppend.Append(r);
78: ps.Add(paragraph);
79: }
80: }
81: }
82: }
83: ps.ForEach(t => t.Remove());
84: //尝试去除最后一个回车符
85: var lastP = body.Elements<Paragraph>().Last();
86: if (lastP.Descendants<Text>().Count() == 0)
87: {
88: lastP.Remove();
89: }
90: }
91: }
92: }
93: }
第16行代码中我们从Word文档的流数据中获取WordprocessingDocument对象,然后在第18行代码获取主文档的Body对象,一个Word文档的主要内容都会在Body中找到。在第20行代码,取得Body中所有的break标签,当break标签的type为Page时,该标签就是分页符。由于每个分页符肯定存在一个段落(p,paragraph)标签中,所以我们在循环中寻找分页符时同时将该分页符所在的P标签存储在一个列表中,最后集中删除,同时寻找p标签的上一个P标签,并在上一个p标签中添加分页符,一删一加达到“移动”的效果。如果分页符所在的段落中含有文字或者图片,我们无需删除该段落,第29行代码做了这样的判断。73行到78行代码是向p标签中添加分页符的代码。当分页符和其他内容在同一个P内的时候,会紧跟在该内容的后面而不会单独成行而造成空白页,如图-8.
图-8 转移分页符后的效果
第32行代码判断分页符所在的段落的上一个标签是不是Tabel,由于表格和P是同级标签,同时表格内不能插入分页符,所以我们要使用特殊的方式,使分页符不在表格的下方单独成段。这个特殊的手段也很简单就是设置合适的表格宽度和环绕。如图-9所示。
图-9 设置表格的环绕使分页符在表格的一侧
那么在代码中如何设置表格的环绕和宽度呢?代码如下:
1:
2: Table t = toAppend as Table;
3:
4: TableProperties oldProperty = t.GetFirstChild<TableProperties>();
5: //TableProperties newProperty = oldProperty.CloneNode(true);
6: TablePositionProperties oldPositionProp = oldProperty.GetFirstChild<TablePositionProperties>();
7: if (oldPositionProp == null)//设置文字环绕
8: {
9: TablePositionProperties tablePositionProperties1 = new TablePositionProperties()
10: {
11: LeftFromText = 180,
12: RightFromText = 180,
13: VerticalAnchor = VerticalAnchorValues.Text,
14: //TablePositionXAlignment = HorizontalAlignmentValues.Center,
15: TablePositionY = 1
16: };
17: oldProperty.Append(tablePositionProperties1);
18: }
19: TableWidth oldWidth = oldProperty.GetFirstChild<TableWidth>();
20: if (oldWidth == null)
21: {
22: TableWidth tableWidth1 = new TableWidth() { Width = "9180", Type = TableWidthUnitValues.Dxa };
23: oldProperty.Append(tableWidth1);
24: }
25: else
26: {
27: if (oldWidth.Type == TableWidthUnitValues.Dxa)
28: {
29: oldWidth.Width = int.Parse(oldWidth.Width.Value) > 9180 ? "9180" : oldWidth.Width.Value;
30: }
31: else if (oldWidth.Type == TableWidthUnitValues.Auto)
32: {
33: oldWidth.Type = TableWidthUnitValues.Dxa;
34: oldWidth.Width = "9180";
35: }
36: }
第四行代码获取的是表格属性对象,在该属性中可以设置表格宽(通过 TableWidth对象)高等属性。TablePositionProperties是表格的定位属性,设置该属性就是设置环绕。定位的属性设置各位可以参考word,如图-10所示。
图-10 表格定位属性
单单设置环绕,如果表格过宽而没有留足够的空间给分页符环绕上去也达不到环绕的效果,可以通过第22行代码所示的方式设置表格的宽度。
最后一个问题是由最后一个换行符带来的空白页 ,可以通过如下的代码获取最后一个换行符并删除掉:
1: var lastP = body.Elements<Paragraph>().Last();
2: if (lastP.Descendants<Text>().Count() == 0)
3: {
4: lastP.Remove();
5: }
还有很多灵活的运用方式,大家可以留言讨论。