【3】利用Word模板生成文档的总结
阅读目录
在各类应用系统开发中,和Word相关的应用可谓相当广泛。如各类MIS系统、各种和实际业务结合紧密的系统、需要制式报表的系统等,都需要对Word进行操作,典型的应用包括:
1、内嵌Word。在系统中内嵌Word,这样,既可以利用Word强大的功能进行文档的新建、编辑、修改、排版,同时还节省了用户对于编辑器操作的学习成本,提高了文档格式的通用性。
2、Word的二次开发。通过Word自带的宏,利用VBA(Visual Basic Appplication)进行开发,实现各种复杂的自动化功能。
3、前台不显示Word操作界面,而在后台对Word文档进行操作。包括:1)读入word文档,解析内容,获取需要的数据;2)把数据写入Word模板,生成符合格式要求的Word文档。
上述应用中,前两个应用领域相对特定,且需要对Word进行深度的二次开发,本人涉猎有限,因而不进行过多的讨论。而对于第三种应用,由于Word软件的普及率非常高,基本上可以把DOC文档看作是一个通用的文档结构。同时,Word在格式控制方面功能非常强大。因此,使用Word来制作输出文件或者报表,不光格式易于控制(用户可以在Word中制作好需要的模板,替换真实数据就获得需要的输出文档或者报表),用户的接受度等方面都有很大的优势,近年来越来越受到重视。下文主要尝试讨论如何利用Word模板生成需要的Word文档的实现。
Word二次开发概况
1983年,微软发布了基于MS-DOS的Word 1.0版,至今已经30余年了。对于Word的二次开发,也是有着悠久的历史。就本人的开发经验而言,在近十年前,就已经在Visual Basic 6.0平台上,进行内嵌Word的开发,这个在当年也是非常流行的一种开发。时至今日,Word的二次开发仍然是每个开发者频繁遇到的问题。
但是,Word的开发相对于其他的二次开发,甚至于相对于同门的也很复杂的Excel来说,开发的难度都要大很多,原因来自以下方面:
1、Word 的对象结构复杂。由于Wrod有着久远的历史,这既是它的优势也是它的包袱,它必须要保持足够的兼容性,因此DOC文档结构也就变得非常的复杂了。在Word中,有着复杂的对象结构,如Application、Document、BookMarks、Range、Selection、Paragraph等,它们之间既有层级关系,还有嵌套关系,有时为了一个小小的功能,却无法找到操作的对象。
Word 的对象结构
2、Word功能复杂。作为微软的拳头产品,多年以来,Word的功能越来越强大。尽管大多数的功能对于二次开发来说是完全用不到的,但还得去了解和学习,这就需要付出额外的代价。以Find为例,其参数竟然高达15个,如下所示:
Find.Execute(FindText, MatchCase, MatchWholeWord, MatchWildcards, MatchSoundsLike, MatchAllWordForms, Forward, Wrap, Format, ReplaceWith, Replace, MatchKashida, MatchDiacritics, MatchAlefHamza, MatchControl)
但大多数情况下,我们只会用到FindText、ReplaceWith等极少数参数而已。
3、版本问题。Word的众多版本也给二次开发带来很多困扰,开发者必须要对于当前多种Word版本都存在的情况有所考虑,并做好兼容性的处理才行。
使用DsoFramer进行开发
谈到Word的二次开发,就必须要提到DsoFramer。它是微软提供一款开源的用于在线编辑、调用Word、 Excel 、PowerPoint等的ActiveX控件。国内很多著名的OA中间件,电子印章,签名留痕等大多数是依此改进而来的。
DsoFramer操作Word很简单,加载ActiveX控件后就可以直接操作Office文档了。以我们要进行的主要操作——替换文档中的关键字为例,在Visual Basic中代码如下:
dso.Open "new.doc" dso.Replace "[标题]","新标题",3 dso.Save "c:\new2.doc" dso.Close
在VB6中加载控件,如下图所示:
由于DsoFramer是COM时代的产物,适用于VB、VC开发者,在 .Net下开发,或者进行Web应用开发,就显得有点力不从心。在实际开发中,常常出现一些莫名其妙的错误。另外,它的工作模式需要先在界面中打开文档再进行各种操作,这种模式也不适应Web应用程序的需要。
使用Interop进行开发
微软在.Net框架下,推出了Microsoft.Office.Interop.Word及其他的互操作方式,能够更好地对Office文档进行二次开发。
使用Interop进行二次开发,首先需要了解Word的对象结构,完整的Word对象结构图如下(来自官方的VBA_Word帮助文件):
Application: 用来表现WORD应用程序,包含其它所有对象。他的成员经常应用于整个Word,可以用它的属性和方法控制Word环境。
Document对象: Document对象是Word编程的核心。当打开一个已有的文档或创建一个新的文档时,就创建了一个新的Document对象,新创建的Document将会被添加到Word Documents Collection。
Selection: Selection对象是描述当前选中的区域。若选择区域为空,则认为是当前光标处。
Rang: 是Document的连续部分,根据起始字符的结束字符定议位置。
Bookmark: 类似于Rang,但Bookmark可以有名字并在保存Document时Bookmark也被保存。
打开关闭和写入操作
了解到Word的对象结构后,就可以考虑怎样操作了。
1、如何打开和关闭Application及Document对象。
打开和关闭操作比较简单,实现代码如下:
//打开 Microsoft.Office.Interop.Word.Application app = new Microsoft.Office.Interop.Word.Application(); Microsoft.Office.Interop.Word.Document doc = app.Documents.Open(ref fn, ref oMiss, ref oTrue, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oTrue,ref oMiss, ref oMiss, ref oMiss, ref oMiss); //关闭 doc.Close(ref oFalse, ref oMiss, ref oMiss); doc = null; app.Quit(ref oFalse, ref oMiss, ref oMiss); app = null;
2、写入
由于Word的结构复杂,要找到写入的位置就比较复杂。在Interop操作中,可以对Range的text进行操作,如:
doc.Range.Text="newtext";
批量替换文本
写入报表,最常用的方法,是把模板做好,定义好特征串,进行替换即可。自然而然我们想到了通过Word的替换功能来完成。其主要代码如下:
object s1 = OldString; object s2 = NewString; object rep = Microsoft.Office.Interop.Word.WdReplace.wdReplaceAll; doc.Content.Find.ClearFormatting(); doc.Content.Find.Execute( ref s1, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref oMiss, ref s2, ref rep, ref oMiss, ref oMiss, ref oMiss, ref oMiss);
用简单的字符串测试,代码工作正常,但是,用实际的数据测试发现无法完成替换。追踪后发现问题:替换的目标字符串不能过长,否则就会替换失败,这个结果和Word软件中替换的实际情况一致。
遍历段落替换文本
由于批量查找替换操作不能完成替换成长文本目标,直观的解决思路就是采用手动的方式,找到一个特征串替换一个。但是在Interop中,由于Find对象比较复杂,多次尝试没有成功,比较实验后,发现可以采用遍历方式进行替换。
由于文档下有多个段落,因而可以对文档中的每个段落进行遍历,如果在段落中找到特征串,就把段落的文字提取出来,放在字符串中,对该字符串进行替换后再重新赋值给这个段落。这种方式需要段落的格式保持一致,这样就可以拼出完成段落来了。核心代码如下:
for (int i = 0; i < doc.Paragraphs.Count; i++) { try //只能用尝试的方法来进行替换 { if (doc.Paragraphs[i].Range.Text.IndexOf(OldStringArray[k]) >= 0) { doc.Paragraphs[i].Range.Text = doc.Paragraphs[i].Range.Text.Replace(OldStringArray[k], NewStringArray[k]); } } catch { } }
在实际操作中,发现遍历操作非常容易出错,原因在于文档对象存在着很多的段落,超过了可以看见的段落数量,因此就必须加入一个错误捕获功能以忽略一些意外的错误。
通过这种替换,可以成功的完成整段的替换,效果如下图:
如果被替换的特征串并不是独立的段落、或者位于表格中的话,上述代码能否工作正常呢?如下图所示,在段落中和表格中增加两个特征串进行替换,结果如下图所示:
结果可以看到,表格中虽然顺利替换,但格式还是受到影响。而段落中的文字虽然替换了,格式也被改为统一的格式了。
查找后逐个替换文本
对于一个追求完美的程序员来说,上述的bug是无法容忍的,尽管它已经可以凑合使用了,但要忽视的确做不到。根据前面的铺垫,可能感觉到问题的解决还得把Word的内部构造搞清楚。
在网上搜索了很久,都没办法找到关于查找和替换的更详细的解决方法。经过一段时间的困惑之后,突然发现,其实这些资料我自己本身就有。就是使用VBA开发Office的一系列资料,里面关于Word的对象结构,有着远比网上只言片语靠谱的解答。学习的过程直接跳过,把几条重要的结论给出来:
1)用Content的Find查找,只能进行批量的查找和替换,如果想找到第一个,停下来,操作,是不行的。
2)上述的“查找——操作”的思路,只能用Selection对象来完成,而Selection对象,Document的属性中没有、Content的属性中也没有。只有谁有?Application!
3)用Application的Selection的Find找到后,结果就在Selection.Text中,但要替换,只能对Selection.Range.Text进行赋值才行。
下面是实现代码:
object oFindText=OldString; app.Selection.SetRange(0, 0); app.Selection.Find.Execute(ref oFindText,ref oMiss,ref oMiss,ref oMiss,ref oMiss,ref oMiss,ref oTrue,ref oMiss,ref oMiss,ref oMiss,ref oMiss,ref oMiss,ref oMiss,ref oMiss,ref oMiss); if (app.Selection.Find.Found) { app.Selection.Range.Text=NewString; }
再次对上述第二种模板进行替换,结果如下:
这段来之不易的代码,当然要保存在CommonCode(v2.0.6)中,以后要调用Word模板实现生成新文档就非常简单了,代码如下:
CommonCode.WordUtil.ReplaceAndSave(Application.StartupPath + "\\temp2.doc", Application.StartupPath + "\\1.doc", new string[] { "[%单选%]", "[%分数%]", "[%数量%]" }, new string[]{@"
1、关于公开信息搜密,正确的是
A.在互联网公开信息中搜密需要高深的技术
B.在互联网中的主流网站中不存在秘密
C.只要通过关键词搜索和定期跟踪网站就可能找到秘密信息
D.公开信息搜密因为方法简单,所以效果较差,不受重视","98","10"});
结论
对于替换Word模板内容生成Word文档的需求,在.Net下可以采用Interop的方式来实现。具体的实现手段,有批量替换、遍历替换、单步查找并替换等方式。批量替换不能进行长文本的替换故不可用,遍历段落替换不能对段内的关键词进行保持格式的替换,也不完美。单步查找替换调用全局的查找功能(app.Selection.Find),并能够定位查找到的内容并进行操作,是完成需求的最佳方案。
单步查找替换实现方案被整合至CommonCode.WordUtil.ReplaceAndSave函数中,可以直接使用。
说明:引用CommonCode.dll和Microsoft.Office.Interop.Word.dll即可。
原来demo缺了log4net引用,添加