场景描述

图1

图1是一个PDF文件生成的简单流程,事先做好的Word模板和数据源进行匹配以生成新的Word文档,然后再将Word文档转换为PDF文档。由Word文档和数据源产生新的Word文档我们采用的是FlexDoc组件(http://flexdoc.codeplex.com/)。生成的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:  <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。

图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文档写在磁盘上之前来修正目录。流程如下:

代码实现

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

   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,如需转载请自行联系原作者

OpenXml编程--修正Word目录页码错误相关推荐

  1. Latex之目录页码错误

    由于使用的模板编辑论文,发现生成的目录出现页码错误,进而导致产生的书签链接错误.查阅模板的命令定义,发现是命令\BiAppendixChapter的定义出现问题: \newcommand{\BiApp ...

  2. 目录页码错误未定义书签怎么解决_目录页码对不齐应该怎么办?这2种方法,工作效率大增...

    在制作文档目录时,你有没有遇到下图情况:右侧页码不对齐或者左侧文本不对齐,你一般是如何处理的,与大家分享相关的2种解决办法.工作效率大增! 1.目录页码不对齐该怎么办? 遇到这种情况,一般是制表位出现 ...

  3. 快速对齐word目录页码

    下图中是word中的目录,但是页码还没有对齐,这个时候要是一个一个点输入,那可是相当的麻烦,如下图: 接下来小编开始介绍自己的小技巧,首先选中所有的目录,然后按照下图中红色箭头步骤2点击[段落设置], ...

  4. word文档点击打印,目录页码却全是2的原因分析及解决办法

    最近也是临近毕业,在忙着毕业设计定稿,在这个过程,同学w遇见了一个很奇怪的问题,就是每次点击文件目录下的打印功能时,自己论文的目录,全部都变为了2.有时候重新打开,又自动好了,有时候,又不是2,是10 ...

  5. word排版之生成目录页码不右对齐

    原文地址为: word排版之生成目录页码不右对齐 用word生成目录时,由于某种原因造成目录的页码参差不齐,页码没有靠右对齐,如上图. 这时可以再生成目录对话框中修改相关选项来更正这个错误. 在< ...

  6. java使用Aspose.word保存word更新目录页码报错以及样式错乱解决

    保存文件之前,使用aspose.word中的这个方法: Document.updateFields() 更新域时会更新目录,但是页码可能会有偏差,原因是无法保证域的更新顺序,目录可能不是最后一个更新的 ...

  7. word目录生成与页码处理

    word目录生成与页码处理: 一.设置标题格式 1.选中文章中的所有一级标题: 2.在"格式"工具栏的左端,"样式"列表中单击"标题1". ...

  8. word插页码 目录不同编号

    word插页码 目录不同编号 在帮别人调word格式,主要是怕自己以后不记得 点一下圈圈那个按钮,等一下比较方便看分节符是怎么样的 光标到目录最后 插入分节符,选择下一页这个 5.不用贴这么紧也行,反 ...

  9. 【word wps文字】目录页码中的格式在打印或打印预览时变为和正文页码格式一样,如何调整?

    一.问题背景 之前在闲鱼上,有个人找我改word排版,有一个需求就是正文页码两边需要横杠. 但是目录中显示的页码,不需要横杠. 我当时是一个一个在目录中删除横杠的,借助了查找与替换功能. 更改后,目录 ...

  10. Word 论文页码、页眉、目录等设置

    Word 论文页码.页眉.目录等设置 逻辑 1 页码.页眉 2 目录 页码.页眉 1 将光标移到指定页面(指定页码页面的起始页)的第一个字符所在的位置前. 2 分节符 - 下一页 注: (1)显示分节 ...

最新文章

  1. 发现价值(1)-无限的网络资源
  2. 虚拟机增强工具的安装
  3. Android“应用克隆”漏洞分析
  4. CSLA.Net 3.0.5 项目管理示例 业务集合基类(ProjectResources.cs,ProjectResource.cs)
  5. 名企进名校精选IT人 07年毕业生就业看好
  6. background-size
  7. [转载] Java实现归并排序(超详细,新手请进)
  8. 95-38-030-Buffer-Java NIO中-关于DirectBuffer,HeapBuffer的疑问
  9. GCC 编译 --sysroot
  10. 编程猫海龟编辑器python_编程猫海龟编辑器
  11. 张珺 2015/07/13 个人文档
  12. 内置函数(内嵌函数或内联函数)
  13. SmartSens在ISSCC 2019 图像传感器技术领域报告会作开场报告,收录论文抢先披露
  14. web常见的五种前端布局方式
  15. Python:体脂计算
  16. 把 Win 8.1 升级成 Windows 2012 R2 (再续)
  17. GroovyGrails
  18. 番茄助手Assist X 快捷键
  19. java秒表计时器_Java-计时器/秒表GUI
  20. 第二届“大数据在清华”高峰论坛,敬请期待!

热门文章

  1. 给广大学习单片机的同学心得,如何学好单片机
  2. 边缘计算是什么 优点
  3. 微信小程序消息通知-打卡考勤
  4. 关于Efficient Subgraph Matching by Postponing Cartesian的批注
  5. VS中给qt按钮添加图标
  6. 2017-06-15 前端日报
  7. 前端Vue、后端SSM、前后端分离项目服务器部署实战
  8. 前端面试八股文(超详细)
  9. 网络安全等级保护合规一览
  10. python高级索引