为 Outlook 2010 构建 C++ 加载项

**摘要:**本文介绍如何使用 C++ 创建自定义 Microsoft Outlook 2010 用户界面的托管加载项。了解如何自定义 Microsoft Office Fluent 用户界面的功能区组件以及如何向 Outlook 2010 添加自定义窗体区域。

上次修改时间: 2011年9月14日

适用范围: Office 2010 | Outlook 2010

本文内容
生成 Outlook 加载项简介
创建 C++ 加载项
添加功能区和功能区按钮
窗体区域
添加 Bing 搜索功能
结论
其他资源

单击以获取代码Outlook 2010 示例:生成 C++ 加载项(该链接可能指向英文页面)

目录

  • 生成 Outlook 加载项简介

  • 创建 C++ 加载项

  • 添加功能区和功能区按钮

  • 窗体区域

  • 添加 Bing 搜索功能

  • 结论

  • 其他资源

生成 Outlook 加载项简介

Microsoft Outlook 具有用于生成加载项的丰富模型,加载项可以极大地增强用户对 Outlook 的日常体验。要生成 Outlook 加载项,可以使用 Microsoft Visual Studio 2010 中的 Microsoft Office 开发工具(或者 Visual Studio 早期版本的 Microsoft Visual Studio Tools for Office),这是一组基于 Outlook 和 Microsoft Office 对象模型生成的托管库。此外,还可以使用托管代码(即 C++)。

备注

Microsoft Visual C++ 支持开发非托管 C++ 项目并且支持使用公共语言运行库 (CLR) 开发托管 C++ 项目。在本文的其余部分,C++ 指的是语言的非托管版本,托管 C++ 指的是基于 CLR 使用的 Visual C++。

下面的列表显示优先选择 C++ 而不是 C# 或 Visual Basic(Visual Studio 中的 Office 开发工具提供的两个语言选项)生成加载项的部分原因。

  • 性能

    一般来说,与使用托管语言编写的加载项相比,非托管加载项可以获得更好的性能。代码的执行速度可以更快,具体取决于使用的代码和算法。此外,非托管加载项的加载速度更快,因为它无需加载 CLR 即可正常运行。如果需要在加载项与 Outlook 之间来回移动大量数据,则非托管版本同样可以获得更好的性能,因为在每次调用时数据无需跨越本机/托管边界(称为封送)。

  • 分发包大小和较少的依赖项

    借助非托管加载项,用户无需安装 CLR 等必备组件。

    备注

    如果您的加载项支持 32 位和 64 位版本的 Outlook,则需要为每个版本生成单独的加载项版本。

  • 消息处理 API (MAPI) 支持

    如果您的加载项需要直接使用 MAPI,则必须使用 C++。不支持从托管代码直接调用 MAPI。

  • 版本控制

    尽管 .NET Framework 允许存在程序集的多个版本,但是 Visual Studio Tools for Office 运行库限制每个 Visual Studio Tools for Office 或 Visual Studio 2010 中的 Office 开发工具版本只能有一个程序集。使用非托管代码,可以更灵活地进行版本控制。

备注

本文面向具有使用 C++ 开发应用程序经验的中级和高级开发人员。本文不一定详细枚举 Visual Studio 中的常见过程。

本文只是以 Bing API 为例来说明如何向自定义用户界面提供功能。实际上可以使用 Outlook 对象模型等 API 在 Outlook 检查器功能区中连接自定义按钮。如果在应用程序中使用 Bing API,请确保代码符合当前受支持的 Bing 版本。有关详细信息,请参阅 Bing 开发人员中心(该链接可能指向英文页面)

创建 C++ 加载项

决定使用 C++ 生成 Outlook 加载项后,首先要使用以下过程创建 C++ 库应用程序。

备注

若要下载随本文提供的代码示例,请参阅 Outlook 2010 示例:生成 C++ 加载项(该链接可能指向英文页面)

第 1 步:创建新 ATL 项目

  1. 打开 Microsoft Visual Studio 2010。

  2. 在"文件"菜单上,指向"新建",然后单击"项目"。

  3. 在"新建项目"对话框中,从"Visual C++"节点中选择"ATL 项目"模板。

  4. 将此项目命名为 NativeAddIn。使用 ATL,可以获得适合使用 Outlook 加载项模型的基础结构,该基础结构基于 COM。

  5. 单击"确定"显示"ATL 项目向导"。

  6. 单击"完成"对此项目使用默认设置。

第 2 步:添加新对象

  1. 在"解决方案资源管理器"中右键单击项目节点,指向"添加",然后单击"类"。

  2. 在"添加类"对话框中,从列表中选择"ATL 简单对象"模板,然后单击"添加"。

  3. 在"ATL 简单对象向导"的"短名称"文本框中,键入 Connect。这是您的加载项将实现的类的名称。在生成加载项类时使用"Connect"是众所周知的惯例。

  4. 在"ProgID"文本框中,键入 NativeAddin.Connect,并由向导填写所有其他信息。

  5. 单击"完成"。

第 3 步:实现接口

接下来,使用 ATL 向导实现加载项所需的接口。

  1. 创建类后,转到"类视图"选项卡。(如果该选项卡尚不可见,请单击"视图"菜单上的"类视图",或者按 Ctrl+Shift+C)

  2. 在"类视图"窗口中展开"NativeAddIn"节点。

  3. 右键单击"CConnect"节点,该节点代表刚刚创建的类,指向"添加",然后单击"实现接口"。

    备注

    可能必须关闭并打开 Visual Studio,"CConnect"节点才能显示。这是 Visual Studio 2010 Beta 2 版本中的已知问题。

  4. 在"实现接口向导"对话框的"可用类型库"框中,选择"Microsoft 加载项设计器 <1.0>"。

  5. 在"接口"列表框中选择"_IDTExtensbility2",然后单击">"按钮,将接口添加到"实现接口"列表中。

  6. 单击"完成"。

_IDTExtensibility2 是所有 Office 加载项必须实现的接口。它定义加载项与宿主(在本示例中为 Outlook)之间的交互。主交互在首次对加载项进行加载时发生:Outlook 调用 OnConnection 方法。您可以通过该方法访问 Outlook 对象模型、存储对这些接口指针的引用并添加自定义项。

第 4 步:设置对 Outlook 加载项的加载

  1. 打开文件 Connect.h。

  2. 将所有方法更改为返回 S_OK 而不是 E_NOTIMPL。Outlook 调用所有这些方法,因此返回成功的结果非常重要。

  3. 向 OnConnection 方法添加代码,以便您可以验证 Outlook 是否加载您的加载项。添加对 MessageBoxW 的调用,如以下代码所示。

    STDMETHOD(OnConnection)(LPDISPATCH Application, 
        ext_ConnectMode ConnectMode, 
        LPDISPATCH AddInInst, 
        SAFEARRAY * * custom)
    {
        MessageBoxW(NULL,L"OnConnection",L"Native Addin",MB_OK);
        return S_OK;
    }
    

通过上面的讲解您已经了解如何创建加载项。然而,在 Outlook 实际加载您的加载项之前,它需要有关加载项的更多元数据信息,以确定加载项确实存在并了解如何加载。您需要将此信息添加到 Windows 注册表中。因为 ATL 项目已经嵌入了注册表脚本(用于将常规 COM 元数据添加到注册表中),所以只添加到该脚本同样可以将特定于 Outlook 的元数据添加到注册表。

