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

使用 VC++/ATL 构建 Office2K COM 插件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (104投票s)

2002 年 2 月 25 日

20分钟阅读

viewsIcon

1865574

downloadIcon

5100

本文介绍如何使用纯 ATL COM 对象编程 Outlook2000/2K+ COM 插件。

本文底部现已包含 FAQ。

引言

最近,我编写了一个 Outlook2000 COM 插件,作为构建 CRM 工具项目的一部分。在编码项目的过程中,我觉得这会是一个很好的主题,因为我在互联网上找到的大部分与 Office 相关的内容都是 VB/VBA 相关的,而几乎没有 ATL 相关的。

本文中的代码没有进行优化,并且总体方法保持简单,以便读者理解。由于我花了很长时间才写完这篇文章,尽管我已尽力而为,但如果发现任何错误或遗漏,请随时给我发邮件。如果您喜欢这篇文章或觉得它很有趣,如果您能给我一个好评并给我发邮件,我将非常高兴。:) 谢谢。

概述

通过本文/教程,我们将学习如何使用纯 ATL COM 对象编程 Outlook2000/2K+ COM 插件。我们将从编写一个基本的、功能齐全的 COM 插件开始。然后,我将向您展示如何将标准的 UI 元素(如工具栏和菜单项)添加到 Outlook,以及如何响应它们的事件。接下来,我们将为 Outlook 的“工具”->“选项”添加我们自己的属性页。在此过程中,我们将了解相关的注册表项,并了解 ATL 向导的有用功能并学会有效使用它们。

尽管我们将编写一个 Outlook2000 COM 插件,但其他 Office2000 应用程序(如 Word、Access 等)的 COM 插件也可以非常相似地构建。除了注册表项等少数细微差别外,基本原理是相同的。

我假设您是一位 VC++ COM 程序员,并且已经有一些使用 ATL 组件开发和 OLE/Automation 的经验,尽管这并非绝对必要。要构建和测试插件,您的系统上必须安装 MS Office 2000,或者至少是 Outlook2K。项目代码已使用 VC++ 6.0 sp3+/ATL3.0 构建,并在安装了 Office 2000 的 Win2K 上进行了测试。

入门

Office 插件是一种 COM Automation 组件,可以动态扩展/增强和控制任何 Office 应用程序套件。Microsoft Office 2000 及更高版本支持一种新的、统一的设计架构来构建此类应用程序插件。通常,此类插件驻留在 ActiveX DLL(进程内服务器)中,并且用户可以通过主应用程序动态加载和卸载它们。

Office COM 插件必须实现 IDTExtensibility2 接口。IDTExtensibility2 dispinterface 定义在 MSADDin Designer typelibrary (MSADDNDR.dll/MSADDNDR.tlb) 文件中,该文件通常位于 <drive>/Program Files/Common Files/Designer 目录下。

该接口定义如下:

    enum {
        ext_cm_AfterStartup = 0,
        ext_cm_Startup = 1,
        ext_cm_External = 2,
        ext_cm_CommandLine = 3
    } ext_ConnectMode;

    enum {
        ext_dm_HostShutdown = 0,
        ext_dm_UserClosed = 1
    } ext_DisconnectMode;

     ...
     ...
     ...
        
    interface _IDTExtensibility2 : IDispatch {
        [id(0x00000001)]
        HRESULT OnConnection(
                        [in] IDispatch* Application, 
                        [in] ext_ConnectMode ConnectMode, 
                        [in] IDispatch* AddInInst, 
                        [in] SAFEARRAY(VARIANT)* custom);
        [id(0x00000002)]
        HRESULT OnDisconnection(
                        [in] ext_DisconnectMode RemoveMode, 
                        [in] SAFEARRAY(VARIANT)* custom);
        [id(0x00000003)]
        HRESULT OnAddInsUpdate([in] SAFEARRAY(VARIANT)* custom);
        [id(0x00000004)]
        HRESULT OnStartupComplete([in] SAFEARRAY(VARIANT)* custom);
        [id(0x00000005)]
        HRESULT OnBeginShutdown([in] SAFEARRAY(VARIANT)* custom);
    };

所有 COM 插件都继承自 IDTExtensibility2 接口,并且必须实现其五个方法。

OnConnectionOnDisconnection,顾名思义,分别在插件加载到内存和从内存卸载时调用。插件可以根据应用程序启动、用户操作或通过自动化加载,并且枚举器 ext_Connect 表示这些连接模式。OnAddinsUpdate 在 COM 插件集发生更改时调用。OnStartupComplete 仅在插件在应用程序启动时加载时调用,而 OnBeginShutdown 在插件在宿主应用程序关闭时断开连接时调用。

注册插件

要将 COM 插件注册到宿主应用程序,我们还需要在以下注册表项下创建几个子项:

HKEY_CURRENT_USER\Software\Microsoft\Office\<TheOfficeApp>\Addins\<ProgID>

其中 ProgID 指的是插件 COM 对象的唯一编程标识符 (ProgID)。插件可以通过其他条目提供有关自身的信息,并指定加载选项给宿主应用程序,例如:

FriendlyName - 字符串值 - 这是宿主应用程序显示的插件名称。
Description - 字符串值 - 插件的描述。
LoadBehavior - DWORD 值。 - 值的组合,决定宿主应用程序如何加载插件。设置为 0x03 在应用程序启动时加载,设置为 0x08 由用户控制激活。
CommandLineSafe - DWORD 值。设置为 0x01(TRUE) 或 0x00(FALSE)。

有关所有值和选项的完整描述,请查阅 MSDN 文档。

构建一个最小的 COM 插件

好了,我们现在知道得够多了,可以开始编写一个最小的 Outlook2K COM 插件了。要构建插件项目文件,请启动 VC++ IDE,创建一个新的 ATL COM Appwizard 项目,并将其命名为 OutlookAddin。请记住,如果您将其命名为其他名称,它可能无法正常工作。(开玩笑的!)

