博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
OpenXml编程--修正Word目录页码错误
阅读量:6080 次
发布时间:2019-06-20

本文共 11103 字,大约阅读时间需要 37 分钟。

场景描述

图1

 

        图1是一个PDF文件生成的简单流程,事先做好的Word模板和数据源进行匹配以生成新的Word文档,然后再将Word文档转换为PDF文档。由Word文档和数据源产生新的Word文档我们采用的是FlexDoc组件()。生成的PDF文档要求有目录,如图2所示。目录是在Word模板中定义的,并没有采用在代码中自动生成目录的方式,这样是因为可以很方便的更改目录的样式,如图3所示。

图2

图3

     生成的Word的页码是不会自动更新的,但是会在转PDF的时候更新,这时候我们遇到了一个FlexDoc的Bug,转换后的目录产生了“未定义书签的错误”。如图4。

图4

        本文从Word目录的原理出发,探寻页码转换出错的原因,继而提出完整的解决方案。

Word目录绑定原理

      word目录有多种类型,类型是拿什么区别的呢?首先我们插入Word2007中的“自动目录2”,如图5。

图5  插入自动目录2

目录插入成功之后,我们选择目录,右键—>编辑域,切换到域编辑界面,如图6。

 

图6  编辑域

       在域编辑页面在域名项选择TOC,然后单击选项,在选项界面中我们可以看到TOC域支持的开关,不同的开关组合就是不同Word目录,如图7所示。刚才我们选择的“自动目录2”的域代码为TOC \o "1-3" \h \z \u 。关于各个开关的含义,您自己看说明就可以 了,我就不啰嗦了。

图7   编辑域选项

下面我们从WordML的角度继续研究目录。打开word文档,找到Body节点,再找到W:sdt节点,如图8。

图8  找到w:sdt节点

w:sdt节点代表SdtBlock,SdtBlock又是什么呢?就是包在目录外面的那个框,SdtBlock并不是word目录必须的元素,插入自动目录 的时候word默认会将目录放在SdtBlock中,您也可以选择去除,由于SdtBlock可以帮助我们在程序中迅速找到目录项,所以我要去所有的目标中的目录必须带SdtBlock。SdtBlock节点下有一个w:sdtContent (对应的对象为SdtContentBlock)子节点,该子节点下包含了多个w:p(对应的对象为Paragraph)标签,这些w:p标签组成了Word目录。现在我们展开其中一个w:p,看看里面包含了什么秘密。

代码清单1   一个目录项

1:  
2:    
3:      
4:      
5:        
6:        
7:      
8:    
9:    
10:      
11:        
12:          
13:          
14:          
15:        
16:        
作答有效性分析
17:      
18:      
19:        
20:          
21:          
22:        
23:        
24:      
25:      
26:        
27:          
28:          
29:        
30:        
31:      
32:      
33:        
34:          
35:          
36:        
37:        
PAGEREF _Toc296003347 \h
38:      
39:      
40:        
41:          
42:          
43:        
44:      
45:      
46:        
47:          
48:          
49:        
50:        
51:      
52:      
53:        
54:          
55:          
56:        
57:        
1
58:      
59:      
60:        
61:          
62:          
63:        
64:        
65:      
66:    
67:  

        代码清单1是w:sdtContent 中的一个w:p项内容。现在我们来看里面几个关键项。第9行代码“<w:hyperlink w:history="1" w:anchor="_Toc296003347">”是w:hyperlink(对应的对象为Hyperlink )标记的起始配置,w:hyperlink代表超链接,点击目录会自动跳转到文档中的正确位置,如果您的TOC域支持的开关没有“\h”选项的话是不会产生w:hyperlink标签的,那么您看到的目录项的代码是另一种样子,这里我就不演示了。这里我们重点关注w:anchor属性,该属性指定了超链接的位置。那么w:anchor的值"_Toc296003347"又是什么呢?先不做解释,我们再看另一个标记,第37行的“<w:instrText xml:space="preserve"> PAGEREF _Toc296003347 \h</w:instrText>”,w:instrText(对应的对象为FieldCode)标签的值 “PAGEREF _Toc296003347 \h ”是用来标识超链接的页码的,但是它本身并没有页码值,而是引用了一个位置,最后更新页码的时候会将那个位置所在页的页码赋值给第57行的<w:t>。第50行的<w:fldChar w:fldCharType="separate" />标签是目录项的标题和页码之间的分隔符样式。第16行的“<w:t>作答有效性分析</w:t>”就是当前目录项的标题,实现显示的是word文档正文中的1级 、二级或3级标题。

    现在我们基本了解了目录的组成,还有一个关键的定位属性没有解释,我们继续查看word文档,看下面这一段代码:

代码2   一个二级标题

