您的位置:首页 > 编程语言

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

2011-06-16 20:42 204 查看

场景描述





图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一个目录项

<w:pw:rsidRPr="00F34D5F"w:rsidR="00F34D5F"w:rsidRDefault="00F34D5F"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">

[code]<w:pPr>
<w:pStylew:val="20"/>

<w:rPr>

<w:rFontsw:asciiTheme="minorHAnsi"w:hAnsiTheme="minorHAnsi"w:eastAsiaTheme="minorEastAsia"/>

<w:colorw:val="auto"/>

</w:rPr>

</w:pPr>

<w:hyperlinkw:history="1"w:anchor="_Toc296003347">

<w:rw:rsidRPr="00F34D5F">

<w:rPr>

<w:rStylew:val="ad"/>

<w:rFontsw:hint="eastAsia"/>

<w:colorw:val="auto"/>

</w:rPr>

<w:t>作答有效性分析</w:t>

</w:r>

<w:rw:rsidRPr="00F34D5F">

<w:rPr>

<w:webHidden/>

<w:colorw:val="auto"/>

</w:rPr>

<w:tab/>

</w:r>

<w:rw:rsidRPr="00F34D5F">

<w:rPr>

<w:webHidden/>

<w:colorw:val="auto"/>

</w:rPr>

<w:fldCharw:fldCharType="begin"/>

</w:r>

<w:rw:rsidRPr="00F34D5F">

<w:rPr>

<w:webHidden/>

<w:colorw:val="auto"/>

</w:rPr>

<w:instrTextxml:space="preserve">PAGEREF_Toc296003347\h</w:instrText>

</w:r>

<w:rw:rsidRPr="00F34D5F">

<w:rPr>

<w:webHidden/>

<w:colorw:val="auto"/>

</w:rPr>

</w:r>

<w:rw:rsidRPr="00F34D5F">

<w:rPr>

<w:webHidden/>

<w:colorw:val="auto"/>

</w:rPr>

<w:fldCharw:fldCharType="separate"/>

</w:r>

<w:rw:rsidRPr="00F34D5F">

<w:rPr>

<w:webHidden/>

<w:colorw:val="auto"/>

</w:rPr>

<w:t>1</w:t>

</w:r>

<w:rw:rsidRPr="00F34D5F">

<w:rPr>

<w:webHidden/>

<w:colorw:val="auto"/>

</w:rPr>

<w:fldCharw:fldCharType="end"/>

</w:r>

</w:hyperlink>
</w:p>

[/code]

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

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

代码2一个二级标题

<w:pw:rsidRPr="00F34D5F"w:rsidR="000535A9"w:rsidP="00F34D5F"w:rsidRDefault="00E24DF2"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">


<w:pPr>


<w:pStylew:val="2"/>


<w:indw:firstLine="372"w:firstLineChars="133"/>



<w:rPr>


<w:rFontsw:ascii="微软雅黑"w:hAnsi="微软雅黑"w:eastAsia="微软雅黑"w:cstheme="minorBidi"/>


<w:bCsw:val="0"/>


<w:colorw:val="93550D"/>


<w:szw:val="28"/>


<w:szCsw:val="24"/>



</w:rPr>



</w:pPr>


<w:bookmarkStartw:name="_Toc295939763"w:id="3"/>


<w:bookmarkStartw:name="_Toc296003347"w:id="4"/>


<w:rw:rsidRPr="00F34D5F">



<w:rPr>


<w:rFontsw:hint="eastAsia"w:ascii="微软雅黑"w:hAnsi="微软雅黑"w:eastAsia="微软雅黑"w:cstheme="minorBidi"/>


<w:bCsw:val="0"/>


<w:colorw:val="93550D"/>


<w:szw:val="28"/>


<w:szCsw:val="24"/>



</w:rPr>


<w:t>作答有效性分析</w:t>


</w:r>


<w:bookmarkEndw:id="3"/>


<w:bookmarkEndw:id="4"/>


</w:p>


