【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, refoMiss, 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, refoMiss, 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引用,添加
分类: CommonCode快速代码集