OpenXml编程--修正Word目录页码错误

简介:

场景描述

image

图1

 

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

image

图2

image

图3

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

image

图4

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

Word目录绑定原理

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

image

图5  插入自动目录2

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

image 

图6  编辑域

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

image

图7   编辑域选项

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

image

图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:  <w:p w:rsidRPr="00F34D5F" w:rsidR="00F34D5F" w:rsidRDefault="00F34D5F" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
   2:    <w:pPr>
   3:      <w:pStyle w:val="20" />
   4:      <w:rPr>
   5:        <w:rFonts w:asciiTheme="minorHAnsi" w:hAnsiTheme="minorHAnsi" w:eastAsiaTheme="minorEastAsia" />
   6:        <w:color w:val="auto" />
   7:      </w:rPr>
   8:    </w:pPr>
   9:    <w:hyperlink w:history="1" w:anchor="_Toc296003347">
  10:      <w:r w:rsidRPr="00F34D5F">
  11:        <w:rPr>
  12:          <w:rStyle w:val="ad" />
  13:          <w:rFonts w:hint="eastAsia" />
  14:          <w:color w:val="auto" />
  15:        </w:rPr>
  16:        <w:t>作答有效性分析</w:t>
  17:      </w:r>
  18:      <w:r w:rsidRPr="00F34D5F">
  19:        <w:rPr>
  20:          <w:webHidden />
  21:          <w:color w:val="auto" />
  22:        </w:rPr>
  23:        <w:tab />
  24:      </w:r>
  25:      <w:r w:rsidRPr="00F34D5F">
  26:        <w:rPr>
  27:          <w:webHidden />
  28:          <w:color w:val="auto" />
  29:        </w:rPr>
  30:        <w:fldChar w:fldCharType="begin" />
  31:      </w:r>
  32:      <w:r w:rsidRPr="00F34D5F">
  33:        <w:rPr>
  34:          <w:webHidden />
  35:          <w:color w:val="auto" />
  36:        </w:rPr>
  37:        <w:instrText xml:space="preserve"> PAGEREF _Toc296003347 \h </w:instrText>
  38:      </w:r>
  39:      <w:r w:rsidRPr="00F34D5F">
  40:        <w:rPr>
  41:          <w:webHidden />
  42:          <w:color w:val="auto" />
  43:        </w:rPr>
  44:      </w:r>
  45:      <w:r w:rsidRPr="00F34D5F">
  46:        <w:rPr>
  47:          <w:webHidden />
  48:          <w:color w:val="auto" />
  49:        </w:rPr>
  50:        <w:fldChar w:fldCharType="separate" />
  51:      </w:r>
  52:      <w:r w:rsidRPr="00F34D5F">
  53:        <w:rPr>
  54:          <w:webHidden />
  55:          <w:color w:val="auto" />
  56:        </w:rPr>
  57:        <w:t>1</w:t>
  58:      </w:r>
  59:      <w:r w:rsidRPr="00F34D5F">
  60:        <w:rPr>
  61:          <w:webHidden />
  62:          <w:color w:val="auto" />
  63:        </w:rPr>
  64:        <w:fldChar w:fldCharType="end" />
  65:      </w:r>
  66:    </w:hyperlink>
  67:  </w:p>

        代码清单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:  <w:p w:rsidRPr="00F34D5F" w:rsidR="000535A9" w:rsidP="00F34D5F" w:rsidRDefault="00E24DF2" 
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
   2:    <w:pPr>
   3:      <w:pStyle w:val="2" />
   4:      <w:ind w:firstLine="372" w:firstLineChars="133" />
   5:      <w:rPr>
   6:        <w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑" w:cstheme="minorBidi" />
   7:        <w:bCs w:val="0" />
   8:        <w:color w:val="93550D" />
   9:        <w:sz w:val="28" />
  10:        <w:szCs w:val="24" />
  11:      </w:rPr>
  12:    </w:pPr>
  13:    <w:bookmarkStart w:name="_Toc295939763" w:id="3" />
  14:    <w:bookmarkStart w:name="_Toc296003347" w:id="4" />
  15:    <w:r w:rsidRPr="00F34D5F">
  16:      <w:rPr>
  17:        <w:rFonts w:hint="eastAsia" w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑" w:cstheme="minorBidi" />
  18:        <w:bCs w:val="0" />
  19:        <w:color w:val="93550D" />
  20:        <w:sz w:val="28" />
  21:        <w:szCs w:val="24" />
  22:      </w:rPr>
  23:      <w:t>作答有效性分析</w:t>
  24:    </w:r>
  25:    <w:bookmarkEnd w:id="3" />
  26:    <w:bookmarkEnd w:id="4" />
  27:  </w:p>
       看代码2所示的内容,实际上是一个二级标题,该二级标题包含在一个单独的<w:p>标记内,从哪里能看出该内容的大纲级别是二级呢?看第3行代码---<w:pStyle w:val="2" />。