1:  
2:    
3:      
4:      
5:      
6:        
7:        
8:        
9:        
10:        
11:      
12:    
13:    
14:    
15:    
16:      
17:        
18:        
19:        
20:        
21:        
22:      
23:      
作答有效性分析
24:    
25:    
26:    
27:  
看代码2所示的内容,实际上是一个二级标题,该二级标题包含在一个单独的
标记内,从哪里能看出该内容的大纲级别是二级呢?看第3行代码---
。 然后我们看第13、1
4、25和26四行代码,是两对w:bookmarkStart 和bookmarkEnd标签,第14行的w:name="_Toc296003347"是不是很眼熟呢?没错,就是目录项中的定位标记。
到现在为止,我们已经明白了目录的原理,那么为什么会出错呢?我们看一个出错的Word文档,如图9。
 
图9  页码更新出错的Word文档
看图9中,比较突出是几个w:bookmarkStart 标签,它们本应该是如代码2里那样,和bookmarkEnd标签一起成对的出现在P标签内然后上学包裹标题,但是现在它却单独跑到了P标签外 ,如果bookmarkEnd标签单独的跑出来也会造成页码更新失败。代码3是标题的内容,我们可以看到只剩下两个孤零零的bookmarkEnd标签。这就是出错的原因。
整体测评结果

修正策略

     问题我们已经分析清楚了,其实这是FlexDoc的bug,当然我们可以通过修改FlexDoc的源代码来解决这个问题,但是我实在是懒得读源码,决定在FlexDoc匹配数据之后将word文档写在磁盘上之前来修正目录。流程如下:

 

代码实现

代码很简单,全部代码如下所示:

1:   public static void FixtDirectory(WordprocessingDocument wdDoc)
2:          {
3:              Body body = wdDoc.MainDocumentPart.Document.Body;
4:              //获取所有包含一、二级标题的段落
5:              var parHasStyle = body.Descendants
().Where(t => t.Descendants
().Count() > 0 && t.Descendants
().All(c => c.Val == "1" || c.Val == "2"));
6:              string bookMarkName = "_Toc{0}";
7:              int num = 988888888;
8:              Dictionary
bookMarkAddedDic = new Dictionary
();
9:   
10:              if (parHasStyle.Count() > 0)
11:              {
12:                  foreach (Paragraph p in parHasStyle)
13:                  {
14:                      var bookmarkEnds = p.Descendants
();//获取段落中所有BookmarkEnd标签
15:                      var bookmarkStarts = p.Descendants
();//获取段落中所有BookmarkStart标签
16:                      int bookmarkEndsCount = bookmarkEnds.Count();
17:                      int bookmarkStartsCount = bookmarkStarts.Count();
18:                      string name = string.Format(bookMarkName, ++num);
19:                      string id = (num++).ToString();
20:   
21:                      //创建新书签用于添加到标题上下
22:                      BookmarkStart bookmarkStart = new BookmarkStart() { Name = name, Id = id };
23:                      BookmarkEnd bookmarkEnd = new BookmarkEnd() { Id = id };
24:   
25:                      if (bookmarkEndsCount == 0 && bookmarkStartsCount == 0)
26:                      {
27:                          if (p.Descendants
().Count() > 0)
28:                          {
29:                              AddBookMarkToParagraph(p, bookmarkEnd, bookmarkStart);//添加书签
30:                              bookMarkAddedDic.Add(p.Descendants
().First().Text, name);//记录添加的书签
31:                          }
32:                      }
33:                      else
34:                          if (bookmarkEndsCount != bookmarkStartsCount)
35:                          {
36:                              DeleteBookMarkFromParagraph(body, p, bookmarkStarts, bookmarkEnds);//删除孤单书签
37:                              AddBookMarkToParagraph(p, bookmarkEnd, bookmarkStart);//添加新书签
38:                              string dicKey = GetKey(p);//获取被添加书签的标题
39:                              bookMarkAddedDic.Add(dicKey, name);//记录添加的书签
40:                          }
41:                  }
42:                  FixtDirectory(bookMarkAddedDic, body);//更新目录
43:              }
44:   
45:          }
46:   
47:          /// 
48:          /// 将段落中文字拼起来得到标题内容
49:          /// 
50:          /// 
51:          /// 
52:          private static string GetKey(Paragraph p)
53:          {
54:              return string.Join("", p.Descendants
().Select(t => t.Text));
55:          }
56:   
57:          /// 
58:          /// 修正书签
59:          /// 
60:          /// 
61:          /// 
62:          private static void FixtDirectory(Dictionary
bookMarkAddedDic, Body body)
63:          {
64:              if (bookMarkAddedDic.Count > 0)
65:              {
66:                  if (body.Descendants
().Count() > 0)
67:                  {
68:                      //得到SdtContentBlock
69:                      SdtContentBlock sdtContentBlock = body.Descendants
().First().GetFirstChild
();
70:                      //遍历每一个超链接,修改里面的书签值
71:                      foreach (Hyperlink hyperlink in sdtContentBlock.Descendants
())
72:                      {
73:   
74:                          Text text = hyperlink.Descendants
().First();//得到目录项绑定的标题内容
75:                          if (bookMarkAddedDic.Keys.Contains(text.Text))
76:                          {
77:                              hyperlink.Anchor = bookMarkAddedDic[text.Text];//超链接绑定到书签的name
78:                              FieldCode pageRef = hyperlink.Descendants
().First(t => t.Text.Contains("PAGEREF"));//
79:                              pageRef.Text = "PAGEREF " + hyperlink.Anchor + "\\h";//更新PAGEREF以更新页码
80:                          }
81:   
82:                      }
83:                  }
84:   
85:              }
86:   
87:          }
88:   
89:          /// 
90:          /// 删除孤单标签
91:          /// 
92:          /// 
93:          /// 
94:          /// 
95:          /// 
96:          private static void DeleteBookMarkFromParagraph(Body body, Paragraph p, IEnumerable
bookmarkStarts, IEnumerable
bookmarkEnds)
97:          {
98:              IEnumerable
singleStartElenmentsIn = null;
99:              IEnumerable
singleEndElenmentsIn = null;
100:              IEnumerable
singleStartElenmentsOut = null;
101:              IEnumerable
singleEndElenmentsOut = null;
102:   
103:              singleStartElenmentsIn = bookmarkStarts.Where(t => !bookmarkEnds.Select(c => c.Id.Value).Contains(t.Id.Value));//获得段落内的孤单BookmarkStart标签
104:              List
bookmarkStartsLst = singleStartElenmentsIn.ToList();
105:              singleEndElenmentsIn = bookmarkEnds.Where(t => !bookmarkStartsLst.Select(c => c.Id.Value). Contains(t.Id.Value));//获得段落内的孤单BookmarkEnd标签
106:   
107:              singleStartElenmentsOut = body.Descendants
().Where(t => singleEndElenmentsIn. Select(c => c.Id.Value).Contains(t.Id.Value));//获得段落外的孤单BookmarkStart标签
108:              singleEndElenmentsOut = body.Descendants
().Where(t => singleStartElenmentsIn. Select(c => c.Id.Value).Contains(t.Id.Value));//获得段落外的孤单BookmarkEnd标签
109:   
110:              //删除所有孤单标签
111:              Remove(singleStartElenmentsOut);
112:              Remove(singleEndElenmentsOut);
113:              Remove(singleStartElenmentsIn);
114:              Remove(singleEndElenmentsIn);
115:   
116:          }
117:   
118:          private static void Remove(IEnumerable
singleElenments)
119:          {
120:              singleElenments.ToList().ForEach(t => t.Remove());//删除标签
121:          }
122:   
123:   
124:          /// 
125:          /// 添加新的标签到段落中标题上下
126:          /// 
127:          /// 
128:          /// 
129:          /// 
130:          private static void AddBookMarkToParagraph(Paragraph p, BookmarkEnd bookmarkEnd, BookmarkStart bookmarkStart)
131:          {
132:              if (p.Descendants
().Count() > 0)
133:              {
134:                  var wtBegin = p.Descendants
().First();
135:                  var wtEnd = p.Descendants
().Last();
136:                  Run rBegin = wtBegin.Parent as Run;//得到标题内容开始行
137:                  Run rEnd = wtEnd.Parent as Run;//得到标题内容结束行
138:   
139:                  rBegin.InsertBeforeSelf(bookmarkStart);//在标题上面插入BookmarkStart
140:                  rEnd.InsertAfterSelf(bookmarkEnd);//在标题下面插入bookmarkEnd
141:              }
142:          }

代码很少,我将说明加在注释上,相信各位都能看的懂。最后还希望大家踊跃留言讨论。谢谢!

本文转自悬魂博客园博客,原文链接:http://www.cnblogs.com/xuanhun/archive/2011/06/16/2083061.html,如需转载请自行联系原作者

你可能感兴趣的文章
atitit.细节决定成败的适合情形与缺点
查看>>
Mysql利用binlog恢复数据
查看>>
我的友情链接
查看>>
用yum安装mariadb
查看>>
一点IT"边缘化"的人的思考
查看>>
WPF 降低.net framework到4.0
查看>>
搭建一个通用的脚手架
查看>>
开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
查看>>
开源磁盘加密软件VeraCrypt教程
查看>>
本地vs云:大数据厮杀的最终幸存者会是谁?
查看>>
阿里云公共镜像、自定义镜像、共享镜像和镜像市场的区别 ...
查看>>
shadowtunnel v1.7 发布:新增上级负载均衡支持独立密码
查看>>
Java线程:什么是线程
查看>>
mysql5.7 创建一个超级管理员
查看>>
【框架整合】Maven-SpringMVC3.X+Spring3.X+MyBatis3-日志、JSON解析、表关联查询等均已配置好...
查看>>
要想成为高级Java程序员需要具备哪些知识呢?
查看>>
带着问题去学习--Nginx配置解析(一)
查看>>
onix-文件系统
查看>>
java.io.Serializable浅析
查看>>
我的友情链接
查看>>