在接下来的 Appwizard Step 1 of 1 对话框中,接受默认的服务器类型 Dynamic Link Library(DLL),勾选 Allow merging of proxy-stub code 以启用此选项,然后单击“完成”。然后单击“确定”生成项目文件。

接下来,转到 Insert->New ATL Object 菜单,通过在 ATL Object Wizard 对话框中选择“Category”下的“Objects”和“Objects”列表下的“Simple Object”来插入一个新的 ATL 简单对象。单击“Next”,然后键入 Addin 作为 ShortName。在“Attributes”选项卡中,勾选 Support ISupportErrorInfo。接受其余选项的默认值,然后单击“OK”。

到目前为止,向导已为我们提供了一个符合 Automation 规范、支持 dispinterface 的进程内 COM 对象,该对象包含在一个 DLL 中。默认情况下,我们还获得了一个注册表脚本 (.rgs) 来添加 COM 对象特定的注册表项。构建项目并检查一切是否正常。

如果您像我一样是那种急切的类型,那么在继续之前,您至少还需要编译项目的 .idl 文件。所以现在就去做吧。

接下来,我们将编写插件特有的代码来实​​现 IDTExtensibility2 。在这里,我们将让 Implement Interface ATL Wizard 发挥作用,让我们的生活变得轻松很多。在 ClassView 中,右键单击 CAddin 类,然后选择 Implement Interface。这将调出 ATL Implement Interface Wizard。接下来,单击 Add Typelib,然后在 Browse Typelibraries 对话框中,向下滚动并勾选 Microsoft Add-in Designer(1.0),然后单击 OK。接下来,在 Implement Interface 对话框的 AddinDesignerObjects 选项卡下的列表中勾选 _IDTExtensibility2 接口,然后单击 OK。

向导通过向 CAddin 类添加 _IDTExtensibility2 的 5 个方法的默认实现以及更新 COM_INTERFACE_MAP() 来实现所选接口。当然,每个方法都只返回 E_NOTIMPL,而我们则需要添加代码来做一些有用的事情。但目前,我们的功能性 COM 插件已准备就绪,只剩下必要的注册表项,我们将在下一步添加。

为了将我们的插件注册到宿主应用程序(本例中为 Outlook2000),请打开项目的文件视图 -> 资源文件下的 Addin.rgs 注册表脚本文件,并在文件末尾添加以下内容。

HKCU
{
      Software
    {
        Microsoft
        {
            Office
            {
                Outlook
                {
                    Addins
                    {
                        'OutlookAddin.Addin'
                        {
                            val FriendlyName = s 'ADOutlook2K Addin'
                            val Description = s 'ATLCOM Outlook Addin'
                            val LoadBehavior = d '00000008'
                            val CommandLineSafe = d '00000000' 
                        }
                    }
                }
            }
        }
    }
}

由于我们希望插件在启动时加载,因此 LoadBehavior 设置为 3。现在构建项目。如果一切正常,项目将成功构建并注册插件。要测试插件,请运行项目并在 Debug Session 的 Executable 中指定 Outlook2K 的完整路径(\Program Files\Microsoft Office\Office\Outlook.exe),或者在注册了 DLL 后在 VC++ IDE 外部运行 Outlook2K。要检查我们的插件是否已成功注册,请在 Outlook 中转到 Tools->Options,然后在“Other”选项卡下单击 Advanced Options->COM Addins。我们的 COM 插件的条目应该已添加到 Addins Available 列表中;该字符串是我们注册表中指定的“FriendlyName”。



虽然插件可以针对不同的用途进行编程,但有一些常见的任务。通常,这包括向 Outlook 添加 UI 元素,如工具栏/工具栏带和菜单项,用户可以通过这些元素来控制插件。通过单击这些按钮和菜单项,用户可以访问插件的功能。所以接下来我们将处理这类工具栏和菜单项的添加。

命令与征服

在 Office 应用程序中,菜单和工具栏被组合成一个完全可编程的集合,称为 CommandBars。CommandBars 是所有 Office 应用程序作为其对象模型一部分提供的通用可共享可编程对象。CommandBars 代表了一种统一的机制,通过该机制可以将单个工具栏和菜单项添加到相应的应用程序中。每个 CommandBars 集合包含单个 CommandBar 对象。每个 CommandBar 对象又包含一个 CommandBarControl 对象集合,称为 CommandBarControls

CommandBarControls 代表一个复杂的对象和子对象层次结构,构成了其对象模型。CommandBarControl 本身可以包含一个 CommandBar 对象,该对象可以通过控件的 CommandBar 属性访问。最后,CommandBarControls 控件集合中的每个 CommandBarControl 对象都可以是 CommandBarComboBox(工具栏组合框)、CommandBarButton(工具栏按钮)或 CommandBarPopup(弹出菜单)。我希望我能画一幅不错的对象层次结构图,但我在这方面很糟糕(说真的!),而且我确信 MSDN 上有描绘 MS Office CommandBars 对象模型的此类图表。

在我们的插件中,我们想向 Outlook 添加以下 UI 元素:

  • 在一个新的工具栏带中添加 2 个工具栏按钮(带位图)。
  • 向“工具”菜单添加一个新的弹出菜单项(带位图)。

首先,我们需要将 Office 和 Outlook 类型库导入到我们的项目中。要做到这一点,请打开项目的 stdafx.h 文件并添加以下 #import 指令。

#import "C:\Program Files\Microsoft Office\Office\mso9.dll" \
        rename_namespace("Office") named_guids 
using namespace Office;

#import "C:\Program Files\Microsoft Office\Office\MSOUTL9.olb" 
        rename_namespace("Outlook"), raw_interfaces_only, named_guids 
using namespace Outlook;


注意:您需要将上面的路径更改为 MSOffice 安装在您的系统上的位置。

现在我们都准备好了,让我们深入研究代码。首先是工具栏带和工具栏按钮。