看代码2所示的内容,实际上是一个二级标题,该二级标题包含在一个单独的<w:p>标记内,从哪里能看出该内容的大纲级别是二级呢?看第3行代码---<w:pStylew: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:pw:rsidRPr="00115C2B"w:rsidR="009E7404"w:rsidP="009A7ED0"w:rsidRDefault="00BD76F7"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:pPr>
<w:pStylew:val="1"/>
<w:jcw:val="center"/>
<w:rPr>
<w:rFontsw:ascii="微软雅黑"w:hAnsi="微软雅黑"w:cstheme="majorBidi"/>
<w:colorw:val="365F91"w:themeColor="accent1"w:themeShade="BF"/>
<w:kernw:val="0"/>
<w:langw:val="zh-CN"/>
</w:rPr>
</w:pPr>
<w:rw:rsidRPr="00115C2B">
<w:rPr>
<w:rFontsw:hint="eastAsia"w:ascii="微软雅黑"w:hAnsi="微软雅黑"w:cstheme="majorBidi"/>
<w:colorw:val="365F91"w:themeColor="accent1"w:themeShade="BF"/>
<w:kernw:val="0"/>
<w:langw:val="zh-CN"/>
</w:rPr>
<w:t>整体测评结果</w:t>
</w:r>
<w:bookmarkEndw:id="2"/>
<w:bookmarkEndw:id="1"/>
</w:p>


修正策略

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





代码实现

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

publicstaticvoidFixtDirectory(WordprocessingDocumentwdDoc)

[code]{
Bodybody=wdDoc.MainDocumentPart.Document.Body;

//获取所有包含一、二级标题的段落

varparHasStyle=body.Descendants<Paragraph>().Where(t=>t.Descendants<ParagraphStyleId>().Count()>0&&
t.Descendants<ParagraphStyleId>().All(c=>c.Val=="1"||c.Val=="2"));

stringbookMarkName="_Toc{0}";

intnum=988888888;

Dictionary<string,string>bookMarkAddedDic=newDictionary<string,string>();


if(parHasStyle.Count()>0)

{

foreach(ParagraphpinparHasStyle)

{

varbookmarkEnds=p.Descendants<BookmarkEnd>();//获取段落中所有BookmarkEnd标签

varbookmarkStarts=p.Descendants<BookmarkStart>();//获取段落中所有BookmarkStart标签

intbookmarkEndsCount=bookmarkEnds.Count();

intbookmarkStartsCount=bookmarkStarts.Count();

stringname=string.Format(bookMarkName,++num);

stringid=(num++).ToString();


//创建新书签用于添加到标题上下

BookmarkStartbookmarkStart=newBookmarkStart(){Name=name,Id=id};

BookmarkEndbookmarkEnd=newBookmarkEnd(){Id=id};


if(bookmarkEndsCount==0&&bookmarkStartsCount==0)

{

if(p.Descendants<Text>().Count()>0)

{

AddBookMarkToParagraph(p,bookmarkEnd,bookmarkStart);//添加书签

bookMarkAddedDic.Add(p.Descendants<Text>().First().Text,name);//记录添加的书签

}

}

else

if(bookmarkEndsCount!=bookmarkStartsCount)

{

DeleteBookMarkFromParagraph(body,p,bookmarkStarts,bookmarkEnds);//删除孤单书签

AddBookMarkToParagraph(p,bookmarkEnd,bookmarkStart);//添加新书签

stringdicKey=GetKey(p);//获取被添加书签的标题

bookMarkAddedDic.Add(dicKey,name);//记录添加的书签

}

}

FixtDirectory(bookMarkAddedDic,body);//更新目录

}


}


///<summary>

///将段落中文字拼起来得到标题内容

///</summary>

///<paramname="p"></param>

///<returns></returns>

privatestaticstringGetKey(Paragraphp)

{

returnstring.Join("",p.Descendants<Text>().Select(t=>t.Text));

}


///<summary>

///修正书签

///</summary>

///<paramname="bookMarkAddedDic"></param>

///<paramname="body"></param>

privatestaticvoidFixtDirectory(Dictionary<string,string>bookMarkAddedDic,Bodybody)