然后我们看第13、1
4、25和26四行代码,是两对w:bookmarkStart 和bookmarkEnd标签,第14行的w:name="_Toc296003347"是不是很眼熟呢?没错,就是目录项中的定位标记。
    到现在为止,我们已经明白了目录的原理,那么为什么会出错呢?我们看一个出错的Word文档,如图9。
image
图9  页码更新出错的Word文档
       看图9中,比较突出是几个w:bookmarkStart 标签,它们本应该是如代码2里那样,和bookmarkEnd标签一起成对的出现在P标签内然后上学包裹标题,但是现在它却单独跑到了P标签外
,如果bookmarkEnd标签单独的跑出来也会造成页码更新失败。代码3是标题的内容,我们可以看到只剩下两个孤零零的bookmarkEnd标签。这就是出错的原因。
<w:p w:rsidRPr="00115C2B" w:rsidR="009E7404" w:rsidP="009A7ED0" w:rsidRDefault="00BD76F7" 
xmlns:w
="http://schemas.openxmlformats.org/wordprocessingml/2006/main"> <w:pPr> <w:pStyle w:val="1" /> <w:jc w:val="center" /> <w:rPr> <w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:cstheme="majorBidi" /> <w:color w:val="365F91" w:themeColor="accent1" w:themeShade="BF" /> <w:kern w:val="0" /> <w:lang w:val="zh-CN" /> </w:rPr> </w:pPr> <w:r w:rsidRPr="00115C2B"> <w:rPr> <w:rFonts w:hint="eastAsia" w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:cstheme="majorBidi" /> <w:color w:val="365F91" w:themeColor="accent1" w:themeShade="BF" /> <w:kern w:val="0" /> <w:lang w:val="zh-CN" /> </w:rPr> <w:t>整体测评结果</w:t> </w:r> <w:bookmarkEnd w:id="2" /> <w:bookmarkEnd w:id="1" /> </w:p>

修正策略

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

 image