第 5 步:将加载项的特定于 Outlook 的元数据插入到 Windows 注册表中

  1. 打开"解决方案资源管理器"。

  2. 展开"资源文件"文件夹,并打开"Connect.rgs"文件。

  3. 将以下注册表脚本添加到文件中。

    HKCU
    {
        NoRemove Software
        {
            NoRemove Microsoft
            {
                NoRemove Office
                {
                    NoRemove Outlook
                    {
                        NoRemove Addins
                        {
                            NativeAddin.Connect
                            {
                                val Description = s 'Sample Addin'
                                val FriendlyName = s 'Sample Addin'
                                val LoadBehavior = d 3
                            }
                        }
                    }
                }
            }
        }
    }
    

此注册表项通知 Outlook 应该加载此加载项,LoadBehavior 项告知 Outlook 自动加载此加载项。此时,如果生成项目,则正确的注册表项将添加到注册表中,并且您的加载项基本上已经可以加载了。

第 6 步:确定适当的加载项版本

现在,您需要确定运行的是 32 位还是 64 位版本的 Outlook。如果运行的是 32 位 Windows,则安装的 Office 位于驱动器:\Program Files\Microsoft Office 目录,如果在 64 位 Windows 上运行 32 位 Outlook,则位于驱动器:\Program Files (x86)\Microsoft Office 目录。如果运行的是 32 位版本的 Outlook,则已经完成;您可以生成加载项,然后打开 Outlook。

如果运行的是 64 位版本的 Outlook,则需要将项目更改为使用 64 位编译器生成加载项。64 位版本的 Outlook 不加载 32 位加载项。

按照以下步骤操作,为 64 位 Outlook 生成加载项:

  1. 在"解决方案资源管理器"中右键单击解决方案节点,然后单击"配置管理器",打开"配置管理器"。

  2. 从"配置管理器"的"活动解决方案平台"框中选择"新建",以便为"NativeAddin"项目创建新平台设置。

  3. 在"新建解决方案平台"对话框中,选择"x64"作为新平台,然后单击"确定"。

  4. 在"配置管理器"对话框中,单击"关闭"。

  5. 重新生成解决方案,然后尝试打开 Outlook。

打开 Outlook 时,您应该会看到 OnConnection 方法的以下代码行生成的消息框。

MessageBoxW(NULL, L"OnConnection", L"Native_Addin", MB_OK);
    return S_OK;

显示的消息框如图 1 所示。

图 1. NativeAddIn 在启动时显示消息框

在启动时显示消息框的 NativeAddIn

为了便于进行调试,应该将项目属性更改为在调试项目时加载 Outlook。

第 7 步(可选):将项目属性更改为加载 Outlook 以便于进行调试

  1. 在"解决方案资源管理器"中右键单击您的项目,然后单击"属性"。

  2. 在左侧窗格的树视图中展开"配置属性",然后选择"调试"节点。

  3. 对于右侧窗格中的"命令"属性,在单击属性右侧时显示的组合框中选择"浏览"。

  4. 浏览到计算机上的 Outlook.exe 文件(同样路径可能会不同,具体取决于您的操作系统以及运行的是 32 位还是 64 位版本的 Outlook)。

在继续之前,您可能要删除(或注释掉)CConnect 实现的 OnConnection 方法中的 MessageBoxW 调用。在进行生成和调试时,每次运行都必须关闭对话框,这是非常乏味的工作。最好注销掉该调用,因为如果稍后在加载或调试过程中遇到故障,始终可以删除注释进行测试,以确保加载项正在加载(或者找出未加载的原因)。

添加功能区和功能区按钮

生成加载项并且加载项可以加载后,您可能要添加一些其他功能。一项常见的自定义操作是向 Outlook 功能区添加新选项卡,并向新选项卡添加选项卡组和按钮。

要完成此任务,需要实现另一接口:IRibbonExtensibility。IRibbonExtensibility 是一个比较简单的接口,因为它只有一个方法 GetCustomUI。GetCustomUI 将 BSTR 作为输出参数,该参数应该包含使用 Microsoft Office Fluent 用户界面架构进行了格式设置的 XML。XML 然后将用于在宿主(在本示例中为 Outlook)中添加其他功能区元素。

IRibbonExtensibility 并没有位于您先前添加的类型库中,而是位于另一个类型库中,因此需要将该类型库添加到项目中。实际上,现在最好清理 StdAfx.h 头文件,以使项目可以更稳定地在不同平台和计算机上运行。

如果打开 StdAfx.h 文件(参阅图 2),您将注意到 ATL 实现接口向导在 #import 语句中使用了绝对路径(这将导致编译项目时类型库被转换为 C++/COM 头文件)。这对您的计算机没什么影响,但如果要与其他开发人员共享项目,则他们的路径可能会不同。此外,如果在 32 位和 64 位系统之间移动,路径可能也会不同。

图 2. 包含向导 #import 语句的 StdAfx.h 文件

包含向导 #import 语句的 StdAfx.h 文件

通过引用类型库的 LIBID(库标识符或 GUID)而不是路径,可以使 #import 语句更加可靠。您还需要包含 Office 类型库(包含 IRibbonExtensibility 接口)的另一个 import 语句。这将使 #import 语句更加可靠,并且添加 rename_namespace 可以解决导入类型库后存在命名冲突时可能引发的问题。

以下过程演示如何添加功能区和功能区按钮。

第 1 步:清理 StdAfx.h 文件

  1. 打开 StdAfx.h 文件。

  2. 删除 #import 语句并替换为以下代码。

    #import "libid:AC0714F2-3D04-11D1-AE7D-00A0C90F26F4"\
    auto_rename auto_search raw_interfaces_only rename_namespace("AddinDesign")
    
    // Office type library (i.e., mso.dll).
    #import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52"\
        auto_rename auto_search raw_interfaces_only rename_namespace("Office")
    using namespace AddinDesign;
    using namespace Office;
    

现在将对 CConnect 类进行一些手动更改,以支持与功能区进行交互。

第 2 步:更改 CConnect 类以支持与功能区进行交互

  1. 打开 Connect.h 文件。

    现在,将为用于简化后续任务的不同接口添加一些 typedef 声明。

  2. 在 CConnect 类上查找 _IDTExtensibility2 的 IDispatchImpl 声明(参阅图 3),并将其更改为名为 IDTImpl 的 typedef。

    图 3. 向导生成的 IDispatchImpl

    向导生成的 IDispatchImpl

  3. 为 IRibbonExtensibility 添加名为 RibbonImpl 的类似 typedef,然后将其添加到 CConnect 的基类列表。完成后,类应该类似于以下代码。

    typedef IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &__uuidof(__AddInDesignerObjects), /* wMajor = */ 1>
    IDTImpl;
    
    typedef IDispatchImpl<IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &__uuidof(__Office), /* wMajor = */ 2, /* wMinor = */ 5>
    RibbonImpl;
    
    class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_NativeAddinLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDTImpl,
    public RibbonImpl
    
  4. 向 ATL COM MAP 添加以下接口。

    COM_INTERFACE_ENTRY(IRibbonExtensibility)
    
  5. 将所有方法(包括您尚未添加实际功能的方法)更改为返回 S_OK 而不是 E_NOT_IMPL。

第 3 步:实现 IRibbonExtensibility

为 IRibbonExtensibility 添加实现。它应该类似于以下代码。

public:
STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml)
{
    if(!RibbonXml)
        return E_POINTER;
    *RibbonXml = CComBSTR("XML GOES HERE");  
    return S_OK;
}