Outlook Object Model 中,Application 对象是代表整个应用程序的对象层次结构的顶层。通过其 ActiveExplorer 方法,我们可以获得代表当前活动窗口的 Explorer 对象。接下来,我们将使用 GetCommandBars 方法获取 CommandBars 对象,该对象如您所知,是 Outlook 所有工具栏带和菜单项的集合。然后,我们只需调用 CommandBars 集合的 Add 方法并传入相关参数即可添加一个新的工具栏带。向工具栏带添加新按钮与获取工具栏带的 CommandBarControls 集合然后调用其 Add 方法一样简单。最后,我们查询按钮以获取 CommandBarButton 对象,该对象将用于设置按钮样式以及标题、工具提示文本等其他按钮属性。

代码片段如下:

STDMETHODIMP CAddin::OnConnection(IDispatch * Application, 
                                  ext_ConnectMode ConnectMode,
                                  IDispatch * AddInInst, SAFEARRAY * * custom)
{
    
    CComPtr < Office::_CommandBars> spCmdBars; 
    CComPtr < Office::CommandBar> spCmdBar;

    // QI() for _Application
    CComQIPtr <Outlook::_Application> spApp(Application); 
    ATLASSERT(spApp);
    // get the CommandBars interface that represents Outlook's
     //toolbars & menu items    

    CComPtr<Outlook::_Explorer> spExplorer; 
    spApp->ActiveExplorer(&spExplorer);

    HRESULT hr =  spExplorer->get_CommandBars(&spCmdBars);
    if(FAILED(hr))
        return hr;
    ATLASSERT(spCmdBars);

    // now we add a new toolband to Outlook
    // to which we'll add 2 buttons
    CComVariant vName("OutlookAddin");
    CComPtr <Office::CommandBar> spNewCmdBar;
    
    // position it below all toolbands
    //MsoBarPosition::msoBarTop = 1
    CComVariant vPos(1); 

    CComVariant vTemp(VARIANT_TRUE); // menu is temporary        
    CComVariant vEmpty(DISP_E_PARAMNOTFOUND, VT_ERROR);            
    //Add a new toolband through Add method
    // vMenuTemp holds an unspecified parameter
    //spNewCmdBar points to the newly created toolband
    spNewCmdBar = spCmdBars->Add(vName, vPos, vEmpty, vTemp);

    //now get the toolband's CommandBarControls
    CComPtr < Office::CommandBarControls> spBarControls;
    spBarControls = spNewCmdBar->GetControls();
    ATLASSERT(spBarControls);
    
    //MsoControlType::msoControlButton = 1
    CComVariant vToolBarType(1);
    //show the toolbar?
    CComVariant vShow(VARIANT_TRUE);
    
    CComPtr < Office::CommandBarControl> spNewBar; 
    CComPtr < Office::CommandBarControl> spNewBar2; 
            
    // add first button
    spNewBar = spBarControls->Add(vToolBarType,vEmpty,vEmpty,vEmpty,vShow); 
    ATLASSERT(spNewBar);
    // add 2nd button
    spNewBar2 = spBarControls->Add(vToolBarType,vEmpty,vEmpty,vEmpty,vShow);
    ATLASSERT(spNewBar2);
            
    _bstr_t bstrNewCaption(OLESTR("Item1"));
    _bstr_t bstrTipText(OLESTR("Tooltip for Item1"));

    // get CommandBarButton interface for each toolbar button
    // so we can specify button styles and stuff
    // each button displays a bitmap and caption next to it
    CComQIPtr < Office::_CommandBarButton> spCmdButton(spNewBar);
    CComQIPtr < Office::_CommandBarButton> spCmdButton2(spNewBar2);
            
    ATLASSERT(spCmdButton);
    ATLASSERT(spCmdButton2);
            
    // to set a bitmap to a button, load a 32x32 bitmap
    // and copy it to clipboard. Call CommandBarButton's PasteFace()
    // to copy the bitmap to the button face. to use
    // Outlook's set of predefined bitmap, set button's FaceId to     //the
    // button whose bitmap you want to use
    HBITMAP hBmp =(HBITMAP)::LoadImage(_Module.GetResourceInstance(),
    MAKEINTRESOURCE(IDB_BITMAP1),IMAGE_BITMAP,0,0,LR_LOADMAP3DCOLORS);

    // put bitmap into Clipboard
    ::OpenClipboard(NULL);
    ::EmptyClipboard();
    ::SetClipboardData(CF_BITMAP, (HANDLE)hBmp);
    ::CloseClipboard();
    ::DeleteObject(hBmp);        
    // set style before setting bitmap
    spCmdButton->PutStyle(Office::msoButtonIconAndCaption);
            
    HRESULT hr = spCmdButton->PasteFace();
    if (FAILED(hr))
        return hr;

    spCmdButton->PutVisible(VARIANT_TRUE); 
    spCmdButton->PutCaption(OLESTR("Item1")); 
    spCmdButton->PutEnabled(VARIANT_TRUE);
    spCmdButton->PutTooltipText(OLESTR("Tooltip for Item1")); 
    spCmdButton->PutTag(OLESTR("Tag for Item1")); 
    
    //show the toolband
    spNewCmdBar->PutVisible(VARIANT_TRUE); 
            
    spCmdButton2->PutStyle(Office::msoButtonIconAndCaption);
    
    //specify predefined bitmap
    spCmdButton2->PutFaceId(1758);  
    
    spCmdButton2->PutVisible(VARIANT_TRUE); 
    spCmdButton2->PutCaption(OLESTR("Item2")); 
    spCmdButton2->PutEnabled(VARIANT_TRUE);
    spCmdButton2->PutTooltipText(OLESTR("Tooltip for Item2")); 
    spCmdButton2->PutTag(OLESTR("Tag for Item2"));
    spCmdButton2->PutVisible(VARIANT_TRUE);

    //..........
    //..........
    //code to add new menubar to be added here
    //read on
    //..........

同样,要将新菜单项添加到 Outlook 的“工具”菜单,我们执行以下操作。CommandBars 集合的 ActiveMenuBar 属性返回一个 CommandBar 对象,该对象代表容器应用程序(对我们而言是 Outlook)中的活动菜单栏。接下来,我们通过 GetControls 方法查询活动菜单栏的控件集合 (CommandBarControls)。由于我们要将弹出菜单项添加到 Outlook 的“工具”(第 6 个位置)菜单,因此我们检索 activemenubars 控件集合中的第 6 个项,然后直接调用 Add 方法来创建新菜单项并将其附加到“工具”菜单。这里没有什么新鲜事。

相应的代码片段如下:

    //......
    //code to add toolbar here
    //......

    _bstr_t bstrNewMenuText(OLESTR("New Menu Item"));
    CComPtr < Office::CommandBarControls> spCmdCtrls;
    CComPtr < Office::CommandBarControls> spCmdBarCtrls; 
    CComPtr < Office::CommandBarPopup> spCmdPopup;
    CComPtr < Office::CommandBarControl> spCmdCtrl;

    // get CommandBar that is Outlook's main menu
    hr = spCmdBars->get_ActiveMenuBar(&spCmdBar); 
    if (FAILED(hr))
        return hr;
    // get menu as CommandBarControls 
    spCmdCtrls = spCmdBar->GetControls(); 
    ATLASSERT(spCmdCtrls);

    // we want to add a menu entry to Outlook's 6th(Tools) menu     //item
    CComVariant vItem(5);
    spCmdCtrl= spCmdCtrls->GetItem(vItem);
    ATLASSERT(spCmdCtrl);
        
    IDispatchPtr spDisp;
    spDisp = spCmdCtrl->GetControl(); 
        
    // a CommandBarPopup interface is the actual menu item
    CComQIPtr < Office::CommandBarPopup> ppCmdPopup(spDisp);  
    ATLASSERT(ppCmdPopup);
            
    spCmdBarCtrls = ppCmdPopup->GetControls();
    ATLASSERT(spCmdBarCtrls);
        
    CComVariant vMenuType(1); // type of control - menu
    CComVariant vMenuPos(6);  
    CComVariant vMenuEmpty(DISP_E_PARAMNOTFOUND, VT_ERROR);
    CComVariant vMenuShow(VARIANT_TRUE); // menu should be visible
    CComVariant vMenuTemp(VARIANT_TRUE); // menu is temporary        
    
        
    CComPtr < Office::CommandBarControl> spNewMenu;
    // now create the actual menu item and add it
    spNewMenu = spCmdBarCtrls->Add(vMenuType, vMenuEmpty, vMenuEmpty, 
                                       vMenuEmpty, vMenuTemp); 
    ATLASSERT(spNewMenu);
            
    spNewMenu->PutCaption(bstrNewMenuText);
    spNewMenu->PutEnabled(VARIANT_TRUE);
    spNewMenu->PutVisible(VARIANT_TRUE); 
    
    //we'd like our new menu item to look cool and display
    // an icon. Get menu item  as a CommandBarButton
    CComQIPtr < Office::_CommandBarButton> spCmdMenuButton(spNewMenu);
    ATLASSERT(spCmdMenuButton);
    spCmdMenuButton->PutStyle(Office::msoButtonIconAndCaption);
    
    // we want to use the same toolbar bitmap for menuitem too.
    // we grab the CommandBarButton interface so we can add
    // a bitmap to it through PasteFace().
    spCmdMenuButton->PasteFace(); 
    // show the menu        
    spNewMenu->PutVisible(VARIANT_TRUE); 

    return S_OK;
}

