在前一篇文章中,我初步介绍了如何如何开发一个VSTO程序,在本文中,我将进一步深入介绍Word的插件开发。Word是一个大家在日常工作中一直接触的文档工具,也是微软最赚钱的产品之一。从最初的Word 1.0到现在的Word 2010历经了13代的演化,已经成为了一个比较复杂的系统。(这里稍微跑题一下,Office 2010的版本代号是version 14,但是我为什么说Word一共演化了13代呢?因为Office并没有Version 13,上一代的Office 2007是Version 12,微软觉得13这个死数字不吉利,所以直接跳过了……Oh my god)。言归正传,我录制了一段视频来演示本文所要介绍的内容。
这段视频,描述了一个简单的Word搜索插件,包含了以下几个功能点
- 自定义Ribbon
- 自定义Task Pane
- VSTO插件中获取Word内容全文
- 修改Word内容和样式
其中关于如何创建Ribbon和Task Pane的内容,我已经在前一篇文章中介绍了,如果你还不熟悉,可以看这里。
Word Object Model 介绍
首先,要开发出良好的程序,我们需要了解我们的开发平台,而Word本身是一个很复查的平台,我在这里先从Word的对象模型开始介绍。Word Object Model中一共包含有数百个不同类型的对象,其中最关键的也是最常用的是Application、Document、Range、Selection和Bookmark,他们的关系如下图:
我来依次介绍这几个对象:
Application 对象
Application代表Word程序,而一个Word程序内可以包含多个Word文档。用通俗的话来说,无论你开几个Word文档,都是在一个Word进程里面管理。这我们以后会讲到的Excel不一样。同时Application又是所有Word对象根,你可以通过Application对象,获得其他对象。在Addin开发过程中,我们可以通过以下方式来获得Application对象:
Globals.ThisAddIn.Application
Document 对象
Document对象代表着一个Word文档,即便你刚打开你的Word,是一个空的新文档,也会有一个Document。在开发过程中,以下这个属性从Application中获得当前的Document对象:
Globals.ThisAddIn.Application.ActiveDocument
此外Application对象也维护着一个集合,即Application.Documents,里面包含着现在所有打开的Word文档。
Range 对象
Range是一个比较特殊的对象(我其实都不知道如何用中文翻译贴切地翻译这个单词),在你日常使用Word的过程中,你甚至可能不会知道有这样一个对象存在,但如果你想通过程序方式修改Word正文的内容,Range是一个很关键的对象。用微软官方的表述,Range代表着文档中一片连续的区域,微软为它列出了一下几个特性:
- Range的组成成分可以是单独的插入点,也可以是一个文本范围或整个文档。
- Range包含非打印字符,例如空格、制表符和段落标记。
- Range可以是当前所选内容所表示的区域,也可以表示当前所选内容之外的区域。
- Range与始终可见的所选内容不同,它在文档中是不可见的。
- Range不随文档保存,仅存在于代码运行期间。
我再为它加2条
- Range有明确的开始和结束,但不同的Range之间是可以有交集的
- Range的长度是在变化的,如果你往一个Range里面插入的一个单词,它的长度会自动变长。
获得Range对象的方式很多,我们可以通过Document对象的Range(ref object Start = Type.Missing, ref object End = Type.Missing)方法,创建一个自定义的Range。通过Word中文档相关的对象都有一个Range属性,比如Paragraph.Range、Selection.Range。
Selection 对象
Selection代表着当前光标所选中的对象,我们在开发过程中这个对象会和Application.WindowSelectionChange一起使用。
1: //
2: // Summary:
3: // Occurs when the selection changes in the active document window.
4: event ApplicationEvents4_WindowSelectionChangeEventHandler WindowSelectionChange;
Delegate接口
1: [TypeLibType(16)]
2: [ComVisible(false)]
3: public delegate void ApplicationEvents4_WindowSelectionChangeEventHandler(Selection Sel);
Bookmark 与 Content Control
Bookmark即书签,在Word文档中做一个标记,方便查阅。开发过程中,我们可以基于Range来创建Bookmark。如:
1: // 将第一段文档标记为一个BookMark
2: Word.Range range = Globals.ThisAddIn.Application.ActiveDocument.Paragraphs[0].Range;
3: range.Bookmarks.Add("JustinTest");
至此我们已经介绍完毕了Word中的主要5个对象,现在我们运用我们学到的东西,来实开头视频中的那个插件
插件:自定义Search面板
我先定义一下我们要实现的功能点,获得Word文档的内容,取得与检索关键字相关的上下文并显示在ListView中,当用户点中ListView中的项目时,高亮显示Word文档中对应的内容。以下是如何实现这几个功能的介绍。
获得Word的全文
要取得当前Word的全文,我们主要要解决两个问题。
- 如何获得当前的Document对象?
- 如何通过Document对象获得文档内容?
对于第一个问题,因为搜索功能主要是写在Task Pane中的UserControl中(这一点在上一篇文章中已经有过介绍),所以取得Document对象的主要方法,是通过Application对象获得ActiveDocument的对象,即当前编辑的文档:
Globals.ThisAddIn.Application.ActiveDocument
对于第二个问题,我们有两个方法:首先,Document对象有Paragraphs集合,这个集合里面包含了每个段落的对象,而每个段落对象,都有Range属性,我们可以通过Paragraph.Range.Text,来获得每个段落的正文。其次Document对象有一个Range方法,通过它我们可以把整个Document作为一个Range。
Search按钮代码
1: private void btnSearch_Click(object sender, EventArgs e)
2: {
3: // 清楚文档中的高亮显示
4: ClearMark();
5:
6: lvSearchResult.Items.Clear();
7: if (string.IsNullOrWhiteSpace(tbSearchText.Text))
8: {
9: return;
10: }
11:
12: // 按段落检索
13: Word.Document currentDocument = Globals.ThisAddIn.Application.ActiveDocument;
14: if (currentDocument.Paragraphs != null &&
15: currentDocument.Paragraphs.Count != 0)
16: {
17: foreach (Word.Paragraph paragraph in currentDocument.Paragraphs)
18: {
19: MatchCollection mc = Regex.Matches(paragraph.Range.Text, tbSearchText.Text.Trim(), RegexOptions.IgnoreCase);
20: if (mc.Count > 0)
21: {
22: foreach (Match m in mc)
23: {
24: try
25: {
26: int startIndex = paragraph.Range.Start + m.Index;
27: int endIndex = paragraph.Range.Start + m.Index + m.Length;
28:
29: Word.Range keywordRange = currentDocument.Range(startIndex, endIndex);
30:
31: // 获取上下文信息
32: // 获取前两个单词的位置(如果有)
33: startIndex = GetStartPositionForView(paragraph, m, startIndex);
34:
35: // 获取后两个单词的位置(如果有)
36: endIndex = GetEndPositionForView(paragraph, m, endIndex);
37:
38: // 在ListView中展示检索的关键字以及其上下文
39: Word.Range range = currentDocument.Range(startIndex, endIndex);
40: ListViewItem item = new ListViewItem(range.Text);
41: item.Tag = keywordRange;
42: lvSearchResult.Items.Add(item);
43: }
44: catch (Exception ex)
45: {
46: MessageBox.Show(ex.Message);
47: }
48: }
49: }
50: }
51: }
52: }
介绍一下这段代码的几个功能点:
- 在Search功能开始时,先清除文档中的高亮显示(ClearMark方法稍后会介绍)。
- 分段落,依次查找关键字。
- 获得关键字的上下文,并放入ListView中显示。需要注意的时候,我在ListViewItem的tag对象里面,存入了Keyword在文档中的Range,为了ListView点击事件。
ListView点击事件
1: private void lvSearchResult_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
2: {
3: ClearMark();
4: if (lvSearchResult.SelectedItems.Count > 0)
5: {
6: Word.Range range = lvSearchResult.SelectedItems[0].Tag as Word.Range;
7:
8: // 为了可以恢复被修改的Range,我先将该Range和原本的Color放入Class的成员
9: _LastRange = range;
10: _LastRangeBackColor = range.HighlightColorIndex;
11: range.HighlightColorIndex = Word.WdColorIndex.wdYellow;
12: }
13: }
总体来说这段代码很简单,我稍微介绍一下Range对象的使用,这里我修改了HighlightColorIndex属性,来修改文字的背景色,如果你修改字体相关的样式,可以通过Range.Font属性。(此外,我会在下一篇文中,介绍如何通过Range加超链接、书签或者Content Control)。
其他方法
1: private void ClearMark()
2: {
3: if (_LastRange != null)
4: {
5: _LastRange.HighlightColorIndex = _LastRangeBackColor;
6: }
7: }
我在Search事件和ListView点击方法中都会先调用这个方法,它会使用在lvSearchResult_ItemSelectionChanged中保存的Range和Color,来恢复之前的样式。
总结
本文中,我介绍了Word对象模型的基本内容和我写的一个Word插件,包含对Application、Document和Range对象的操作。Word程序包含的内容是很多的,本来想把自己了解的Word知识一次写完,但写到这里发现已经写了很多了,怕大家会看得累,所以先发出来,再下一篇文章,我会进一步深入介绍Word插件开发。下次内容预告:
- 修改右键菜单
- 往文档中插入内容
- 添加超链接、书签
- 基于选中内容,显示悬浮框
最后,本文范例的代码可以在这里下载。此外,本文欢迎转载,但请保留出处,大家如果有问题,可以联系我 [email protected]。