现在,您需要决定功能区 XML 的存储位置。一个选择是将 XML 直接嵌入代码中,但是更便于管理的解决方法是将 XML 作为资源添加到项目中。

第 4 步:将功能区 XML 设置为项目的资源

  1. 在"解决方案资源管理器"中,右键单击"NativeAddIn"项目,指向"添加",然后单击"新项目"。

  2. 从"添加新项"对话框中选择"XML 文件"模板,并将其命名为 RibbonManifest.xml。

  3. 单击"添加"将其添加到项目中。

功能区 XML 相当简单。它包含一个 ribbon 元素和一个 tab,后者可以包含多个 tab 元素。在功能区中,选项卡可以包含组,每个组可以包含多个按钮。图 4 显示如何将此转换为 Outlook 功能区用户界面 (UI)。组由红色矩形表示,按钮由蓝色椭圆表示。

图 4. Outlook 功能区上的选项卡、组和按钮

Outlook 功能区上的选项卡、组和按钮

现在,需要创建新选项卡、组和按钮。

第 5 步:为新选项卡、组和按钮指定 XML

打开 RibbonManifest.xml 文件并添加以下 XML。

<customUI xmlns="https://schemas.microsoft.com/office/2006/01/customui">
    <ribbon>
        <tabs>
            <tab id="Bing" label="Bing">
                <group id="BingGroup" label="Bing Group">
                    <button id="NewCustomButton" 
                        imageMso="WebPagePreview" 
                        size="large" 
                        label="Search Bing" 
                        onAction="ButtonClicked"/>
                </group>
            </tab>
        </tabs>
    </ribbon>
</customUI>

在第 4 步中设置的 RibbonManifest.xml 文件已存储为资源,因此您可以从 IRibbonExtensibility::GetCustomUI 方法返回 XML。

第 6 步:实现 IRibbonExtensibility::GetCustomUI 以返回 XML

  1. 在"视图"菜单上,指向"其他窗口",然后单击"资源视图"以转到此项目的"资源视图"。

  2. 展开 NativeAddIn 节点,右键单击 NativeAddIn.rc 节点,然后单击"添加资源"。

  3. 在"添加资源"对话框中,单击"导入"按钮。

  4. 在对话框的右下角将文件类型更改为"所有文件",在文件列表中选择 RibbonManifest.xml 文件,然后单击"打开"。

  5. 在"自定义资源类型"对话框中,将资源类型指定为"XML",然后单击"确定"。

  6. 现在,需要一些可以真正处理 XML 资源的代码。您可以自己实现,也可以将以下代码添加到 CConnect 类。

    HRESULT HrGetResource(int nId, 
        LPCTSTR lpType, 
        LPVOID* ppvResourceData,       
        DWORD* pdwSizeInBytes)
    {
        HMODULE hModule = _AtlBaseModule.GetModuleInstance();
        if (!hModule)
            return E_UNEXPECTED;
        HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(nId), lpType);
        if (!hRsrc)
            return HRESULT_FROM_WIN32(GetLastError());
        HGLOBAL hGlobal = LoadResource(hModule, hRsrc);
        if (!hGlobal)
            return HRESULT_FROM_WIN32(GetLastError());
        *pdwSizeInBytes = SizeofResource(hModule, hRsrc);
        *ppvResourceData = LockResource(hGlobal);
        return S_OK;
    }
    
    BSTR GetXMLResource(int nId)
    {
        LPVOID pResourceData = NULL;
        DWORD dwSizeInBytes = 0;
        HRESULT hr = HrGetResource(nId, TEXT("XML"), 
            &pResourceData, &dwSizeInBytes);
        if (FAILED(hr))
            return NULL;
        // Assumes that the data is not stored in Unicode.
        CComBSTR cbstr(dwSizeInBytes, reinterpret_cast<LPCSTR>(pResourceData));
        return cbstr.Detach();
    }
    
    SAFEARRAY* GetOFSResource(int nId)
    {
        LPVOID pResourceData = NULL;
        DWORD dwSizeInBytes = 0;
        if (FAILED(HrGetResource(nId, TEXT("OFS"), 
            &pResourceData, &dwSizeInBytes)))
            return NULL;
        SAFEARRAY* psa;
        SAFEARRAYBOUND dim = {dwSizeInBytes, 0};
        psa = SafeArrayCreate(VT_UI1, 1, &dim);
        if (psa == NULL)
            return NULL;
        BYTE* pSafeArrayData;
        SafeArrayAccessData(psa, (void**)&pSafeArrayData);
        memcpy((void*)pSafeArrayData, pResourceData, dwSizeInBytes);
        SafeArrayUnaccessData(psa);
        return psa;
    }
    
  7. 插入以下代码来完成 IRibbonExtensibility::GetCustomUI 的实现,以便返回实际 XML。

    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml)
    {
        if(!RibbonXml)
            return E_POINTER;
        *RibbonXml = GetXMLResource(IDR_XML1);
        return S_OK;
    }
    

如果生成加载项并运行 Outlook,现在应该会看到添加的选项卡、组和按钮,如图 5 所示。

图 5. Outlook 功能区上添加的选项卡、组和按钮

Outlook 功能区上新增的选项卡、组和按钮

现在需要添加一些代码,以便在用户单击"Search Bing"按钮时做出响应。(稍后,将实现实际 Bing 搜索 API 代码。)

与很多其他回调环境不同,Office(包括 Outlook)使用 IDispatch 作为回调到 IRibbonExtensibility 实现的方法,这与使用连接点的自定义接口相反。您的对象需要在传入功能区 XML 中的 onAction 属性值(在本示例中为 ButtonClicked)时从 GetIDsofName 返回 DISPID,并且使 IDispatch::Invoke 方法正确响应 DISPID。该方法还需要一个输入参数,该参数是 IDispatch 指针。

代码现在有点复杂,因为它不仅仅是实现简单 COM 加载项通常所需的代码,而是深入到了 ATL 中的更低级别。

此时,您首先需要创建具有 ButtonClicked 方法的 COM 接口。

第 7 步:声明具有 ButtonClicked 方法的 COM 接口

  1. 打开项目的 IDL 文件 NativeAddIn.idl。

  2. 添加接口。该接口应该为双重接口,并且您应该为 id 属性选择一个唯一 DISPID 值。这样有可能避免稍后出现 DISPID 冲突。NativeAddIn.idl 文件中的声明应该类似于以下代码。

    [
        object,
        uuid(CE895442-9981-4315-AA85-4B9A5C7739D8),
        dual,
        nonextensible,
        helpstring("IRibbonCallback Interface"),
        pointer_default(unique)
    ]
    interface IRibbonCallback : IDispatch{
        [id(42),helpstring("Button Callback")]
        HRESULT ButtonClicked([in]IDispatch* ribbonControl);
    };
    
  3. 使 IRibbonCallback 取代 IConnect 来作为类的默认接口。

    coclass Connect
    {
        [default] interface IRibbonCallback;
    };
    

声明接口并设置默认接口后,您可以使 CConnect 类实现该接口。