{

if(bookMarkAddedDic.Count>0)

{

if(body.Descendants<SdtBlock>().Count()>0)

{

//得到SdtContentBlock

SdtContentBlocksdtContentBlock=body.Descendants<SdtBlock>().First().GetFirstChild<SdtContentBlock>();

//遍历每一个超链接,修改里面的书签值

foreach(HyperlinkhyperlinkinsdtContentBlock.Descendants<Hyperlink>())

{


Texttext=hyperlink.Descendants<Text>().First();//得到目录项绑定的标题内容

if(bookMarkAddedDic.Keys.Contains(text.Text))

{

hyperlink.Anchor=bookMarkAddedDic[text.Text];//超链接绑定到书签的name

FieldCodepageRef=hyperlink.Descendants<FieldCode>().First(t=>t.Text.Contains("PAGEREF"));//

pageRef.Text="PAGEREF"+hyperlink.Anchor+"\\h";//更新PAGEREF以更新页码

}


}

}


}


}


///<summary>

///删除孤单标签

///</summary>

///<paramname="body"></param>

///<paramname="p"></param>

///<paramname="bookmarkStarts"></param>

///<paramname="bookmarkEnds"></param>

privatestaticvoidDeleteBookMarkFromParagraph(Bodybody,Paragraphp,IEnumerable<BookmarkStart>bookmarkStarts,
IEnumerable<BookmarkEnd>bookmarkEnds)

{

IEnumerable<BookmarkStart>singleStartElenmentsIn=null;

IEnumerable<BookmarkEnd>singleEndElenmentsIn=null;

IEnumerable<BookmarkStart>singleStartElenmentsOut=null;

IEnumerable<BookmarkEnd>singleEndElenmentsOut=null;


singleStartElenmentsIn=bookmarkStarts.Where(t=>
!bookmarkEnds.Select(c=>c.Id.Value).Contains(t.Id.Value));//获得段落内的孤单BookmarkStart标签

List<BookmarkStart>bookmarkStartsLst=singleStartElenmentsIn.ToList();

singleEndElenmentsIn=bookmarkEnds.Where(t=>!bookmarkStartsLst.Select(c=>c.Id.Value).
Contains(t.Id.Value));//获得段落内的孤单BookmarkEnd标签


singleStartElenmentsOut=body.Descendants<BookmarkStart>().Where(t=>singleEndElenmentsIn.
Select(c=>c.Id.Value).Contains(t.Id.Value));//获得段落外的孤单BookmarkStart标签

singleEndElenmentsOut=body.Descendants<BookmarkEnd>().Where(t=>singleStartElenmentsIn.
Select(c=>c.Id.Value).Contains(t.Id.Value));//获得段落外的孤单BookmarkEnd标签


//删除所有孤单标签

Remove(singleStartElenmentsOut);

Remove(singleEndElenmentsOut);

Remove(singleStartElenmentsIn);

Remove(singleEndElenmentsIn);


}


privatestaticvoidRemove(IEnumerable<OpenXmlElement>singleElenments)

{

singleElenments.ToList().ForEach(t=>t.Remove());//删除标签

}



///<summary>

///添加新的标签到段落中标题上下

///</summary>

///<paramname="p"></param>

///<paramname="bookmarkEnd"></param>

///<paramname="bookmarkStart"></param>

privatestaticvoidAddBookMarkToParagraph(Paragraphp,BookmarkEndbookmarkEnd,BookmarkStartbookmarkStart)

{

if(p.Descendants<Text>().Count()>0)

{

varwtBegin=p.Descendants<Text>().First();

varwtEnd=p.Descendants<Text>().Last();

RunrBegin=wtBegin.ParentasRun;//得到标题内容开始行

RunrEnd=wtEnd.ParentasRun;//得到标题内容结束行


rBegin.InsertBeforeSelf(bookmarkStart);//在标题上面插入BookmarkStart

rEnd.InsertAfterSelf(bookmarkEnd);//在标题下面插入bookmarkEnd

}

}

[/code]
代码很少,我将说明加在注释上,相信各位都能看的懂。最后还希望大家踊跃留言讨论。谢谢!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: