编写 MS Word 加载项






4.76/5 (16投票s)
2003 年 4 月 14 日
9分钟阅读

410560

3891
使用 COM 和 VB 宏编写 Word 加载项
引言
在之前的一篇文章中,我曾介绍过开发 Office COM 插件,之后我收到了许多关于编写 Word 插件的邮件。通过这篇文章,我们将讨论关于 Word 插件开发中普遍遇到的问题。首先,我们将学习如何构建一个简单的 WORD 2000 ATL COM 插件。之后,我们将深入到 Word 的 VBA 宏方面,并编写一个适用于所有 Word 版本、与 COM 插件支持无关的插件。
我假设您已经阅读了我之前关于 Office 插件的文章,并且查看了示例项目。在插件开发中,有很多问题,比如添加自定义菜单/工具栏、处理事件、属性页等,这些问题对包括 Word 在内的所有 Office 插件来说都是普遍存在的。这些问题已经在那篇 文章 中进行了讨论。在这里,我们首先将编写一个 Word 2000 COM 插件。之后,我们将更广泛地深入到 Office 和 VBA,以及 C++ 插件。
编写 Word2000 插件
首先,创建一个名为 WordAddin
的新的 ATL COM Appwizard 生成的 dll 项目。接下来,插入一个名为 Addin
的 ATL Simple Object,就像在前一篇文章中一样。然后,使用 **Implement Interface ATL Wizard** 来实现 _IDTExtensibility2
。您知道,这是所有 Office 应用程序支持 COM 插件的核心。接下来,为了编程操作 Word,我们需要导入 Word 的 typelib。这意味着我们需要导入 3 个相互依赖的 typelib。在您的项目的 stdafx.h 文件中添加以下代码:
//for Office XP //C:\Program Files\\Common Files\\Microsoft Shared\\Office10\\MSO.DLL #import "C:\\Program Files\\Microsoft Office\\Office\\mso9.dll" rename_namespace("Office2000") using namespace Office2000; #import "C:\\Program Files\\Common Files\\Microsoft Shared\\VBA\\VBA6\\VBE6EXT.olb" rename_namespace("VBE6") using namespace VBE6;
在您的项目的 Addin.h 文件顶部,添加:
//for Office XP //C:\\Program Files\\Micorosft Office\\Office10\\MSWORD.olb #import "C:\\Program Files\\Microsoft Office\\Office\\MSWORD9.olb" rename("ExitWindows","MyExitWindows"),named_guids, rename_namespace("MSWord") using namespace MSWord;
请确保更改路径以指向您系统上文件的正确位置。
之所以需要不同的 #import 语句位置,是为了让编译器能够识别所有内容,生成正确的包装器集,并正确编译和链接。接着,为了将您的插件注册到 Word2000,请在 Addin .rgs 注册表脚本文件(位于 FileView -> Resource Files 下)中添加以下代码,并将其添加到文件末尾。
HKCU
{
Software
{
Microsoft
{
Office
{
Word
{
Addins
{
'WordAddin.Addin'
{
val FriendlyName = s 'WORD Custom Addin'
val Description = s 'Word Custom Addin'
val LoadBehavior = d '00000003'
val CommandLineSafe = d '00000001'
}
}
}
}
}
}
}
是的,我们希望我们的插件被称为“Word Custom Addin”(“I love CodeProject.com Addin”就太明显了!),并在 Word 启动时加载。那么还有什么新东西呢?:)
如果一切顺利,您现在应该有了一个可以工作的 WORD 插件,您可以为其添加自己的实现代码。到目前为止,您已经知道如何添加按钮和菜单项、属性表等;之前讨论过的所有内容仍然适用。Word 对象模型中唯一与 Outlook 插件示例中的代码不同的是,Word 中没有 ActiveExplorer
对象,并且您应该直接从对象模型最顶层的 Application
获取 CommandBars 接口。
处理事件
在您的插件中,您可能还对处理 Word 的某些事件感兴趣。一个例子是 Application 对象的 DocumentOpen
事件,其 DISPID 为 4,此处进行了处理。Word 拥有一个复杂的对象模型,您会发现还有许多其他类似的事件。如果您使用经典的 OLE/COM 对象查看器来查看 msword9.olb,您会发现 IDL 如下:
.... [id(0x00000003), helpcontext(0x00061a83)] void DocumentChange(); [id(0x00000004), helpcontext(0x00061a84)] void DocumentOpen([in] Document* Doc); ......
与之前一样,我们将使用 ATL 的 IDispEventSimpleImpl<>
模板类来实现我们的接收器。为简洁起见,仅提及了对先前代码所做的必要更改。
extern _ATL_FUNC_INFO DocumentOpenInfo; class ATL_NO_VTABLE CAddin : public CComObjectRootEx, public CComCoClass, public ISupportErrorInfo, public IDispatchImpl, public IDispatchImpl<_IDTExtensibility2, &IID__IDTExtensibility2, &LIBID_AddInDesignerObjects>, public IDispEventSimpleImpl<1,CAddin, &__uuidof(MSWord::ApplicationEvents2)> { public: .... .... void __stdcall DocumentOpen(IDispatchPtr ptr) { CComQIPtr<_Document> spDoc(ptr); ATLASSERT(spDoc); .... .... } BEGIN_SINK_MAP(CAddin) SINK_ENTRY_INFO(1,__uuidof(MSWord::ApplicationEvents2),4, DocumentOpen,&DocumentOpenInfo) END_SINK_MAP() private: CComPtr<MSWord::_Application> m_spApp; };
DocumentOpenInfo
定义在 CAddin.cpp 的顶部,如下所示:
_ATL_FUNC_INFO DocumentOpenInfo = {CC_STDCALL,VT_EMPTY,1,
{VT_DISPATCH|VT_BYREF}};
我们所要做的就是添加设置和断开连接的代码。因此,使用 ATL 模板类,我们只需调用 DispEventAdvise()
和 DispEventUnadvise()
。我们 CAddin
的 OnConnection()
和 OnDisconnection()
方法,不用说,是执行此操作的正确位置。
CComQIPtr<_Application> spApp(Application); ATLASSERT(spApp); m_spApp = spApp; HRESULT hr = DispEventAdvise(m_spApp); if(FAILED(hr)) return hr;
以及在 OnDisconnection()
中:
DispEventUnadvise(m_spApp); m_spApp = NULL;
现在,您已经拥有了一个可工作的 Word COM 插件模板,并且可以自豪地将其收入囊中了。您应该在此基础上添加自己的实现、错误处理例程等。接着,让我们转向其他方面,探索 WORD 的 VBA 部分。接下来,我将讨论一个非常具体的情况,其中我在插件中混合使用了 C++ 代码和 VBA 宏。
宏和 Visual Basic 编辑器
我曾在一个项目上工作,其中的 COM 插件模式非常适合我们。唯一的例外是,客户有大量的 Word97 用户,而 Word97 不支持 COM 插件,特别是 IDTExtensibility2
接口。基本上,在我的插件中,我需要添加一些自定义菜单项和按钮,点击它们通常会显示几个对话框。现在,对于 Word2000 用户来说,COM 插件是完美的。但是,是否有办法让插件兼容 Word97 呢?
这就引出了 Visual Basic for Applications (VBA)。大多数 Office 开发者都知道,MS Office 组件和应用程序支持丰富的脚本对象模型和称为 VBA 的脚本接口。在 Office 4 版本之前,套件中的每个应用程序都非常独立。对于开发者来说,使用多个 Office 应用程序创建集成解决方案并不容易,因为每个应用程序都有独特的编程环境。
这个问题通过 MS Visual Basic for Applications (VBA) 得到了解决,VBA 在 Office 95 中首次亮相——尽管是在少数应用程序中。到 Office 97,套件中的每个应用程序都支持标准的 VBA 接口。VBA 例程或宏可以用来控制其宿主应用程序的函数集,与外部控制应用程序的 OLE Automation 客户端所能使用的函数集相同,而与控制器的编程语言无关。Word 97 包含了 Visual Basic 5.0,这是一个在 Office 应用程序(Word、Excel、PowerPoint 和 Access)之间共享的复杂的开发环境。
在 Word 中,VBA 和 Visual Basic 不仅仅是一种宏语言——它是一个功能齐全的编程开发环境。Visual Basic 编辑器 (VBE) 使用 Microsoft Visual Basic 4.0 的常用编程界面作为基础来创建和编辑脚本代码。通过 VBA,Word 支持从宏到插件再到文档模板的所有功能。那么什么是文档模板?文档模板(*.dot) 仅仅是一个嵌入了宏代码(脚本)的文档。在 Word 中,宏以 Visual Basic 模块的形式存储在文档和模板中。尽管宏通常存储在用户的默认模板 Normal.dot 中,Word 允许您将宏存储和使用在任何文档或模板中。此外,Office 安装的 Startup 文件夹中的任何此类文档模板都会被自动运行。可以通过“工具”菜单中的“模板和加载项”或“宏”对话框加载其他模板和宏。宏和文档模板(也支持 WordPerfect)也可以在工作组中的用户之间共享。
还有一点很有趣的是,MS Word97 及更高版本还支持一组全局宏,这些宏会与宿主应用程序一起调用。这些 AutoXXX 宏,如 AutoExec()
, AutoNew()
, AutoOpen()
, AutoClose()
和 AutoExit()
,会与宿主应用程序一起执行,正如其名称所示。这听起来非常有用,而且确实如此。
如何实现?如果我们创建一个文档模板,并在其中处理这些 Auto 宏,然后通过 VBA 宏创建和销毁我们的插件(它被编写为 ActiveX 服务器)——那么我们就基本完成了。这就是 IDTExtensibility2
对 COM 插件所做的——主要是连接和断开与最顶层 Application 对象的连接方式。那么,如何在我们的 Word97 插件中替代它呢?通过文档模板。文档模板本身可以被描述为 VBA 插件。每个文档模板都设置为通过 ThisDocument
属性与项目关联的 Document
对象进行交互。我们还必须记住,所有宏都隐式引用 Application 对象。
回到手头的任务,假设我们要编写一个文档模板,并在其中添加代码到我们的 AutoExec()
和 AutoExit()
处理程序中。在处理程序中,我们创建和释放我们的插件 COM 类对象,并随后调用它的方法。此外,我们的 ATL COM 类应该通过 Init()
和 Uninit()
方法实现,通过这些方法,Application 对象被设置到 C++ 插件中。我们的宏应该看起来像:
Dim o as Application
Dim obj as Object
Sub AutoExec()
Set obj = CreateObject("Word97Addin.Addin")
Set o = ThisDocument.Application
obj.Init o
End Sub
Sub AutoExit()
Set o = ThisDocument.Application
obj.Uninit o
Set obj = Nothing
Set o = Nothing
End Sub
我们可以选择将模板自动加载和卸载,方法是将其放置在 Office 的 Startup 文件夹中。同样,您可以处理 AutoClose()
, AutoNew()
和 AutoOpen()
宏,这些宏与文档更相关。目前,由于我负责在 C++ COM 对象中处理所有按钮/菜单的创建和事件,通信是单向的(即宏 -> 插件),但双向通信并非难事。假设您的模板中定义了一个名为“NewMacro”的宏,一种从 C++ 插件触发此宏的方法是使用 Application::Run()
或 Application::RunOld()
,具体取决于您是否需要传递任何参数。有趣的是,您还可以通过点击按钮来触发宏,方法是设置您的按钮或菜单项的 CommandBarButton
对象的 OnAction
属性。
// get CommandBarButton interface so we can specify button styles CComQIPtr < Office::CommandBarButton> spCmdButton(spNewBar); ATLASSERT(spCmdButton); spCmdButton->PutCaption(_T("Button")); spCmdButton->put_OnAction(OLESTR("NewMacro")); //set other button properties spCmdButton->PutVisible(VARIANT_TRUE);
这正是我为我的项目所做的。我们有一个单一的解决方案,由一个 dll 和一个 dot 文件组成,可以在从 97 到 XP 的所有 Word 版本中运行,通过编译的 C++ 代码公开和封装其大部分功能。不幸的是,这种 VBA 支持带来了安全问题,而在 XP 等较新版本的 Office 中,用户可以在宏执行上下文中决定安全级别。虽然这对我们来说不是问题,但您需要了解这一点。
到目前为止,我们学到的所有内容都已包含在附带的 VC++ 6.0 Universal Addin 项目中,我将在接下来简要描述一下。
通用插件
Universal Addin 是一个简单的 ATL/COM AppWizard 生成的 dll 项目,我向其中插入了一个基于 ATL COM IDispatch 的 Simple Object,名为 Addin。虽然名称听起来很夸张,“Universal”指的是它能够跨所有 Word 版本运行的能力。
使其具有这种通用性的是插件中的 addin.dot Word 文档模板及其 AutoXXX 宏。我们的 Addin 类有两个成员方法 Init()
和 Uninit()
,它们接受一个指向 Application
对象的 IDispatch*
。我们的 Uninit()
实现非常简单,我们可以省略 IDispatchPtr
参数;但这可能不适用于您的实现。在项目中,我们通过插件添加了一个按钮,点击它会触发一个名为 VBA 的宏,该宏又会调用我们的 COM 类的某个方法。您可以通过 CommandBarButtonEvents
连接并编写 C++ 代码来处理按钮点击,就像之前所做的那样。
我们的目标是编写一个 Word 插件,绕过 COM 插件架构和 IDTExtensibility2
。到此为止,我所要说的就是,您可以通过 VBA 在 C++ 中完成的任何事情,反之亦然——魔力就在于 OLE Automation。
致谢
我所了解的 Office 开发知识,都是在 MSDN 上学到的。您也可以在那里找到大量关于 Office 开发、COM 和 OLE Automation 的技术文章和简短的 KB 代码片段。MSDN 无所不能——正如我们都知道的那样。:)
感谢 Peter Hauptmann(又名 Peterchen)的评论和建议。
历史
- 首次修订 - 2003 年 4 月 14 日。