第 8 步:实现接口

  1. 在 Connect.h 文件的开头添加以下声明。

    typedef IDispatchImpl<IRibbonCallback, &__uuidof(IRibbonCallback)> 
    CallbackImpl;
    

    添加 typedef 对于稍后实现 IDispatch::Invoke 方法非常有用。

  2. 将接口添加到 CConnect 类的基本层次结构。该类现在应该类似于以下代码。

    class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_NativeAddInLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDTImpl,
    public RibbonImpl,
    public CallbackImpl
    
  3. 将接口添加到 ATL COM MAP,并替换 COM_INTERFACE_ENTRY2 条目中的 _IDTExtensibility2 接口。您的 COM MAP 应该类似于以下代码。

    BEGIN_COM_MAP(CConnect)
        COM_INTERFACE_ENTRY2(IDispatch, IRibbonCallback)
        COM_INTERFACE_ENTRY(IConnect)
        COM_INTERFACE_ENTRY(_IDTExtensibility2)
        COM_INTERFACE_ENTRY(IRibbonExtensibility)
        COM_INTERFACE_ENTRY(IRibbonCallback)
    END_COM_MAP()
    
  4. 实现 IRibbonCallback::ButtonClicked 方法。添加以下代码以打开一个消息框,以便您可以确认回调正在正常工作。

    STDMETHOD(ButtonClicked)(IDispatch* ribbon)
    {
        MessageBoxW(NULL,L"Button Clicked!",L"NativeAddin",MB_OK);
        return S_OK;
    }
    

还需要完成另外一个步骤,此回调才能正常工作:必须重写 IDispatch::Invoke 的实现。可以通过多种方式执行此操作,但是以下过程将实现该目标。请注意,这并不是通过 IDispatch 公开多个自定义接口的一般方法。

第 9 步:重写 IDispatch::Invoke 的实现

  1. 将以下代码添加到 CConnect 类中。

    STDMETHOD(Invoke)(DISPID dispidMember, 
        const IID &riid, 
        LCID lcid, 
        WORD wFlags, 
        DISPPARAMS *pdispparams, 
        VARIANT *pvarResult, 
        EXCEPINFO *pexceptinfo, 
        UINT *puArgErr)
    {
        HRESULT hr=DISP_E_MEMBERNOTFOUND;
        if(dispidMember==42)
        {
            hr  = CallbackImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags,       
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        }
        if (DISP_E_MEMBERNOTFOUND == hr)
            hr = IDTImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags, 
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        return hr;
    }
    

    此代码确定 dispidMember 参数对于回调是否唯一(在本示例中为 42)。如果唯一,则代码调用 CallbackImpl::Invoke 方法。否则,它调用 IDTImpl::Invoke 方法,因为这是此时您需要的唯一其他 IDispatch 接口。

此时,您可以生成加载项。单击新功能区上的自定义按钮时,应该显示一个消息框。

窗体区域

向 Outlook 添加 UI 的另一种方式是添加自定义窗体区域。您可以通过自定义窗体区域将控件面板添加到 Outlook 检查器中。您可以将这些窗体区域放置在检查器中的不同位置,包括取代整个对话框。

要完成此加载项,您将向用于显示邮件项目的窗体添加一个窗体区域。该窗体区域将包含 Web 浏览器控件。在加载项中生成窗体区域使用的步骤与添加功能区 UI 所需的步骤相似。使用以下过程添加自定义窗体区域。

首先,需要添加所需的类型库。在本示例中,您将添加三个类型库:分别用于 Microsoft Word(因为 Outlook 使用 Word 编辑电子邮件)、Outlook(允许您根据 Outlook 对象模型进行编程)和窗体区域。

第 1 步:导入所需的类型库

将以下 #import 语句添加到 StdAfx.h 文件中。

#import "libid:00020905-0000-0000-C000-000000000046"\
auto_rename auto_search raw_interfaces_only rename_namespace("Word")

// Outlook type library (i.e., msoutl.olb).
#import "libid:00062FFF-0000-0000-C000-000000000046"\
auto_rename auto_search raw_interfaces_only rename_namespace("Outlook")

// Forms type library (i.e., fm20.dll).
#import "libid:0D452EE1-E08F-101A-852E-02608C4D0BB4"\
auto_rename auto_search raw_interfaces_only  rename_namespace("Forms")
using namespace Word;
using namespace Outlook;

using namespace Forms;

现在,将实现 _FormRegionStartup 接口。

第 2 步:实现 _FormRegionStartup 接口

  1. 将 IDispatchImpl 的以下 typedef 添加到 Connect.h 文件的开头。

    typedef IDispatchImpl<_FormRegionStartup, &__uuidof(_FormRegionStartup), &__uuidof(__Outlook), /* wMajor = */ 9, /* wMinor = */ 4>
    FormImpl;
    
  2. 将 FormImpl 定义添加到 CConnect 类中。最终 CConnect 声明应该类似于以下代码。

    class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_NativeAddinLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDTImpl,
    public RibbonImpl,
    public CallbackImpl,
    public FormImpl
    
  3. 因为在上一节中重写了 IDispatch::Invoke,所以需要将以下代码添加到该方法,以处理新接口。

    STDMETHOD(Invoke)(DISPID dispidMember, 
        const IID &riid, 
        LCID lcid, 
        WORD wFlags, 
        DISPPARAMS *pdispparams, 
        VARIANT *pvarResult,  
        EXCEPINFO *pexceptinfo, 
        UINT *puArgErr)
    {
        HRESULT hr=DISP_E_MEMBERNOTFOUND;
        if(dispidMember==42)
        {
            hr  = CallbackImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags, 
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        }
        if (DISP_E_MEMBERNOTFOUND == hr)
            hr = IDTImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags, 
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        if (DISP_E_MEMBERNOTFOUND == hr)
            hr = FormImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags, 
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        return hr;
    }
    
  4. 将 _FormRegionStartup 添加到 ATL COM 接口映射。

    BEGIN_COM_MAP(CConnect)
        COM_INTERFACE_ENTRY2(IDispatch, IRibbonCallback)
        COM_INTERFACE_ENTRY(IConnect)
        COM_INTERFACE_ENTRY(_IDTExtensibility2)
        COM_INTERFACE_ENTRY(IRibbonExtensibility)
        COM_INTERFACE_ENTRY(IRibbonCallback)
        COM_INTERFACE_ENTRY(_FormRegionStartup)
    END_COM_MAP()
    
  5. 添加 _FormRegionStartup 的实现。

    public:
    STDMETHOD(GetFormRegionStorage)(BSTR FormRegionName, 
        LPDISPATCH Item, 
        long LCID, 
        OlFormRegionMode FormRegionMode, 
        OlFormRegionSize FormRegionSize, 
        VARIANT * Storage)
    {
        V_VT(Storage) = VT_ARRAY | VT_UI1;
        V_ARRAY(Storage) = GetOFSResource(IDR_OFS1);
        return S_OK;
    }
    
    STDMETHOD(BeforeFormRegionShow)(_FormRegion * FormRegion)
    {
        if(m_pFormRegionWrapper)
            delete m_pFormRegionWrapper;
        m_pFormRegionWrapper = new FormRegionWrapper();
        if (!m_pFormRegionWrapper)
            return E_OUTOFMEMORY;
        return m_pFormRegionWrapper->HrInit(FormRegion);
    }
    
    STDMETHOD(GetFormRegionManifest)(BSTR FormRegionName, 
        long LCID, 
        VARIANT * Manifest)
    {
        V_VT(Manifest) = VT_BSTR;
        BSTR bstrManifest = GetXMLResource(IDR_XML2);
        V_BSTR(Manifest) = bstrManifest;
        return S_OK;
    }
    
    STDMETHOD(GetFormRegionIcon)(BSTR FormRegionName, 
        long LCID, 
        OlFormRegionIcon Icon, 
        VARIANT * Result)
    {
        return S_OK;
    }
    
    private:
    FormRegionWrapper* m_pFormRegionWrapper;
    
  6. 修改 FinalConstruct 以初始化 m_pFormRegionWrapper。

    HRESULT FinalConstruct()
    {
        m_pFormRegionWrapper = NULL;
        return S_OK;
    }
    

请注意,实现包含 _FormRegionStartup::GetFormRegionManifest 方法,该方法与 IRibbonExtensibility::GetCustomUI 类似。GetFormRegionManifest 方法也需要返回一些特定于 Outlook 的 XML,以告知 Outlook 有关要添加的窗体区域的信息。

第 3 步:在 XML 中指定窗体区域的显示方式

  1. 将另一 XML 文件添加到项目中。将其命名为 FormManifest.xml。

  2. 将以下 XML 添加到该文件中。

    <FormRegion xmlns="https://schemas.microsoft.com/office/outlook/12/formregion.xsd">
        <name>Native</name>
        <formRegionType>adjoining</formRegionType>
        <formRegionName>Search Bing</formRegionName>
        <hidden>true</hidden>
        <ribbonAccelerator>I</ribbonAccelerator>
        <showInspectorCompose>true</showInspectorCompose>
        <showInspectorRead>true</showInspectorRead>
        <showReadingPane>false</showReadingPane>
        <addin>NativeAddin.Connect</addin>
    </FormRegion>
    
  3. 转到此项目的"资源视图"(在"视图"菜单上,指向"其他窗口",然后单击"资源视图")。

  4. 展开"NativeAddIn"节点,右键单击"NativeAddIn.rc"节点,然后单击"添加资源"。

  5. 在"添加资源"对话框中,单击"导入"按钮。

  6. 在对话框的右下角将文件类型更改为"所有文件",在文件列表中选择 FormManifest.xml 文件,然后单击"打开"。

  7. 在"自定义资源类型"对话框中,将资源类型指定为"XML",然后单击"确定"。

要完成此实现,需要创建帮助程序类 FormRegionWrapper,_FormRegionStartup 接口使用该帮助程序类管理窗体区域(并调用 Bing API)。

第 4 步:创建帮助程序类以管理窗体区域并调用 Bing API

  1. 将以下 #include 语句添加到 StdAfx.h 头文件中。

    #include <msxml2.h>//msxml for parsing the Bing results
    #include <Mshtml.h>//mshtml for controlling the web browser control
    #include <Winhttp.h>//for making the HTTP calls to Bing
    #include <atlstr.h>//for CString
    #include <atlsafe.h>//for the ATL Safearray helper classes
    
  2. 将 Winhttp.lib 和 Msxml2.lib 添加为链接器依赖项。为此,请右键单击您的项目,然后单击"属性"。在"属性"对话框中,展开"链接器"节点,然后选择"输入"。单击"附加依赖项"旁边的下拉框,然后单击"编辑"。在"附加依赖项"对话框中,分行键入 winhttp.lib 和 msxml2.lib,然后单击"确定"。

  3. 创建名为 FormRegionWrapper.h 的新头文件,并将以下代码添加到该文件中。

    #pragma once
    #include "stdafx.h"
    #include <vector>
    #include <algorithm>
    
    /*!-----------------------------------------------------------------------
    FormRegionWrapper - Used to help track all the form regions and to 
    listen to some basic events such as the send button click and close.
    -----------------------------------------------------------------------!*/
    class FormRegionWrapper;
    typedef 
    IDispEventSimpleImpl<2, FormRegionWrapper, &__uuidof(FormRegionEvents)>
    FormRegionEventSink;
    const DWORD dispidEventOnClose = 0xF004;
    class FormRegionWrapper : public FormRegionEventSink
    {
        public:
            HRESULT HrInit(_FormRegion* pFormRegion);
            void Show();
            void SearchSelection();
            void Search(BSTR term);
        private:
            static _ATL_FUNC_INFO VoidFuncInfo; 
        public:
            BEGIN_SINK_MAP(FormRegionWrapper)
            SINK_ENTRY_INFO(2, __uuidof(FormRegionEvents), 
                dispidEventOnClose,    
                OnFormRegionClose, 
                &VoidFuncInfo)
            END_SINK_MAP()
            void __stdcall OnFormRegionClose();
        private:
            CComPtr<_FormRegion> m_spFormRegion;
            CComPtr<_MailItem> m_spMailItem;
            CComPtr<IWebBrowser> m_spWebBrowser;
    };
    
  4. 创建名为 FormRegionWrapper.cpp 的 C++ (.cpp) 文件,并将以下代码添加到该文件中。

    #include "stdafx.h"
    #include "FormRegionWrapper.h"
    using namespace ATL;
    
    #define ReturnOnFailureHr(h) { hr = (h); ATLASSERT(SUCCEEDED((hr))); if (FAILED(hr)) return hr; }
    
    // Macro that calls a COM method that returns an HRESULT value.
    #define CHK_HR(stmt)        do { hr=(stmt); if (FAILED(hr)) ; } while(0)
    // Macro to verify memory allocation.
    #define CHK_ALLOC(p)        do { if (!(p)) { hr = E_OUTOFMEMORY; ; } } while(0)
    // Macro that releases a COM object if not NULL.
    #define SAFE_RELEASE(p)     do { if ((p)) { (p)->Release(); (p) = NULL; } } while(0)
    /*!-----------------------------------------------------------------------
    FormRegionWrapper implementation
    -----------------------------------------------------------------------!*/
    _ATL_FUNC_INFO FormRegionWrapper::VoidFuncInfo = {CC_STDCALL, VT_EMPTY, 0, 0}; 
    
    HRESULT FormRegionWrapper::HrInit(_FormRegion* pFormRegion)
    {
        HRESULT hr = S_OK;
        m_spFormRegion = pFormRegion;
        FormRegionEventSink::DispEventAdvise(m_spFormRegion);
        CComPtr<IDispatch> spDispatch;
        ReturnOnFailureHr(pFormRegion->get_Form(&spDispatch));
        CComPtr<Forms::_UserForm> spForm;
        ReturnOnFailureHr(spDispatch->QueryInterface(&spForm));
        CComPtr<Forms::Controls> spControls;
        ReturnOnFailureHr(spForm->get_Controls(&spControls));
        CComPtr<Forms::IControl> spControl;
        CComBSTR bstrWBName(L"_webBrowser");
        spControls->_GetItemByName(bstrWBName.Detach(),&spControl);
        ReturnOnFailureHr(spControl->QueryInterface<IWebBrowser>
            (&m_spWebBrowser));
        spControl.Release();
        CComPtr<IDispatch> spDispItem;
        ReturnOnFailureHr(pFormRegion->get_Item(&spDispItem));
        ReturnOnFailureHr(spDispItem->QueryInterface(&m_spMailItem));
        return hr;
    }
    
    void FormRegionWrapper::Show()
    {
        if(m_spFormRegion)
        {
            m_spFormRegion->Select();
        }
    }
    
    void FormRegionWrapper::SearchSelection()
    {
        if (m_spMailItem)
        {
            CComPtr<_Inspector> pInspector;
            m_spMailItem->get_GetInspector(&pInspector);
            CComPtr<IDispatch> pWordDispatch;
            pInspector->get_WordEditor(&pWordDispatch);
            CComQIPtr<Word::_Document> pWordDoc(pWordDispatch);
            CComPtr<Word::_Application> pWordApp;
            pWordDoc->get_Application(&pWordApp);
            CComPtr<Word::Selection> pSelection;
            pWordApp->get_Selection(&pSelection);
            if(pSelection)
            {
                CComBSTR text;
                pSelection->get_Text(&text);
                Search(text);
            }
        }
    }
    
    void FormRegionWrapper::Search(BSTR term)
    {
        if(m_spWebBrowser)
        {
            CComBSTR html("<html><body><H1>You searched for:");
            html.AppendBSTR(term);
            html.Append("</H1></body></html>");
            CComPtr<IDispatch> docDispatch;
            m_spWebBrowser->get_Document(&docDispatch);
            if(docDispatch==NULL)
            {
                VARIANT vDummy;
                vDummy.vt=VT_EMPTY;
                m_spWebBrowser->Navigate
                    (L"about:blank",&vDummy,&vDummy,&vDummy,&vDummy);
                m_spWebBrowser->get_Document(&docDispatch);
            }
            if(docDispatch!=NULL)
            {
                CComPtr<IHTMLDocument2> doc;
                HRESULT hr = 
                    docDispatch.QueryInterface<IHTMLDocument2>(&doc);
                if(hr==S_OK)
                {
                    SAFEARRAY *psaStrings = 
                        SafeArrayCreateVector(VT_VARIANT, 0, 1);
                    VARIANT *param;
                    HRESULT hr = 
                        SafeArrayAccessData(psaStrings, (LPVOID*)&param);
                    param->vt = VT_BSTR;
                    param->bstrVal = html.Detach();
                    hr = SafeArrayUnaccessData(psaStrings);
                    doc->write(psaStrings);
                    SafeArrayDestroy(psaStrings);
                }
            }
        }
    }
    
    void FormRegionWrapper::OnFormRegionClose()
    {
        if (m_spMailItem)
        {
            m_spMailItem.Release();
        }
        if (m_spFormRegion)
        {
            FormRegionEventSink::DispEventUnadvise(m_spFormRegion);
            m_spFormRegion.Release();
        }
    }
    

    备注

    为了简单起见,此文件中包含多个类;然而在实际应用程序中,应该将每个类放在单独的文件中。

  5. 将针对 FormRegionWrapper.h 的 #include 语句添加到 Connect.h 头文件中。

  6. 通过添加以下代码修改 ButtonClicked 回调方法,以调用 FormRegion 成员变量 (m_pFormRegion) 的 SearchSelection 方法。

    STDMETHOD(ButtonClicked)(IDispatch* ribbon)
    {
        if(m_pFormRegionWrapper)
        {
            m_pFormRegionWrapper->SearchSelection();
        }
        return S_OK;
    }
    

您已经为 Outlook 创建用于显示窗体区域的 XML 以及用于运行窗体区域的代码。现在将创建窗体区域本身。

第 5 步:创建窗体区域并在 Outlook 中添加控件

  1. 在 Outlook 的功能区上,启用"开发工具"选项卡。依次单击"文件"选项卡、"选项"和"自定义功能区"。在"自定义功能区"下,选中"开发工具"复选框以启用"开发工具"选项卡,然后单击"确定"。

  2. 启用"开发工具"选项卡后,将其打开并单击"设计窗体"按钮。

  3. 选择"邮件"窗体,然后单击"打开"。

    在这种情况下,在"设计窗体"对话框中选择哪个窗体不重要,因为最终要从此对话框导出窗体区域。

  4. 显示窗体设计器后,单击"新建窗体区域"按钮。这将创建一个新窗体区域,并将它置于窗体设计器的最上面。

  5. 单击"控件工具箱"按钮,以便显示您可以添加到窗体的控件。

  6. 现在添加自定义控件。在"工具箱"中,右键单击"控件"选项卡的任意位置,然后单击"自定义控件"。在"可用控件"下,选择"Microsoft Web 浏览器",然后单击"确定"。

    在本示例中使用 WebBrowser 控件是一个不错的选择,因为在其中显示 Bing 搜索结果列表相当简单。还可以在控件中插入超链接;Outlook 最终用户可以单击控件中的链接并在其浏览器中转到相应页面。

  7. 通过将"WebBrowser"控件从"工具箱"拖动到窗体区域,将该控件添加到窗体中。调整控件大小以便占满窗体区域中的所有空间。

  8. 将新添加的控件的名称更改为 _webBrowser。为此,请右键单击该控件,然后单击"属性"。准确命名控件非常重要,因为 FormRegionWrapper 类中的代码按名称(只是一个字符串)检索控件。

  9. 为了美观,请转到 WebBrowser 控件属性的"版式"选项卡,并将"水平"和"垂直"属性设置为"随窗体增大/缩小"。您的窗体应该如图 6 所示。

    图 6. 包含新增控件的窗体区域

    包含新增控件的表单区域

  10. 将窗体区域另存为 .ofs 文件(.ofs 文件格式是 Outlook 用于窗体区域的二进制文件)。为此,请单击"保存区域"按钮,然后单击"窗体区域另存为"。指定文件名 WebBrowser.ofs 并将它保存在 NativeAddIn 的项目目录中。

第 6 步:将窗体区域与 NativeAddIn 连接

  1. 返回 Visual Studio 2010 并在"资源视图"中展开"NativeAddIn"节点。右键单击"NativeAddIn.rc"节点,然后单击"添加资源"。

  2. 在"添加资源"对话框中,单击"导入"按钮。

  3. 在对话框的右下角将文件类型更改为"所有文件",在文件列表中选择 WebBrowser.ofs 文件,然后单击"打开"。

  4. 将资源类型指定为"OFS",然后单击"确定"。

调用 Connect.h 文件中的 _FormRegionStartup::GetFormRegionStorage 方法时,它将 .ofs 文件中的数据返回到 Outlook 中。在生成和试用加载项之前,您需要在 Connect.rgs 文件中添加一些其他项。同样必须在 Windows 注册表中注册窗体区域。

第 7 步:在注册表中添加项

在 Connect.rgs 文件中插入以下代码。

HKCU
{
    NoRemove Software
    {
        NoRemove Microsoft
        {
            NoRemove Office
            {
                NoRemove Outlook
                {
                    NoRemove Addins
                    {
                        NativeAddin.Connect
                        {
                            val Description = s 'Sample Addin'
                            val FriendlyName = s 'Sample Addin'
                            val LoadBehavior = d 3
                        }
                    }
                    NoRemove FormRegions
                    {
                        IPM.Note
                        {
                            val TestNativeCOMAddin = s '=NativeAddin.Connect'
                        }
                        IPM.Note.NativeAddin
                        {
                            val TestNativeCOMAddin = s '=NativeAddin.Connect'
                        }
                    }
                }
            }
        }
    }
}

第 8 步:生成并测试加载项

  1. 生成项目。

  2. 打开 Outlook 并创建新邮件。

  3. 在邮件窗体的底部,您应该可以看到创建的窗体区域。突出显示邮件中的文本,然后单击"Search Bing"按钮。您的代码应该修改 WebBrowser 控件并显示搜索词(参阅图 7)。

    图 7. 显示的自定义功能区和窗体区域

    显示的自定义功能区和表单区域

添加 Bing 搜索功能

现在,您已经了解如何使用 C++ 生成非托管 Outlook 加载项。拥有基础结构后,您将能够基于此基础结构生成特定功能。