有了这些,就可以按 F5 键了。如果一切正常,项目将成功构建,您将有机会一睹您的插件的风采(如果您还没有的话)。由于我们将运行 Outlook 来测试插件,因此在“Executable for Debug”对话框中,浏览到 Outlook 可执行文件 (Outlook.exe) 的当前路径,我们终于可以开始了。在 Outlook 中,转到 Tools->Options,然后在“Other”选项卡下单击“Advanced Options”。从“Advanced Options”对话框中,单击“COM Addins”按钮。接下来,在“Addins Available”列表框中勾选我们的插件条目,然后单击 OK。当我们的插件加载时,会创建一个新的停靠工具栏带,其中包含 2 个按钮。还可以查看添加到 Outlook“工具”菜单的那个很酷的菜单项。

就是这样!!您不仅编写了一个功能正常的 Outlook 插件,而且它还通过酷炫的工具栏和菜单项扩展了 Outlook。得益于 ATL,您小于 50KB 的插件也堪称镇上最精简、最强大的 COM 服务器。所以,享受这一刻吧!:)




事件接收器之王

单独地组合几个工具栏和菜单项并没有太大用处,除非我们可以编写响应其事件的命令处理程序。所以让我们开始吧。当然,在这里,当我们单击不同的按钮和菜单项时,我们将只弹出简单的消息框。但这正是您编写插件功能的地方。从 CRM 工具、自动联系人管理、邮件通知和过滤到高级文档管理系统,再到功能齐全的应用程序,COM 插件可以执行无数的任务。

CommandBarButton 控件公开了一个 Click 事件,当用户单击命令栏按钮时会触发该事件。我们将使用此事件在用户单击工具栏按钮或菜单项时运行代码。为此,我们的 COM 对象必须实现一个事件接收器接口。对于 CommandBarButton 控件,此事件 dispinterface 是 _CommandBarButtonEvents。Click 方法声明如下:

//...
//....Office objects typelibrary
//....

[id(0x00000001), helpcontext(0x00038271)]
  void Click(
    [in] CommandBarButton* Ctrl, 
    [in, out] VARIANT_BOOL* CancelDefault);

//....
//...

因此,我们所要做的就是实现事件接收器接口,当菜单或工具栏按钮被单击时,事件源将通过常规的连接点协议调用该接口。我们作为参数传递给我们回调方法的,是指向源 CommandBarButton 对象和一个布尔值的指针,该布尔值可用于接受或拒绝默认操作。至于实现派发事件接收器接口,这没什么新鲜的,作为 ATL 程序员,您可能已经做过很多次了。