代码实现

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

   1:   public static void FixtDirectory(WordprocessingDocument wdDoc)
   2:          {
   3:              Body body = wdDoc.MainDocumentPart.Document.Body;
   4:              //获取所有包含一、二级标题的段落
   5:              var parHasStyle = body.Descendants<Paragraph>().Where(t => t.Descendants<ParagraphStyleId>().Count() > 0 && 
t.Descendants<ParagraphStyleId>().All(c => c.Val == "1" || c.Val == "2"));
   6:              string bookMarkName = "_Toc{0}";
   7:              int num = 988888888;
   8:              Dictionary<string, string> bookMarkAddedDic = new Dictionary<string, string>();
   9:   
  10:              if (parHasStyle.Count() > 0)
  11:              {
  12:                  foreach (Paragraph p in parHasStyle)
  13:                  {
  14:                      var bookmarkEnds = p.Descendants<BookmarkEnd>();//获取段落中所有BookmarkEnd标签
  15:                      var bookmarkStarts = p.Descendants<BookmarkStart>();//获取段落中所有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<Text>().Count() > 0)
  28:                          {
  29:                              AddBookMarkToParagraph(p, bookmarkEnd, bookmarkStart);//添加书签
  30:                              bookMarkAddedDic.Add(p.Descendants<Text>().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:          /// <summary>
  48:          /// 将段落中文字拼起来得到标题内容
  49:          /// </summary>
  50:          /// <param name="p"></param>
  51:          /// <returns></returns>
  52:          private static string GetKey(Paragraph p)
  53:          {
  54:              return string.Join("", p.Descendants<Text>().Select(t => t.Text));
  55:          }
  56:   
  57:          /// <summary>
  58:          /// 修正书签
  59:          /// </summary>
  60:          /// <param name="bookMarkAddedDic"></param>
  61:          /// <param name="body"></param>
  62:          private static void FixtDirectory(Dictionary<string, string> bookMarkAddedDic, Body body)
  63:          {
  64:              if (bookMarkAddedDic.Count > 0)
  65:              {
  66:                  if (body.Descendants<SdtBlock>().Count() > 0)
  67:                  {
  68:                      //得到SdtContentBlock
  69:                      SdtContentBlock sdtContentBlock = body.Descendants<SdtBlock>().First().GetFirstChild<SdtContentBlock>();
  70:                      //遍历每一个超链接,修改里面的书签值
  71:                      foreach (Hyperlink hyperlink in sdtContentBlock.Descendants<Hyperlink>())
  72:                      {
  73:   
  74:                          Text text = hyperlink.Descendants<Text>().First();//得到目录项绑定的标题内容
  75:                          if (bookMarkAddedDic.Keys.Contains(text.Text))
  76:                          {
  77:                              hyperlink.Anchor = bookMarkAddedDic[text.Text];//超链接绑定到书签的name
  78:                              FieldCode pageRef = hyperlink.Descendants<FieldCode>().First(t => t.Text.Contains("PAGEREF"));//
  79:                              pageRef.Text = "PAGEREF " + hyperlink.Anchor + "\\h";//更新PAGEREF以更新页码
  80:                          }
  81:   
  82:                      }
  83:                  }
  84:   
  85:              }
  86:   
  87:          }
  88:   
  89:          /// <summary>
  90:          /// 删除孤单标签
  91:          /// </summary>
  92:          /// <param name="body"></param>
  93:          /// <param name="p"></param>
  94:          /// <param name="bookmarkStarts"></param>
  95:          /// <param name="bookmarkEnds"></param>
  96:          private static void DeleteBookMarkFromParagraph(Body body, Paragraph p, IEnumerable<BookmarkStart> bookmarkStarts, 
IEnumerable<BookmarkEnd> bookmarkEnds)
  97:          {
  98:              IEnumerable<BookmarkStart> singleStartElenmentsIn = null;
  99:              IEnumerable<BookmarkEnd> singleEndElenmentsIn = null;
 100:              IEnumerable<BookmarkStart> singleStartElenmentsOut = null;
 101:              IEnumerable<BookmarkEnd> singleEndElenmentsOut = null;
 102:   
 103:              singleStartElenmentsIn = bookmarkStarts.Where(t => 
!bookmarkEnds.Select(c => c.Id.Value).Contains(t.Id.Value));//获得段落内的孤单BookmarkStart标签
 104:              List<BookmarkStart> bookmarkStartsLst = singleStartElenmentsIn.ToList();
 105:              singleEndElenmentsIn = bookmarkEnds.Where(t => !bookmarkStartsLst.Select(c => c.Id.Value).
Contains(t.Id.Value));//获得段落内的孤单BookmarkEnd标签
 106:   
 107:              singleStartElenmentsOut = body.Descendants<BookmarkStart>().Where(t => singleEndElenmentsIn.
Select(c => c.Id.Value).Contains(t.Id.Value));//获得段落外的孤单BookmarkStart标签
 108:              singleEndElenmentsOut = body.Descendants<BookmarkEnd>().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<OpenXmlElement> singleElenments)
 119:          {
 120:              singleElenments.ToList().ForEach(t => t.Remove());//删除标签
 121:          }
 122:   
 123:   
 124:          /// <summary>
 125:          /// 添加新的标签到段落中标题上下
 126:          /// </summary>
 127:          /// <param name="p"></param>
 128:          /// <param name="bookmarkEnd"></param>
 129:          /// <param name="bookmarkStart"></param>
 130:          private static void AddBookMarkToParagraph(Paragraph p, BookmarkEnd bookmarkEnd, BookmarkStart bookmarkStart)
 131:          {
 132:              if (p.Descendants<Text>().Count() > 0)
 133:              {
 134:                  var wtBegin = p.Descendants<Text>().First();
 135:                  var wtEnd = p.Descendants<Text>().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,如需转载请自行联系原作者

相关文章
|
传感器 安全 物联网
深入理解 Franca IDL 在 IPC 通信中的应用
深入理解 Franca IDL 在 IPC 通信中的应用
472 1
|
API Python
Python-Docx库 | Word与Python的完美结合(附使用文档)
Python-Docx库 | Word与Python的完美结合(附使用文档)
3488 0
|
存储 关系型数据库 MySQL
轻松入门MySQL:揭秘MySQL游标,数据处理的神秘利器(16)
轻松入门MySQL:揭秘MySQL游标,数据处理的神秘利器(16)
439 0
|
机器学习/深度学习 自然语言处理 达摩院
Modelscope 工程介绍及实战演示| 学习笔记
快速学习 Modelscope 工程介绍及实战演示
Modelscope 工程介绍及实战演示| 学习笔记
|
缓存 监控 数据挖掘
亿级数据如何实现秒级响应?
本文详细介绍了瓴羊Quick BI的性能架构、性能工具和性能保障,旨在帮助企业更好地理解和使用这一商业智能工具。文章首先概述了BI产品在企业中的重要性,随后深入探讨了Quick BI的性能架构,包括应用架构、分析引擎和渲染引擎,以及其优势和测试效果。接着,文章介绍了性能工具,包括性能分析和性能诊断,帮助用户精准诊断和优化性能瓶颈。最后,文章阐述了性能保障措施,如线上监控、版本巡检和定期报告,确保系统的稳定性和高效运行。通过这些设计,Quick BI能够满足企业在不同场景下的性能需求,提升数据分析效率和决策能力。
504 3
|
文字识别 Serverless 开发工具
【全自动改PDF名】批量OCR识别提取PDF自定义指定区域内容保存到 Excel 以及根据PDF文件内容的标题来批量重命名
学校和教育机构常需处理成绩单、报名表等PDF文件。通过OCR技术,可自动提取学生信息并录入Excel,便于统计分析和存档管理。本文介绍使用阿里云服务实现批量OCR识别、内容提取、重命名及导出表格的完整步骤,包括开通相关服务、编写代码、部署函数计算和设置自动化触发器等。提供Python示例代码和详细操作指南,帮助用户高效处理PDF文件。 链接: - 百度网盘:[链接](https://pan.baidu.com/s/1mWsg7mDZq2pZ8xdKzdn5Hg?pwd=8866) - 腾讯网盘:[链接](https://share.weiyun.com/a77jklXK)
2091 5
|
开发者
HarmonayOS通过应用链接拉起指定应用
本节介绍通过应用链接跳转拉起指定应用的方法。应用链接是将用户引导至应用内特定位置或相关网页的URL,分为Deep Linking和App Linking两种方式。Deep Linking支持自定义scheme,但缺乏域名校验;App Linking限定scheme为https,并增加校验机制以确保目标应用的安全性。文中演示了创建目标应用“ArkTSDeepLinkingTarget”和拉起应用“ArkTSDeepLinkingStartup”的具体步骤,包括配置module.json5文件和编写代码实现跳转功能。更多学习资源可参考《跟老卫学HarmonyOS开发》等教程。
355 4
|
JavaScript 前端开发 Java
uniapp Android 原生插件开发(Module 扩展为例·2022)(一)
uniapp Android 原生插件开发(Module 扩展为例·2022)
2619 0
uniapp Android 原生插件开发(Module 扩展为例·2022)(一)
|
安全 数据库 开发者
告别Navicat:彻底卸载指南及注意事项
【10月更文挑战第12天】 Navicat,作为一款广受数据库管理员和开发者喜爱的数据库管理工具,以其强大的功能和用户友好的界面著称。然而,有时出于各种原因,如软件升级、更换工具或系统维护,我们需要将其从系统中卸载。本文将提供一个详细的Navicat卸载指南,确保卸载过程既彻底又安全。
2283 6
|
NoSQL Linux 网络安全
linux安装redis超级详细教程
linux安装redis超级详细教程