65.9K
CodeProject 正在变化。 阅读更多。
Home

编写 MS Word 加载项

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (16投票s)

2003 年 4 月 14 日

9分钟阅读

viewsIcon

410560

downloadIcon

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()。我们 CAddinOnConnection()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 日。
© . All rights reserved.