本系列描述的是如何使用C++/COM来编写PowerPoint插件,使用的开发工具是 Visual Studio 2017。

Step 1:清理 pch.h

pch.h是预编译头文件,如果你在更老的Visual Studio版本上进行开发,这个文件可能是StdAfx.h

  1. 删除pch.h中原来的import语句,原来的import语句长这样

    1
    #import "C:\Program Files (x86)\Common Files\Designer\MSADDNDR.DLL" raw_interfaces_only, raw_native_types, no_namespace, named_guids, auto_search
  2. 加入下面的代码

    1
    2
    3
    4
    5
    6
    7
    8
    #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;

Step 2:修改 Connect.h,实现IRibbonExtensibility接口

  1. 我们添加一些typedef来简化代码的编写,再加上IRibbonExtensibility接口的实现

    大概长这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    typedef IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &__uuidof(__AddInDesignerObjects), /* wMajor = */ 1>
    IDTImpl;

    typedef IDispatchImpl<IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &__uuidof(__Office), /* wMajor = */ 2, /* wMinor = */ 5>
    RibbonImpl;

    // CConnect

    class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_NativePPTAddinLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDTImpl,
    public RibbonImpl
  2. 添加下面的代码到 ATL COM MAP

    1
    COM_INTERFACE_ENTRY(IRibbonExtensibility)
  3. 实现IRibbonExtensibility接口

    1
    2
    3
    4
    5
    6
    7
    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml)
    {
    if(!RibbonXml)
    return E_POINTER;
    *RibbonXml = CComBSTR("XML GOES HERE");
    return S_OK;
    }

    这个接口的输出参数RibbonXml是用来定义功能区Tab页的UI,格式是XML。

Step 3:添加XML到工程

  1. 右键NativePPTAddin工程,添加->新建项,选择XML

  2. 打开刚刚添加的RibbonManifest.xml,输入以下内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?xml version="1.0" encoding="gb2312"?>
    <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
    <ribbon>
    <tabs>
    <tab id="NativePPTAddinTab" label="Native测试">
    <group id="userGroup" label="用户">
    <button id="loginButton" screentip="登录" label="登录" size="large" imageMso="WebPagePreview" onAction="ButtonClicked" />
    </group>
    <group id="actionGroup" label="操作">
    <button id="uploadButton" screentip="上传" label="上传" size="large" imageMso="WebPagePreview" onAction="ButtonClicked" />
    </group>
    </tab>
    </tabs>
    </ribbon>
    </customUI>

    在此XML中,我们定义了两个组(用户和操作),在每个组中都有一个按钮。

Step 4:实现GetCustomUI接口

  1. 在实现之前,我们先将Connect.h中函数定义的位置转移到Connect.cpp。(光标移到函数名,Alt+Enter -> 移动定义位置)

  2. 切换到资源视图,展开NativePPTAddin节点,在NativePPTAddin.rc节点右键,添加资源

  3. 在弹出的对话框中选择导入

  4. 在文件选择对话框中,将文件类型改为所有文件。然后选择刚刚创建的RibbonManifest.xml

  5. 此时会弹出自定义资源类型对话框,输入XML

  6. 添加下面的代码到Connect.cpp用来处理XML文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    namespace {
    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;
    }
    } // End of anonymous namespace
  7. 修改GetCustomUI函数的实现

    1
    2
    3
    4
    5
    6
    7
    STDMETHODIMP_(HRESULT __stdcall) CConnect::GetCustomUI(BSTR RibbonID, BSTR * RibbonXml)
    {
    if (!RibbonXml)
    return E_POINTER;
    *RibbonXml = GetXMLResource(IDR_XML1);
    return S_OK;
    }
  8. 验证一下。启动调试我们将看到功能区有我们新加的Tab页。

Step 5:实现按钮点击事件

  1. 打开 NativePPTAddin.idl 文件

    IDL是用来描述软件组件接口的一种计算机语言。IDL通过一种独立于编程语言的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Java写成。

  2. 添加接口。代码大概长这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [
    object,
    uuid(CE895442-9981-4315-AA85-4B9A5C7739D8),
    dual,
    nonextensible,
    helpstring("IRibbonCallback Interface"),
    pointer_default(unique)
    ]
    interface IRibbonCallback : IDispatch {
    [id(0x4000), helpstring("Button Callback")] HRESULT ButtonClicked([in]IDispatch* pControl);
    };
  1. 修改NativePPTAddin.idl中Connect的定义为

    1
    2
    3
    4
    5
    coclass Connect
    {
    interface IConnect;
    [default] interface IRibbonCallback;
    };
  2. 实现IRibbonCallback接口

    在Connect.h中添加如下typedef

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

    同时修改Connect类的继承关系

    1
    2
    3
    4
    5
    6
    7
    class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_NativePPTAddinLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDTImpl,
    public RibbonImpl,
    public CallbackImpl

    添加接口到ATL COM MAP

    1
    2
    3
    4
    5
    6
    7
    8
    BEGIN_COM_MAP(CConnect)
    COM_INTERFACE_ENTRY2(IDispatch, IRibbonCallback)
    COM_INTERFACE_ENTRY(IConnect)
    COM_INTERFACE_ENTRY2(IDispatch, _IDTExtensibility2)
    COM_INTERFACE_ENTRY(_IDTExtensibility2)
    COM_INTERFACE_ENTRY(IRibbonExtensibility)
    COM_INTERFACE_ENTRY(IRibbonCallback)
    END_COM_MAP()
  3. 实现ButtonClicked接口

    1
    2
    3
    4
    5
    STDMETHODIMP_(HRESULT __stdcall) CConnect::ButtonClicked(IDispatch * ribbon)
    {
    MessageBoxW(NULL, L"Button Clicked!", L"NativePPTAddin", MB_OK);
    return S_OK;
    }
  4. 验证一下。启动调试后我们点击按钮,将会弹出消息框。

  5. 修改一下ButtonClicked函数,实现不同按钮点击弹出不同的消息框。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    STDMETHODIMP_(HRESULT __stdcall) CConnect::ButtonClicked(IDispatch * control)
    {
    CComQIPtr<IRibbonControl> ribbonCtl(control);
    CComBSTR idStr;
    WCHAR msg[64];
    if (ribbonCtl->get_Id(&idStr) != S_OK)
    return S_FALSE;
    if (idStr == OLESTR("loginButton")) {
    swprintf_s(msg, L"I am loginButton");
    } else if (idStr == OLESTR("uploadButton")) {
    swprintf_s(msg, L"I am uploadButton");
    }
    MessageBoxW(NULL, msg, L"NativePPTAddin", MB_OK);
    return S_OK;
    }

下一篇,我们将描述如何使用其他钩子来实现按钮的可见性、图片、显示文字等功能。

完整的代码在这里

Reference