但对于未入门者,ATL 为 ATL COM 对象提供了两个模板类 IDispEventImpl<>IDispEventSimpleImpl<>,它们提供了 IDispatch 的实现。我更喜欢轻量级的 IDispEventSimpleImpl,因为它不需要额外的类型库信息。只需从 IDispEventSimpleImpl<> 派生您的类,设置事件接收器映射,通过 _ATL_SINK_INFO 结构配置您的回调参数,最后调用 DispEventAdviseDispEventUnadvise 来连接和断开与源接口的连接。对于我们的工具栏按钮和菜单项,如果我们为所有事件编写一个回调方法,那么一旦我们获得触发事件的 CommandBarButton 的指针,我们就可以使用它的 GetCaption 方法来获取按钮文本,并基于此执行一些选择性操作。但对于这个示例,我们将为每个事件编写一个回调。

代码如下:
  • 从 IDispSimpleEventImpl 派生您的类 - 对于 ActiveX 控件,第一个参数通常是子窗口 ID。但对我们而言,这可以是任何预定义的整数,它唯一标识事件源(在我们的例子中是第一个工具栏按钮)<>

    class ATL_NO_VTABLE CAddin :
    public CComObjectRootEx < CComSingleThreadModel>,  
    .....
    .....
    public IDispEventSimpleImpl<1,CAddin,
    &__uuidof(Office::_CommandBarButtonEvents>
  • 设置回调 - 首先,我们定义一个回调如下:
    void __stdcall OnClickButton(IDispatch * 
    /*Office::_CommandBarButton**/ Ctrl, VARIANT_BOOL * CancelDefault);
    接下来,我们使用 _ATL_SINK_INFO 结构来描述回调参数。打开 Addin.h 文件并在顶部添加以下声明:
    extern _ATL_FUNC_INFO OnClickButtonInfo;
    接下来,打开 Addin.cpp 文件并添加定义:
    _ATL_FUNC_INFO OnClickButtonInfo =
       {CC_STDCALL,VT_EMPTY,2,{VT_DISPATCH,VT_BYREF | VT_BOOL}};
    OnClickButton 在这里非常基础,看起来像这样:
    void __stdcall CAddin::OnClickButton(IDispatch* 
    /*Office::_CommandBarButton* */ Ctrl, VARIANT_BOOL * CancelDefault) { USES_CONVERSION; CComQIPtr<Office::_CommandBarButton> pCommandBarButton(Ctrl); //the button that raised the event. Do something with this... MessageBox(NULL, "Clicked Button1", "OnClickButton", MB_OK); }
  • 事件接收器映射 我们将使用 ATL 宏 BEGIN_SINK_MAP()END_SINK_MAP() 来设置事件接收器映射,并通过 SINK_ENTRY_XXX 填充事件接收器映射。事件接收器映射基本上提供了派发 ID(定义事件)和处理它的成员函数之间的映射。

    BEGIN_SINK_MAP(CAddin)
    SINK_ENTRY_INFO(1, __uuidof(Office::_CommandBarButtonEvents),
    /*dispid*/ 0x01, OnClickButton, &OnClickButtonInfo) END_SINK_MAP()

现在一切就绪,我们必须使用 DispEventAdvise()DispEventUnadvise() 来连接和断开与事件源的连接。我们的 CAddin 类的 OnConnection()OnDisconnection() 方法正是执行此操作的合适位置。DispEventAdvise()DispEventUnadvise() 的参数是事件源上的任何接口以及所需的事件源接口。

//connect to event source in OnConnection
// m_spButton member variable is a smart pointer to _CommandBarButton
// that is used to cache the pointer to the first toolbar button.

DispEventAdvise((IDispatch*)m_spButton,&DIID__CommandBarButtonEvents);

//when I'm done disconnect from the event source
//some where in OnDisconnection()

DispEventUnadvise((IDispatch*)m_spButton);

类似地,为所有命令按钮和菜单项实现派发事件接收器,编写处理程序,并如上所述进行连接和断开连接。就是这样。如果一切顺利,在您重新构建并运行插件后,每当单击按钮或菜单项时,都应该执行相应的回调方法。

添加属性页

在本教程的最后,我们将学习如何将我们自己的“选项”属性页添加到 Outlook 的“工具”->“选项”属性表中。



这里的难点在于,要将页面作为我们插件的一部分添加到 Outlook 的“工具”->“选项”菜单中,我们的插件必须将属性页实现为一个 activeX 控件。当用户访问“工具”->“选项”菜单项时,Application 对象会触发一个 OptionsPagesAdd 事件,该事件通过 Outlook 对象模型中的 _ApplicationEvents dispinterface 公开。从 IDL 定义(如通过 OLE/COM 对象查看器查看的 MSOUTL9.olb 中所述)提取的内容如下:

dispinterface ApplicationEvents
{
....

[id(0x0000f005), helpcontext(0x0050df87)]
            void OptionsPagesAdd([in] PropertyPages* Pages);
....
}

[
      odl,
      uuid(00063080-0000-0000-C000-000000000046),
      helpcontext(0x0053ec78),
      dual,
      oleautomation
]
....
....

interface PropertyPages : IDispatch {
        [id(0x0000f000), propget, helpcontext(0x004deb87)]
        HRESULT Application([out, retval] _Application** Application);
    ....
    ....
        
    [id(0x0000005f), helpcontext(0x00526624)]
        HRESULT Add([in] VARIANT Page, 
                        [in, optional] BSTR Title);
    
    [id(0x00000054), helpcontext(0x00526625)]
        HRESULT Remove([in] VARIANT Index);
    };

OptionsPagesAdd 事件将一个指向 PropertyPages dispinterface 的指针传递给我们,其 Add 方法将最终添加页面。Add 方法的参数是我们的控件的 ProgID 和新选项卡标题文本。同样,要删除页面,请调用 Remove() 并传入目标页面的索引。现在让我们深入了解细节。

首先,通过 Insert->New ATL Object 向项目添加一个新的 ATL activex composite control。在 Objects 列表框中选择“Category”下的“Controls”和“Lite Composite Control”,然后单击 Next。将 ShortName 输入为 PropPage,在“Attributes”选项卡中,勾选 Support ISupportErrorInfo 选项。单击 OK 接受所有默认选项。

现在我们将实现 PropertyPage 接口。所以在 ClassView 中,右键单击 CPropPage 类,选择 Implement Interface,然后像以前一样单击 Add Typelib 按钮。这次勾选 Microsoft Outlook 9.0 Object Library,然后单击 OK。在“Interfaces”列表框中勾选 PropertyPage,然后单击 OK。

向导为 PropertyPage 的 3 个方法 Apply()、get_Dirty() 和 GetPageInfo() 添加了默认实现。
现在进行以下修改。在 com map 中,修改以下行:
COM_INTERFACE_ENTRY(IDispatch)
to
COM_INTERFACE_ENTRY2(IDispatch,IPropPage)
以消除任何歧义。接下来,要实现 IDispatch,我们将使用 IDispatchImpl<> 模板类。所以,在 CPropPage 类声明中,替换:
class ATL_NO_VTABLE CPropPage : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public IDispatchImpl<IPropPage, &IID_IPropPage, &LIBID_TRAILADDINLib>,
....
....
    public PropertyPage
public IDispatchImpl < Outlook::PropertyPage,&__uuidof(Outlook::PropertyPage),
       &LIBID_OUTLOOKADDINLib>
同样,删除 PropPage.h 文件顶部的冗余 #import 语句。类型库已在 stdafx.h 中导入一次,因此不再需要。

接下来,我们将连接和断开我们的插件与 ApplicationEvents dispinterface 的连接,并编写回调方法。您已经知道该怎么做。同样,使用 IDispEventSimpleImpl<> 模板为 ApplicationEvents 设置派发事件接收器,更新事件接收器映射,并如上所述编写 OptionsAddPages 事件的回调方法。由于我们多次使用 IDispEventSimpleImpl<>,因此为每个事件接口实现使用 typedef。因此,添加的内容是:

extern _ATL_FUNC_INFO OnOptionsAddPagesInfo;

class ATL_NO_VTABLE CAddin : 
....
....
public IDispEventSimpleImpl<4,CAddin,&__uuidof(Outlook::ApplicationEvents)>
{
public:
//typedef for applicationEvents sink implementation
typedef IDispEventSimpleImpl</*nID =*/ 4,CAddin, 
                            &__uuidof(Outlook::ApplicationEvents)> AppEvents;
....
....
....
BEGIN_SINK_MAP(CAddin)
....
SINK_ENTRY_INFO(4,__uuidof(Outlook::ApplicationEvents),
                  /*dispid*/0xf005,OnOptionsAddPages,&OnOptionsAddPagesInfo)
END_SINK_MAP()

public:
//callback method for OptionsAddPages event
void __stdcall OnOptionsAddPages(IDispatch *Ctrl);
};

//in PropPage.cpp file

_ATL_FUNC_INFO OnOptionsAddPagesInfo = (CC_STDCALL,VT_EMPTY,1,{VT_DISPATCH}};

void __stdcall CAddin::OnOptionsAddPages(IDispatch* Ctrl)
{
    CComQIPtr<Outlook::PropertyPages> spPages(Ctrl);
    ATLASSERT(spPages);

    //ProgId of the propertypage control
    CComVariant varProgId(OLESTR("OutlookAddin.PropPage"));

    //tab text
    CComBSTR bstrTitle(OLESTR("OutlookAddin"));
    
    HRESULT hr = spPages->Add((_variant_t)varProgId,(_bstr_t)bstrTitle);
    if(FAILED(hr))
        ATLTRACE("\nFailed adding propertypage");
}
最后,在 OnConnection() 和 OnDisconnection() 中,像以前一样调用 DispEventAdvise 和 DispEventUnadvise 来连接和断开与 ApplicationEvents 的连接。现在一切就绪,请重新构建项目。接下来按 F5 键并转到 Outlook 的“工具”->“选项”菜单。您应该看到我们刚刚添加的新选项卡。但是等等,当您单击新选项卡时,属性页没有显示,而是弹出一个 MessageBox,告诉我们无法显示属性页。发生什么了?我们所有的辛勤工作都白费了吗?别担心!

发生的情况是,尽管属性页已创建,但 Outlook 未收到有关属性页控件键盘行为的任何信息。IOleControl 的 GetControlInfo 方法的默认 ATL 实现返回 E_NOTIMPL,因此容器站点无法处理属性页或其中控件的任何键盘输入。因此,我们的页面未显示。要解决此问题,只需覆盖 GetControlInfo() 以返回 S_OK

在 PropPage.h 文件中,添加声明:

STDMETHOD(GetControlInfo)(LPCONTROLINFO lpCI);

我们的重写方法仅返回 S_OK,因此在 PropPage.cpp 文件中,按以下方式实现 GetControlInfo()

STDMETHODIMP CPropPage::GetControlInfo(LPCONTROLINFO lpCI)
{
    return S_OK;
}

就是这样。现在,当您构建项目并激活 Options 属性表的 OutlookAddin 选项卡时,我们的新属性页应该可以毫无错误地显示。

至此,我们来到了旅程的终点。您可以在 Outlook2000 或 Office2000 插件中执行的操作是无穷无尽的。由于在插件中,您可以访问父应用程序的内部对象模型,因此您可以执行应用程序所做的一切,甚至更多。此外,您还可以使用与任何应用程序不直接相关的其他接口,如 MS Assistant。唯一的限制就是您的想象力!

祝您插件编程愉快。;-)

更新

2003 年 4 月 2 日

本文在 CP 上产生的反响促成了这个小问答环节。当我发布这篇文章时,我并没有为接踵而至的大量问题和评论做好准备。但我认为这是一件好事。所以请继续提问!

这里讨论的一些主题和代码适用于所有 Office 插件。

  • 处理新的 Inspector 事件
  • 自定义 Outlook 菜单

处理新的 Inspector 事件

问:我如何将自定义按钮/菜单项添加到 Outlook 的窗口,如新建->邮件消息窗口等?

您可以通过 Inspector 和 Inspectors 集合访问 Outlook 的所有打开的窗口。特别是,InspectorEventInspectorsEvents dispinterface 具有您应该处理的一系列事件。最突出的是 InspectorsEvents::NewInspector 事件,该事件在每次打开新的 Inspector 窗口时触发。其声明如下:

 ....
 dispinterface InspectorsEvents {
        properties:
        methods:
            [id(0x0000f001), helpcontext(0x0050e6f0)]
            void NewInspector([in] _Inspector* Inspector);
    };

在插件类中处理 NewInspector 事件的一种方法是通过 ATL IDispatchImpl<> 类模板,然后在类重写的 Invoke() 实现中处理 NewInspector 事件。我们使用 IDispatchImpl<> 而不是其他 ATL 类,只是为了变化。以下是您需要添加到项目中的代码:

class ATL_NO_VTABLE CAddin : 
    public CComObjectRootEx < CComSingleThreadModel>,
    public CComCoClass < CAddin, &CLSID_Addin>,
        ...
        ...
        public IDispatchImpl < Outlook::InspectorEvents, 
               &DIID_InspectorsEvents, &LIBID_OLADDINLib>
{
...
...

private:
CComQIPtr <IConnectionPointContainer,&IID_IConnectionPointContainer> 
m_spInspectorsCPC; DWORD m_dwCookie;
};

接下来,声明您的重写 Invoke(),如下所示:

STDMETHOD(Invoke)(DISPID, REFIID, LCID, WORD, DISPPARAMS*, 
        VARIANT*, EXCEPINFO*, UINT*);

并按如下方式实现它:

HRESULT CAddin::Invoke(DISPID dispidMember,REFIID riid,LCID lcid, WORD wFlags,
                        DISPPARAMS* pDispParams, VARIANT* pvarResult,
                        EXCEPINFO*  pExcepInfo,  UINT* puArgErr)
{
 
    if(dispidMember = 0xf001)
    {
    if (!pDispParams)
        return E_INVALIDARG;
    
    if (pDispParams->cArgs > 0)
    {
       //the only parameter is an Inspector    *

       IDispatchPtr pDisp(pDispParams->rgvarg[0].pdispVal);  
       CComQIPtr<_Inspector> spInspector(pDisp);
            
       CComPtr<_CommandBars> pCmdBars;
       HRESULT hr = spInspector->get_CommandBars(&pCmdBars); 
       if(FAILED(hr))
        return hr;
    //once you have access to the new windows CommandBars collection,
    //now add your own menubars and toolbar items to it
    ....
    ....
    }
    }   
};

最后,我们需要 Advise Unadvise 来接收事件。这可以通过连接点来完成,如前所述。OnConnection() OnDisconnection() 是设置通信的理想场所。用于设置连接的代码如下:

    CComPtr<Outlook::_Inspectors> spInspectors;
    hr = spApp->get_Inspectors(&spInspectors);

    m_spInspectorsCPC = spInspectors;
    ATLASSERT(m_spInspectorsCPC);

    CComPtr spCP;
    hr = m_spInspectorsCPC->FindConnectionPoint(
__uuidof(Outlook::InspectorsEvents) ,&spCP); if(FAILED(hr)) return hr; hr = spCP->Advise( reinterpret_cast (this), &m_dwCookie); if (FAILED(hr)) return hr;

并在 OnDisconnection() 中断开连接:

    CComPtr<IConnectionPoint> spCP;
    hr = m_spInspectorsCPC->FindConnectionPoint(DIID_InspectorsEvents,&spCP);
    if(FAILED(hr))
        return hr;

    hr = spCP4->Unadvise(m_dwInspectors);
    if(FAILED(hr))
        return hr;

如果一切顺利,您应该能够成功处理 NewInspector 事件。本文已展示如何添加您自己的菜单和工具栏项。就是这样。

自定义 Outlook 菜单

问:如何将内容添加到 Outlook 的标准菜单项,如 FileEdit 等?

一旦您获得 Outlook CommandBars 集合,您就需要首先搜索“Menu Bar”CommandBar,然后遍历其 CommandBarControl 集合(其中包含 File、Edit 等菜单项)以获取目标菜单项。接下来获取其 CommandBarControls 集合并添加您的菜单项。请记住,CommandBarPopup 代表菜单项,CommandBarButton 代表工具栏项。如果您知道 Outlook 菜单 ID,也可以使用 FindControl()

这是我在插件的 OnConnection() 期间调用的一段小程序,它向 Outlook 的 File 菜单添加了一个菜单项:

HRESULT CAddin::AddNewMenu(_CommandBars *pCmdBars)
{
    CComPtr < Office::CommandBar> spNewCmdBar;
    
    HRESULT hr = pCmdBars->get_ActiveMenuBar(&spNewCmdBar);
    if (FAILED(hr))
        return hr;
    
    int nCount;
    hr =pCmdBars->get_Count(&nCount); 
    if (FAILED(hr))
        return hr;
    
    CComBSTR bstrBarName;
    CComBSTR bstrToolbarName(OLESTR("Menu Bar"));
    CComPtr < Office::CommandBar> pTempCmdBar;
    
    for (int i = 1; i <= nCount; i++)
    {
        pTempCmdBar = pCmdBars->GetItem(CComVariant(i));
        ATLASSERT(pTempCmdBar);
        
        pTempCmdBar->get_Name(&bstrBarName);
        if (_bstr_t(bstrBarName, TRUE) == _bstr_t(bstrToolbarName, TRUE)) 
        {
            CComPtr< Office::CommandBarControl > spCmdBarCtrl; 
            CComPtr< Office::CommandBarControls > spCmdBarCtrls; 
            
            spCmdBarCtrls = pTempCmdBar->GetControls(); 
            
            int n = spCmdBarCtrls->GetCount(); 
            
            for (int j = 1; j < n; j++)
            {
                spCmdBarCtrl = spCmdBarCtrls->GetItem(CComVariant(j));
                CComBSTR bstrCaption;
                spCmdBarCtrl->get_Caption(&bstrCaption); 
                CComBSTR bstrTarget(OLESTR("&File"));
                
                // we want to add to File menu
                if (_bstr_t(bstrCaption, TRUE) == _bstr_t(bstrTarget, TRUE))
                {
                    // File is a menu item
                    CComQIPtr<Office::CommandBarPopup> 
spPopup(spCmdBarCtrl->GetControl()); ATLASSERT(spPopup); CComPtr<CommandBarControls> spCmdCtrls; spCmdCtrls = spPopup->GetCommandBar()->GetControls(); ATLASSERT(spCmdCtrls); CComVariant vtType(1); // menu item is a button CComVariant vtEmpty(DISP_E_PARAMNOTFOUND, VT_ERROR); CComVariant vtTemp(VARIANT_TRUE); CComPtr< Office::CommandBarControl > spNewBarCtrl; // the 4th param to add is position before which // menu will be added. if you pass 2,for example, // menuitem will be added before the 2nd menuitem in File // menu, i.e. before Open. spNewBarCtrl = spCmdCtrls->Add(vtType, vtEmpty,
vtEmpty, vtEmpty, vtTemp); ATLASSERT(spNewBarCtrl); CComQIPtr< Office::_CommandBarButton >
spButton(spNewBarCtrl); ATLASSERT(spButton); //once you have the menuitem as a button //set it's icon and caption spButton->PutCaption(OLESTR("New Menu Item")); spButton->put_FaceId(4); spButton->PutStyle(Office::msoButtonIconAndCaption); spButton->PutVisible(VARIANT_TRUE); } } break; } pTempCmdBar.Detach(); } return S_OK; }


问:如何添加一个新菜单栏,例如,放在 Outlook 的 File 和 Edit 菜单之间?

这与前面关于向标准 Outlook 菜单项添加内容的问题有些相关。同样,我们需要做的是:从 Outlook 的 CommandBars 集合中获取活动的菜单栏 CommandBar,它代表 Outlook 的所有菜单。接下来,通过其 CommandBarControls 集合对此 CommandBar 对象进行添加。菜单是 CommandBarPopup 对象(MsoControlType::msoControlPopup = 10),我们需要向其添加菜单项,即 CommandBarButton(MsoControlType::msoControlButton = 1)。此外,在调用 CommandBarControls::Add() 时,我们使用第四个参数(Before)将位置指定为 2(即在 File 之后,在 Edit 之前)。

与前面那个小程序类似,AddNewMenubar() 在 File 和 Edit 之间添加了一个“Custom”菜单栏,并且只有一个按钮(菜单项)。

HRESULT CAddin::AddNewMenubar(_CommandBars *pCmdBars)
{
    CComPtr < Office::CommandBar > spNewCmdBar;
    
    HRESULT hr = pCmdBars->get_ActiveMenuBar(&spNewCmdBar);
    if (FAILED(hr))
        return hr;
    
    CComPtr< Office::CommandBarControls > spCtrls; 
    spCtrls = spNewCmdBar->GetControls(); 
    ATLASSERT(spCtrls);
    
    CComVariant varType(10);
    CComVariant vtEmpty(DISP_E_PARAMNOTFOUND, VT_ERROR);
    CComVariant vtTemp(VARIANT_TRUE);
    CComVariant vtBefore(2);
    CComPtr< CommandBarControl > spCtrl;
    spCtrl = spCtrls->Add(varType, vtEmpty, vtEmpty, vtBefore, vtTemp);
    
    ATLASSERT(spCtrl);
    
    CComQIPtr< Office::CommandBarPopup > spPopup(spCtrl->GetControl());
    ATLASSERT(spPopup);
    
    // set menu caption
    spPopup->PutCaption(OLESTR("Custom"));
    spPopup->PutVisible(VARIANT_TRUE);
    
    // now add a menu item to the menubar
    // as a CommandBarButton so you can specify styles
    
    CComPtr< Office::CommandBarControls > spCmdBarCtrls; 
    spCmdBarCtrls = spPopup->GetControls(); 
    
    // A menu is a CommandBarPopup 
    // object(MsoControlType::msoControlPopup = 10), 
    // to which we have to add our menuitems i.e. 
    // CommandBarButton(MsoControlType::msoControlButton = 1).
    // you can also add popup menus similarly.
    
    CComVariant vType(1); // button
    CComPtr< Office::CommandBarControl > spBarCtrl; 
    
    spBarCtrl = spCmdBarCtrls->Add(vType, vtEmpty, vtEmpty, vtEmpty, vtTemp); 
    ATLASSERT(spBarCtrl);
    
    
    CComQIPtr< Office::_CommandBarButton > spButton(spBarCtrl);
    ATLASSERT(spButton);
    
    // set button styles
    spButton->PutCaption(OLESTR("Menu1"));
    spButton->PutFaceId(4);
    spButton->PutStyle(Office::msoButtonIconAndCaption);
    spButton->PutVisible(VARIANT_TRUE); 
    
    return S_OK;
}

现在就到这里。再见!:)

参考和致谢

MSKB 文章
Q220600 - 如何使用 VC++ 自动化 Outlook
Q238228 - 在 VB 中构建 Office2000 COM 插件
Q259298 - 如何通过 #import 使用 Outlook Object Model
Q173604 - 如何在 Outlook 解决方案中使用 CommandBars
Q182394- 如何在 Outlook 解决方案中使用 CommandBars


MSDN 文章
为 Outlook2000 构建 COM 插件 - Thomas Rizzo
为 MS Office 2000 开发 COM 插件 - Ed Schultz

MS 新闻组
microsoft.public.office.developer.com.addins
microsoft.public.outlook.prog_addins

书籍
ATL Internals - Brent Rector & Chris Sells。
Developer's workshop to COM and ATL 3.0 - Andrew W.Troelsen。

其他
Renat Garyaev (garyaev@acm.org) 的 Vernoter 示例。
David Mavin,没有他,这一切项目就不会发生。
codeproject.com 上的人们,他们鼓励我并提供了急需的支持。
所有开发了所有 Office 应用程序背后出色的 COM 对象模型的开发人员。:)

80 年代的渐进摇滚/金属乐队 Queensryche 的音乐。

© . All rights reserved.