现在您的 Outlook 加载项可以正常工作,但是添加一些其他功能可能更好。本文主要介绍如何生成非托管 Outlook 加载项,从这方面考虑本文不一定要介绍如何添加其他功能,但是这部分内容可以很好地阐述跨产品的功能。如果需要,您可以使用以下过程包含基于电子邮件中所选词的 Bing 搜索。

备注

必须在 Bing 开发人员中心(该链接可能指向英文页面)上注册 AppID 才能添加此功能。

添加 Bing 搜索功能

  1. 使用以下代码替换 FormsRegionWrapper.cpp 文件中的代码:

    #include "stdafx.h"
    #include "FormRegionWrapper.h"
    using namespace  ATL;
    
    #define ReturnOnFailureHr(h) { hr = (h); ATLASSERT(SUCCEEDED((hr))); if (FAILED(hr)) return hr; }
    
    // Macro that calls a COM method that returns an HRESULT value.
    #define CHK_HR(stmt)        do { hr=(stmt); if (FAILED(hr)) ; } while(0)
    
    // Macro to verify memory allocation.
    #define CHK_ALLOC(p)        do { if (!(p)) { hr = E_OUTOFMEMORY; ; } } while(0)
    
    // Macro that releases a COM object if not NULL.
    #define SAFE_RELEASE(p)     do { if ((p)) { (p)->Release(); (p) = NULL; } } while(0)
    class AutoVariant : public VARIANT
    {
    public:
    
        AutoVariant()
        {
            VariantInit(this);
        }
    
        ~AutoVariant()
        {
            VariantClear(this);
        }
    
        HRESULT SetBSTRValue(LPCWSTR sourceString)
        {
            VariantClear(this);
            V_VT(this) = VT_BSTR;
            V_BSTR(this) = SysAllocString(sourceString);
            if (!V_BSTR(this))
            {
                return E_OUTOFMEMORY;
            }        
            return S_OK;
        }
    
        void SetObjectValue(IUnknown *sourceObject)
        {
            VariantClear(this);
            V_VT(this) = VT_UNKNOWN;
            V_UNKNOWN(this) = sourceObject;
            if (V_UNKNOWN(this))
            {
                V_UNKNOWN(this)->AddRef();
            }
        }
    };
    
    class BingHttpRequest
    {
    public:
    
        BingHttpRequest(){}
    
        void Complete()
        {  
            HRESULT hr = CoInitializeEx(NULL,COINIT_APARTMENTTHREADED); 
            m_buffer.Append(m_tempBuffer);
            loadDOM(m_buffer);
            CoUninitialize();
        }
    
        void DoRequestSync(LPWSTR request,IWebBrowser* pWebBrowser)
        {
            m_WebBrowser = pWebBrowser;
            DWORD err =0;
            DWORD dwSize = 0;
            DWORD dwDownloaded = 0;
            LPSTR pszOutBuffer;
            HINTERNET hSession = ::WinHttpOpen(0,
                WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                WINHTTP_NO_PROXY_NAME,
                WINHTTP_NO_PROXY_BYPASS,
                0);
            HINTERNET hConnection  = ::WinHttpConnect(hSession,               
                L"api.bing.net",
                INTERNET_DEFAULT_PORT,
                0);
            CString lpstrRequest =  L"xml.aspx?Sources=
                web&AppID=70FA6D77B407BA830359D291DD4531800EA4DA38";
            lpstrRequest += L"&query=";
            lpstrRequest += request;
            HINTERNET hRequest = ::WinHttpOpenRequest(hConnection,
                L"GET",
                (lpstrRequest.GetString()),
                0, // use HTTP version 1.1
                WINHTTP_NO_REFERER,
                WINHTTP_DEFAULT_ACCEPT_TYPES,
                0); // flags
            if (!::WinHttpSendRequest(hRequest,
                WINHTTP_NO_ADDITIONAL_HEADERS,
                0, // headers length
                WINHTTP_NO_REQUEST_DATA,
                0, // request data length
                0, // total length
                (DWORD_PTR)this)) // context
            {
                err = GetLastError();
            }
            else
            { 
                bool result = WinHttpReceiveResponse( hRequest, NULL);
                do 
                {
                    // Check for available data.
                    dwSize = 0;
                    if (!WinHttpQueryDataAvailable( hRequest, &dwSize))
                        printf( "Error %u in WinHttpQueryDataAvailable.\n",
                            GetLastError());
                        // Allocate space for the buffer.
                        pszOutBuffer = new char[dwSize+1];
                    if (!pszOutBuffer)
                    {
                        printf("Out of memory\n");
                        dwSize=0;
                    }
                    else
                    {
                        // Read the Data.
                        ZeroMemory(pszOutBuffer, dwSize+1);
                        if (!WinHttpReadData( hRequest, (LPVOID)pszOutBuffer, 
                            dwSize, &dwDownloaded))
                            printf("Error %u in WinHttpReadData.\n", 
                                GetLastError());
                        else
                        {
                            m_buffer.Append(pszOutBuffer);
                            printf("%s", pszOutBuffer);
                        }          
                        // Free the memory allocated to the buffer.
                        delete [] pszOutBuffer;
                    }
                } while (dwSize > 0);
                OutputDebugStringW(m_buffer);
                loadDOM(m_buffer);
            }
            WinHttpCloseHandle(hSession);
        }
    
        CComPtr<IWebBrowser> m_WebBrowser;
    
        HRESULT VariantFromString(PCWSTR wszValue, VARIANT &Variant)
        {
            HRESULT hr = S_OK;
            BSTR bstr = SysAllocString(wszValue);
            CHK_ALLOC(bstr);    
            V_VT(&Variant) = VT_BSTR;
            V_BSTR(&Variant) = bstr;
            return hr;
        }
    
        // Helper function to create a DOM instance. 
        HRESULT CreateAndInitDOM(IXMLDOMDocument2 **ppDoc)
        {
            HRESULT hr = CoCreateInstance(CLSID_DOMDocument, 
                NULL,     
                CLSCTX_INPROC_SERVER,  
                IID_IXMLDOMDocument2,
                (void**)ppDoc);
    
            return hr;
        }
    
        void loadDOM(BSTR xml)
        {
            OutputDebugStringW(xml);
            HRESULT hr = S_OK;
            IXMLDOMDocument2 *pXMLDom=NULL;
            IXMLDOMParseError *pXMLErr = NULL;
            BSTR bstrXML = NULL;
            BSTR bstrErr = NULL;
            VARIANT_BOOL varStatus;
            WCHAR ns[]= L"'xmlns:web=
                'https://schemas.microsoft.com/LiveSearch/2008/04/XML/web' " 
                L"'xmlns:search=
                'https://schemas.microsoft.com/LiveSearch/2008/04/XML/element' ";
            AutoVariant v;
            v.SetBSTRValue(L"xmlns:search=\
                "https://schemas.microsoft.com/LiveSearch/2008/04/XML/element\"
                ");
            CHK_HR(CreateAndInitDOM(&pXMLDom));    
            CHK_HR(pXMLDom->loadXML(xml, &varStatus));
            if (varStatus == VARIANT_TRUE)
            {
                CHK_HR(pXMLDom->get_xml(&bstrXML));
            }
            else
            {
                CHK_HR(pXMLDom->get_parseError(&pXMLErr));
                CHK_HR(pXMLErr->get_reason(&bstrErr));
                printf("Failed to load DOM from BING. %S\n", bstrErr);
            }
            CComPtr<IXMLDOMNodeList> resultNodes;
            pXMLDom->setProperty (L"SelectionNamespaces", 
            CComVariant(L"xmlns:search=\
                "https://schemas.microsoft.com/LiveSearch/2008/04/XML/element\"
                xmlns:web=\
                "https://schemas.microsoft.com/LiveSearch/2008/04/XML/web\""));
            pXMLDom->setProperty(L"SelectionLanguage", CComVariant("XPath"));
            CComPtr<IXMLDOMElement> doc;
            CHK_HR(pXMLDom->get_documentElement(&doc));
            hr =  doc->selectNodes(CComBSTR(L
                "//search:SearchResponse/web:Web/web:Results/web:WebResult"),
                &resultNodes);
            long length;
            if(resultNodes!=NULL)
            {
                resultNodes->get_length(&length);
                printf("Results node count %d",length);
                if(m_WebBrowser)
                {
                    CComBSTR html("<html><body><ul>");
                    CComPtr<IXMLDOMNode> pNode;
                    CComPtr<IXMLDOMNode> pTitleNode;
                    CComPtr<IXMLDOMNode> pUrlNode;
                    CComBSTR title;
                    CComBSTR url;
                    for(int i=0;i<length;i++)
                    {
                        resultNodes->get_item(i,&pNode);
                        pNode->get_firstChild(&pTitleNode);
                        pNode->selectSingleNode(CComBSTR(L"web:Url"),&pUrlNode);
                        if(pTitleNode!=NULL&&pUrlNode!=NULL)
                        {
                            html.Append("<li><a target=\"_blank\" href=\"");
                            pUrlNode->get_text(&url);
                            html.AppendBSTR(url);
                            html.Append("\">");
                            pTitleNode->get_text(&title);
                            html.AppendBSTR(title);
                            html.Append("</a></li>");
                        }
                        pNode.Release();
                        pUrlNode.Release();
                        pTitleNode.Release();
                    }
                    CComPtr<IDispatch> docDispatch;
                    m_WebBrowser->get_Document(&docDispatch);
                    if(docDispatch==NULL)
                    {
                        VARIANT vDummy;
                        vDummy.vt=VT_EMPTY;
                        m_WebBrowser->Navigate(L"about:blank",
                            &vDummy,&vDummy,&vDummy,&vDummy);
                        m_WebBrowser->get_Document(&docDispatch);
                        if(docDispatch!=NULL)
                        {
                            CComPtr<IHTMLDocument2> doc;
                            hr = docDispatch.QueryInterface
                                <IHTMLDocument2>(&doc);
                            if(hr==S_OK)
                            {
                                // Creates a new one-dimensional array.
                                SAFEARRAY *psaStrings =
                                    SafeArrayCreateVector(VT_VARIANT, 0, 1);
                                VARIANT *param;
                                HRESULT hr = SafeArrayAccessData(psaStrings,
                                    (LPVOID*)&param);
                                param->vt = VT_BSTR;
                                param->bstrVal = html.Detach();
                                hr = SafeArrayUnaccessData(psaStrings);
                                doc->write(psaStrings);
                                SafeArrayDestroy(psaStrings);
                            }
                            else
                            {
                                OutputDebugString(L
                                    "QI for IHtmlDocument2 failed");
                            }
                        }
                        else
                            OutputDebugString(L"DOC IS STILL NULL!!!!");
                    }
                }
                else
                {
                    printf("No nodes found");
                }
            }
        }
    
        LPSTR m_tempBuffer;
        CComBSTR m_buffer;
    };
    
    /*!-----------------------------------------------------------------------
        FormRegionWrapper implementation
    -----------------------------------------------------------------------!*/
    
    _ATL_FUNC_INFO FormRegionWrapper::VoidFuncInfo = {CC_STDCALL, VT_EMPTY, 0, 0}; 
    
    HRESULT FormRegionWrapper::HrInit(_FormRegion* pFormRegion)
    {
        HRESULT hr = S_OK;
        m_spFormRegion = pFormRegion;
        FormRegionEventSink::DispEventAdvise(m_spFormRegion);
        CComPtr<IDispatch> spDispatch;
        ReturnOnFailureHr(pFormRegion->get_Form(&spDispatch));
        CComPtr<Forms::_UserForm> spForm;
        ReturnOnFailureHr(spDispatch->QueryInterface(&spForm));
        CComPtr<Forms::Controls> spControls;
        ReturnOnFailureHr(spForm->get_Controls(&spControls));
        CComPtr<Forms::IControl> spControl;
        CComBSTR bstrWBName(L"_webBrowser");
        spControls->_GetItemByName(bstrWBName.Detach(),&spControl);
        ReturnOnFailureHr(spControl->QueryInterface
            <IWebBrowser>(&m_spWebBrowser));
        spControl.Release();
        CComPtr<IDispatch> spDispItem;
        ReturnOnFailureHr(pFormRegion->get_Item(&spDispItem));
        ReturnOnFailureHr(spDispItem->QueryInterface(&m_spMailItem));
        return hr;
    }
    
    void FormRegionWrapper::Show()
    {
        if(m_spFormRegion)
        {
            m_spFormRegion->Select();
        }
    }
    
    void FormRegionWrapper::SearchSelection()
    {
        if (m_spMailItem)
        {
            CComPtr<_Inspector> pInspector;
            m_spMailItem->get_GetInspector(&pInspector);
            CComPtr<IDispatch> pWordDispatch;
            pInspector->get_WordEditor(&pWordDispatch);
            CComQIPtr<_Document> pWordDoc(pWordDispatch);
            CComPtr<Word::_Application> pWordApp;
            pWordDoc->get_Application(&pWordApp);
            CComPtr<Word::Selection> pSelection;
            pWordApp->get_Selection(&pSelection);
            if(pSelection)
            {
                CComBSTR text;
                pSelection->get_Text(&text);
                Search(text);
            }
        }
    }
    
    void FormRegionWrapper::Search(BSTR term)
    {
        BingHttpRequest* r = new BingHttpRequest();
        r->DoRequestSync(term,m_spWebBrowser);
    }
    
    void FormRegionWrapper::OnFormRegionClose()
    {
        if (m_spMailItem)
        {
            m_spMailItem.Release();
        }
        if (m_spFormRegion)
        {
            FormRegionEventSink::DispEventUnadvise(m_spFormRegion);
            m_spFormRegion.Release();
        }
    }
    
  2. 现在可以调试或启动 Outlook。打开一封新邮件。键入一些文本,选择该文本,然后单击"Search Bing"按钮。您看到的内容应该如图 8 所示。

    图 8. 集成了 Bing 搜索的加载项

    集成了 Bing 搜索的加载项

结论

您已经了解了如何使用 C++ 生成实现 _IDTExtensibility2 接口的 Outlook 加载项。_IDTExtensibility2 以及特定于 Outlook 的注册表项是将加载项加载到 Outlook 环境中的关键。

您还使用两个常见的扩展机制为加载项添加了一些自定义 Outlook UI 的功能:自定义功能区和窗体区域。通过自定义功能区,您可以向功能区添加 UI(如按钮)并允许加载项对基于上下文的 UI 事件做出响应。通过自定义窗体区域,您可以向 Outlook 窗体添加 UI 元素(如 WebBrowser 控件)。

此时,您拥有了自定义 Outlook UI 的加载项的基础结构。通过此基础结构,您可以进一步扩展加载项(例如使用 Bing 搜索功能)以满足您的需要。

其他资源

有关 Outlook 窗体区域的详细信息,请参阅以下资源:

有关自定义 Office Fluent 功能区的详细信息,请参阅以下资源: