#简介
本文以Word插件为例,介绍如何使用C++实现一个完整的Office插件。简单来说Office插件就是一个实现了IDTExtensibility2和IRibbonExtensibility两个接口的COM组件。IDTExtensibility2提供了Office插件接口,IRibbonExtensibility提供了Ribbon界面接口。下面详细介绍实现步骤。
本例的开发环境为:Win8 + VS2013
本例工程的 gitee 地址:C++实现Office插件
#实现COM组件
首先Word插件是一个COM组件,因此第一步我们要实现一个COM组件。
新建一个Win32项目,命名为MyWordAddin,点击确定。如下图:
在应用程序设置页面,应用程序类型选择DLL,勾选空项目,点击完成。如下图所示:
这样一来工程就建立好了,下面开始实现一个标准的COM组件。
首先建立一个接口描述文件idl。在文件名称上右键,添加,新建项,选择Midl文件,命名为MyWordAddin.idl,点击添加。如下图所示:
在MyWordAddin.idl定义一个IConnect接口,继承自IUnknown。代码如下:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(8789AA77-C75C-474C-8AB0-0CD53E295948),
pointer_default(unique)
]
interface IConnect : IUnknown
{
};
[
uuid(9CEA336F-7263-4844-BFA2-E6AA3CD316DC),
version(1.0)
]
library MyWordAddinLib
{
importlib("stdole32.tlb");
[
uuid(91A7F0E2-1235-45FE-AEB1-F3C5D0C20DB1)
]
coclass MyWordAddin
{
[default] interface IConnect;
};
};
其中,uuid可以用工具->创建GUID来生成。修改MyWordAddin.idl属性,将头文件改成IFact.h,将IID文件改成IFace.c。如下图所示:
编译MyWordAddin.idl,会在项目目录生成IFace.h和IFace.c,这两个文件提供了我们需要的接口定义,以及接口、组件和类型库的uuid。另外可以在项目目录下的Debug子目录中找到类型库文件MyWordAddin.tlb。
工程名称右键,添加,现有项,选择IFace.h和IFace.c,将这两个文件添加到工程中。
为了发布类型库方便,我们将类型库文件作为DLL的资源发布,这样在发布时就不用单独提供tlb文件了。工程名称右键,添加,资源,导入,找到MyWordAddin.tlb,资源类型填写TYPELIB(资源类型必须为TYPELIB,否则加载类型库时会失败),确定。如下图所示:
有了以上的准备工作,就可以开始实现组件类了。新建组件头文件MyWordAddin.h。内容如下:
#include <objbase.h>
#include "IFace.h"
class MyWordAddin : public IConnect
{
public:
static ULONG s_instanceCount; // 组件的个数
// 构造、析构
public:
MyWordAddin();
virtual ~MyWordAddin();
// IUnknown 实现
public:
STDMETHOD(QueryInterface)(REFIID riid, void **ppv) override;
STDMETHOD_(ULONG, AddRef)() override;
STDMETHOD_(ULONG, Release)() override;
private:
ULONG m_ref = 0UL; // 引用计数
};
新建组件实现文件MyWordAddin.cpp,内容如下:
#include "MyWordAddin.h"
ULONG MyWordAddin::s_instanceCount = 0UL;
MyWordAddin::MyWordAddin()
{
InterlockedIncrement(&s_instanceCount); // 增加类实例个数
}
MyWordAddin::~MyWordAddin()
{
InterlockedDecrement(&s_instanceCount); // 减少类实例个数
}
STDMETHODIMP MyWordAddin::QueryInterface(REFIID riid, void **ppvObject)
{
//
// 根据不同的riid获取不同的接口指针并增加引用计数
//
if (riid == IID_IUnknown)
{
IUnknown *pIUnknown = static_cast<IUnknown*>(this);
*ppvObject = pIUnknown;
pIUnknown->AddRef();
return S_OK;
}
else if (riid == IID_IConnect)
{
IConnect *pIConnect = static_cast<IConnect*>(this);
*ppvObject = pIConnect;
pIConnect->AddRef();
return S_OK;
}
else
{
*ppvObject = nullptr;
return E_NOINTERFACE;
}
}
STDMETHODIMP_(ULONG) MyWordAddin::AddRef()
{
InterlockedIncrement(&m_ref); // 增加引用计数
return m_ref;
}
STDMETHODIMP_(ULONG) MyWordAddin::Release()
{
InterlockedDecrement(&m_ref); // 减少引用计数
if (m_ref == 0UL) // 如果引用计数为0
{
delete this; // 销毁组件对象
return 0;
}
return m_ref;
}
到此组件类就实现完了,下面开始实现工厂类,新建工厂类头文件Factory.h,内容如下:
#include <objbase.h>
// COM规定工厂类必须实现IClassFactory
class Factory : public IClassFactory
{
public:
static ULONG s_lockCount; // 加锁次数
// 构造、析构
public:
Factory();
virtual ~Factory();
// IUnknown 实现
public:
STDMETHOD(QueryInterface)(REFIID riid, void **ppvObject) override;
STDMETHOD_(ULONG, AddRef)() override;
STDMETHOD_(ULONG, Release)() override;
// IClassFactory 实现
public:
STDMETHOD(CreateInstance)(IUnknown *pUnkOuter, REFIID riid, void **ppvObject) override;
STDMETHOD(LockServer)(BOOL fLock) override;
private:
ULONG m_ref = 0UL; // 引用计数
};
新建工厂类实现文件Factory.cpp,内容如下:
#include "Factory.h"
#include "MyWordAddin.h"
ULONG Factory::s_lockCount = 0UL;
Factory::Factory()
{
}
Factory::~Factory()
{
}
STDMETHODIMP Factory::QueryInterface(REFIID riid, void **ppvObject)
{
//
// 根据不同的riid获取不同的接口指针并增加引用计数
//
if (riid == IID_IUnknown)
{
IUnknown *pIUnknown = static_cast<IUnknown*>(this);
*ppvObject = pIUnknown;
pIUnknown->AddRef();
return S_OK;
}
else if (riid == IID_IClassFactory)
{
IClassFactory *pIClassFactory = static_cast<IClassFactory*>(this);
*ppvObject = pIClassFactory;
pIClassFactory->AddRef();
return S_OK;
}
else
{
*ppvObject = nullptr;
return E_NOINTERFACE;
}
}
STDMETHODIMP_(ULONG) Factory::AddRef()
{
InterlockedIncrement(&m_ref); // 增加引用计数
return m_ref;
}
STDMETHODIMP_(ULONG) Factory::Release()
{
InterlockedDecrement(&m_ref); // 减少引用计数
if (m_ref == 0UL) // 如果引用计数为0
{
delete this; // 销毁工厂对象
return 0;
}
return m_ref;
}
STDMETHODIMP Factory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject)
{
// 不支持聚合
if (pUnkOuter != nullptr)
{
return CLASS_E_NOAGGREGATION;
}
MyWordAddin *addin = new MyWordAddin(); // 创建组件
// 获取所请求的接口,这里先AddRef()再Release()的目的
// 是当QueryInterface失败时,自动销毁组件对象
addin->AddRef();
HRESULT hr = addin->QueryInterface(riid, ppvObject);
addin->Release();
return hr;
}
STDMETHODIMP Factory::LockServer(BOOL fLock)
{
if (fLock)
{
InterlockedIncrement(&s_lockCount); // 增加锁定计数
}
else
{
InterlockedDecrement(&s_lockCount); // 减少锁定计数
}
return S_OK;
}
新建dllexport.cpp,实现DLL导出函数,内容如下:
#include <objbase.h>
#include <string>
#include "MyWordAddin.h"
#include "Factory.h"
#include "IFace.h"
#include <memory>
HMODULE g_hModule = NULL; // DLL句柄
static const std::wstring ProgID = L"MyWordAddin.Component.1";
static const std::wstring VersionIndependentProgID = L"MyWordAddin.Component";
static const std::wstring FriendlyName = L"My Word Addin";
// 辅助函数,将键值写入注册表
static LONG SetKeyValue(HKEY hKey, const std::wstring &subKey, const std::wstring &valueName, const std::wstring &value);
// 辅助函数,获取GUID字符串
static std::wstring GetGUIDString(REFGUID refGUID);
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
// 在DLL加载时,获取句柄
if (fdwReason == DLL_PROCESS_ATTACH)
{
g_hModule = hinstDLL;
}
return TRUE;
}
STDAPI DllCanUnloadNow()
{
// 如果组件的个数为0,并且类厂的锁定计数为0,则可以卸载该DLL
if (MyWordAddin::s_instanceCount == 0 && Factory::s_lockCount == 0)
{
return S_OK;
}
else
{
return S_FALSE;
}
}
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv)
{
if (rclsid != CLSID_MyWordAddin)
{
return CLASS_E_CLASSNOTAVAILABLE;
}
Factory *factory = new Factory(); // 创建工厂对象
// 获取所请求的接口,这里先AddRef()再Release()的目的
// 是当QueryInterface失败时,自动销毁工厂对象
factory->AddRef();
HRESULT hr = factory->QueryInterface(riid, ppv);
factory->Release();
return hr;
}
STDAPI DllRegisterServer()
{
// HKEY_CLASSES_ROOT
// |-CLSID
// | |-{91A7F0E2-1235-45FE-AEB1-F3C5D0C20DB1} - My Word Addin
// | |-InprocServer32 - G:\projects\vs\MyWordAddin\Debug\MyWordAddin.dll
// | |-ProgID - MyWordAddin.Component.1
// | |-VersionIndependentProgID - MyWordAddin.Component
// | |-TypeLib - {9CEA336F-7263-4844-BFA2-E6AA3CD316DC}
// |-MyWordAddin.Component - My Word Addin
// | |-CLSID - {91A7F0E2-1235-45FE-AEB1-F3C5D0C20DB1}
// | |-CurVer - MyWordAddin.Component.1
// |-MyWordAddin.Component.1 - My Word Addin
// |-CLSID - {91A7F0E2-1235-45FE-AEB1-F3C5D0C20DB1}
// 将组件ID作为 HKEY_CLASSES_ROOT\CLSID 的子健写入注册表
const std::wstring clsid = GetGUIDString(CLSID_MyWordAddin);
std::wstring key = L"CLSID\\" + clsid;
SetKeyValue(HKEY_CLASSES_ROOT, key, L"", FriendlyName);
// 将 InprocServer32 作为组件ID的子健写入注册表
std::unique_ptr<wchar_t[]> filename(new wchar_t[MAX_PATH + 1]());
GetModuleFileName(g_hModule, filename.get(), MAX_PATH);
*(filename.get() + MAX_PATH) = L'\0';
SetKeyValue(HKEY_CLASSES_ROOT, key + L"\\InprocServer32", L"", filename.get());
// 将 ProgID 作为组件ID的子健写入注册表
SetKeyValue(HKEY_CLASSES_ROOT, key + L"\\ProgID", L"", ProgID);
// 将 VersionIndependentProgID 作为组件ID的子健写入注册表
SetKeyValue(HKEY_CLASSES_ROOT, key + L"\\VersionIndependentProgID", L"", VersionIndependentProgID);
// 将ProgID作为 HKEY_CLASSES_ROOT 的子健写入注册表
SetKeyValue(HKEY_CLASSES_ROOT, ProgID, L"", FriendlyName);
SetKeyValue(HKEY_CLASSES_ROOT, ProgID + L"\\CLSID", L"", clsid);
// 将版本无关的 ProgID 作为 HKEY_CLASSES_ROOT 的子健写入注册表
SetKeyValue(HKEY_CLASSES_ROOT, VersionIndependentProgID, L"", FriendlyName);
SetKeyValue(HKEY_CLASSES_ROOT, VersionIndependentProgID + L"\\CLSID", L"", clsid);
SetKeyValue(HKEY_CLASSES_ROOT, VersionIndependentProgID + L"\\CurVer", L"", ProgID);
return S_OK;
}
STDAPI DllUnregisterServer()
{
// 删除 HKEY_CLASSES_ROOT\CLSID 下的组件ID
std::wstring subKey = L"CLSID\\" + GetGUIDString(CLSID_MyWordAddin);
RegDeleteTree(HKEY_CLASSES_ROOT, subKey.c_str());
// 删除 HKEY_CLASSES_ROOT 下的 ProgID
RegDeleteTree(HKEY_CLASSES_ROOT, ProgID.c_str());
// 删除 HKEY_CLASSES_ROOT 下的版本无关的 ProgID
RegDeleteTree(HKEY_CLASSES_ROOT, VersionIndependentProgID.c_str());
return S_OK;
}
LONG SetKeyValue(HKEY hKey, const std::wstring &subKey, const std::wstring &valueName, const std::wstring &value)
{
HKEY hSubKey = NULL;
LONG res = RegCreateKeyEx(hKey, subKey.c_str(), 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hSubKey, NULL);
if (res != ERROR_SUCCESS)
{
return res;
}
return RegSetValueEx(hSubKey, valueName.c_str(), 0, REG_SZ,
reinterpret_cast<const BYTE*>(value.c_str()),
(value.length() + 1) * sizeof(std::wstring::value_type));
}
std::wstring GetGUIDString(REFGUID refGUID)
{
LPOLESTR lpGUID = nullptr;
StringFromCLSID(refGUID, &lpGUID);
std::wstring strGUID(lpGUID);
CoTaskMemFree(lpGUID);
return strGUID;
}
新建模块定义文件MyWordAddin.def,内容如下:
LIBRARY MyWordAddin.dll
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
编译工程,生成MyWordAddin.dll。
至此一个标准的COM组件就完成了,但它还不是一个Office插件。为了让这个COM组件成为Office插件,还需要实现开篇提到的那两个接口:IDTExtensibility2和IRibbonExtensibility
#实现 IDTExtensibility2
为了实现 IDTExtensibility2 接口,首先需要找到该接口的定义,该接口定义在与MSADDNDR.DLL绑定的类型库中,该类型库的LIBID为{AC0714F2-3D04-11D1-AE7D-00A0C90F26F4}。因此,我们就可以在MyWordAddin.h中导入该类型库:
#import "libid:AC0714F2-3D04-11D1-AE7D-00A0C90F26F4" auto_rename auto_search raw_interfaces_only rename_namespace("ADO")
其中,auto_name表示如果类型库中有以C++的关键字命名的类型,则自动在其名称前加两个下划线。auto_search表示如果类型库中引入了其他的类型库,则也将其他的类型库包含进来。raw_interface_only表示不产生带有错误处理的包装函数,仅用原始的API。rename_namespace用来重命名名称空间。
IDTExtensibility2接口定义如下:
struct __declspec(uuid("b65ad801-abaf-11d0-bb8b-00a0c90f2744"))
_IDTExtensibility2 : IDispatch
{
virtual HRESULT __stdcall OnConnection (
/*[in]*/ IDispatch * Application,
/*[in]*/ enum ext_ConnectMode ConnectMode,
/*[in]*/ IDispatch * AddInInst,
/*[in]*/ SAFEARRAY * * custom ) = 0;
virtual HRESULT __stdcall OnDisconnection (
/*[in]*/ enum ext_DisconnectMode RemoveMode,
/*[in]*/ SAFEARRAY * * custom ) = 0;
virtual HRESULT __stdcall OnAddInsUpdate (
/*[in]*/ SAFEARRAY * * custom ) = 0;
virtual HRESULT __stdcall OnStartupComplete (
/*[in]*/ SAFEARRAY * * custom ) = 0;
virtual HRESULT __stdcall OnBeginShutdown (
/*[in]*/ SAFEARRAY * * custom ) = 0;
};
该接口最常用的是OnConnection与OnDisconnection两个函数,这两个函数在插件被加载和卸载时调用。另外需要注意的是_IDTExtensibility2继承自IDispatch,因此我们需要自己实现IDispatch接口。
修改MyWordAddin.h以继承该接口,修改的内容如下:
...
#import "libid:AC0714F2-3D04-11D1-AE7D-00A0C90F26F4" auto_rename auto_search raw_interfaces_only rename_namespace("ADO")
// Office 插件必须实现 ADO::_IDTExtensibility2
class MyWordAddin : public ADO::_IDTExtensibility2, public IConnect
{
...
public:
...
// 初始化函数,获取ITypeInfo接口,在需要时注册类型库到注册表,见实现
STDMETHOD(Init)();
...
// IDispatch 实现
public:
STDMETHOD(GetTypeInfoCount)(UINT *pctinfo) override;
STDMETHOD(GetTypeInfo)(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) override;
STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) override;
STDMETHOD(Invoke)(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) override;
// _IDTExtensibility2 实现
public:
STDMETHOD(OnConnection)(IDispatch *Application, ADO::ext_ConnectMode ConnectMode, IDispatch * AddInInst, SAFEARRAY **custom) override;
STDMETHOD(OnDisconnection)(ADO::ext_DisconnectMode RemoveMode, SAFEARRAY **custom) override;
STDMETHOD(OnAddInsUpdate)(SAFEARRAY **custom) override;
STDMETHOD(OnStartupComplete)(SAFEARRAY **custom) override;
STDMETHOD(OnBeginShutdown)(SAFEARRAY **custom) override;
private:
...
ITypeInfo *m_pITypeInfo = nullptr; // 类型信息,用来实现 IDispatch
};
注意这里还增加了一个函数Init,该函数用来获取类型信息并在需要时注册类型库到注册表。
修改MyWordAddin.cpp以实现该接口,修改的内容如下:
...
STDMETHODIMP MyWordAddin::QueryInterface(REFIID riid, void **ppvObject)
{
//
// 根据不同的riid获取不同的接口指针并增加引用计数
//
if (riid == IID_IUnknown)
{
// 由于两个基类都继承自IUnknown,因此有两个IUnknown指针,
// 所以这里先转换成IConnect基类指针,再转换为IUnknown指针
IUnknown *pIUnknown = static_cast<IUnknown*>(static_cast<IConnect*>(this));
*ppvObject = pIUnknown;
pIUnknown->AddRef();
return S_OK;
}
else if (riid == IID_IDispatch)
{
IDispatch *pIDispatch = static_cast<IDispatch*>(this);
*ppvObject = pIDispatch;
pIDispatch->AddRef();
return S_OK;
}
else if (riid == __uuidof(ADO::_IDTExtensibility2))
{
ADO::_IDTExtensibility2 *pIDTExt2 = static_cast<ADO::_IDTExtensibility2*>(this);
*ppvObject = pIDTExt2;
pIDTExt2->AddRef();
return S_OK;
}
else if (riid == IID_IConnect)
{
IConnect *pIConnect = static_cast<IConnect*>(this);
*ppvObject = pIConnect;
pIConnect->AddRef();
return S_OK;
}
else
{
*ppvObject = nullptr;
return E_NOINTERFACE;
}
}
...
STDMETHODIMP MyWordAddin::Init()
{
if (m_pITypeInfo == nullptr)
{
// 从注册表中读取类型库接口
ITypeLib *pITypeLib = nullptr;
HRESULT hr = LoadRegTypeLib(LIBID_MyWordAddinLib, 1, 0, 0x00, &pITypeLib);
if (FAILED(hr)) // 如果失败,表示类型库没有被注册
{
// 从文件中读取类型库接口
// 读取资源中的类型库时,需要指定的路径为:G:\projects\vs\MyWordAddin\Debug\MyWordAddin.dll\资源ID
extern HMODULE g_hModule;
std::unique_ptr<wchar_t[]> filename(new wchar_t[MAX_PATH + 1]());
GetModuleFileName(g_hModule, filename.get(), MAX_PATH);
*(filename.get() + MAX_PATH) = L'\0';
std::wstringstream oss;
oss << filename.get() << L"\\" << IDR_TYPELIB1 << std::ends;
hr = LoadTypeLib(oss.str().c_str(), &pITypeLib);
if (FAILED(hr))
{
return hr;
}
// 注册类型库到注册表
RegisterTypeLib(pITypeLib, filename.get(), NULL);
}
// 获取类型信息接口
hr = pITypeLib->GetTypeInfoOfGuid(IID_IConnect, &m_pITypeInfo);
pITypeLib->Release();
if (FAILED(hr))
{
return hr;
}
}
return S_OK;
}
STDMETHODIMP MyWordAddin::GetTypeInfoCount(UINT *pctinfo)
{
*pctinfo = 1; // 类型信息的数量为1
return S_OK;
}
STDMETHODIMP MyWordAddin::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
{
*ppTInfo = nullptr;
// 因为只有一个类型信息,因此索引必须为0
if (iTInfo != 0)
{
return DISP_E_BADINDEX;
}
*ppTInfo = m_pITypeInfo;
(*ppTInfo)->AddRef(); // 增加引用计数
return S_OK;
}
STDMETHODIMP MyWordAddin::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
{
if (riid != IID_NULL)
{
return DISP_E_UNKNOWNINTERFACE;
}
// 利用 ITypeInfo 实现 GetIDsOfNames
return m_pITypeInfo->GetIDsOfNames(rgszNames, cNames, rgDispId);
}
STDMETHODIMP MyWordAddin::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
if (riid != IID_NULL)
{
return DISP_E_UNKNOWNINTERFACE;
}
SetErrorInfo(0, NULL);
// 利用 ITypeInfo 实现 Invoke
return m_pITypeInfo->Invoke(static_cast<IDispatch*>(this),
dispIdMember, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
}
STDMETHODIMP MyWordAddin::OnConnection(IDispatch *Application, ADO::ext_ConnectMode ConnectMode, IDispatch * AddInInst, SAFEARRAY **custom)
{
// 在加载成功时弹出消息框
MessageBox(NULL, TEXT("测试Word插件"), TEXT(""), MB_OK | MB_TOPMOST);
return S_OK;
}
STDMETHODIMP MyWordAddin::OnDisconnection(ADO::ext_DisconnectMode RemoveMode, SAFEARRAY **custom)
{
return S_OK;
}
STDMETHODIMP MyWordAddin::OnAddInsUpdate(SAFEARRAY **custom)
{
return S_OK;
}
STDMETHODIMP MyWordAddin::OnStartupComplete(SAFEARRAY **custom)
{
return S_OK;
}
STDMETHODIMP MyWordAddin::OnBeginShutdown(SAFEARRAY **custom)
{
return S_OK;
}
为了完成组件的初始化,需要在工厂类中调用组件的Init()方法,修改的内容如下:
STDMETHODIMP Factory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject)
{
...
MyWordAddin *addin = new MyWordAddin(); // 创建组件对象
HRESULT hr = addin->Init(); // 初始化组件
if (FAILED(hr))
{
return hr;
}
...
}
Office会根据注册表中 HKEY_CURRENT_USER\Software\Microsoft\Office\Word\Addins 下面的子健加载相应的插件。每个子健的名字为插件的版本无关的ProgID,值可以为以下几个值:
- Description REG_SZ类型,插件的描述
- FriendlyName REG_SZ类型,友好的名字,出现在Office插件列表中的名字
- LoadBehavior REG_DWORD类型,表示加载行为,当值为3时,表示启动时加载
我们需要修改dllexport.cpp中的DllRegisterServer()和DllUnregisterServer(),以便在注册表中登记和删除插件信息,另外还需要在DllUnregisterServer()中删除类型库的信息,修改的内容如下:
STDAPI DllRegisterServer()
{
...
//
// 登记 Office 插件信息
//
// HKEY_CURRENT_USER\Software\Microsoft\Office\Word\Addins
// |-MyWordAddin.Component
// Description My Word Addin
// FriendlyName My Word Addin
// LoadBehavior 3
key = L"Software\\Microsoft\\Office\\Word\\Addins\\" + VersionIndependentProgID;
SetKeyValue(HKEY_CURRENT_USER, key, L"Description", FriendlyName);
SetKeyValue(HKEY_CURRENT_USER, key, L"FriendlyName", FriendlyName);
HKEY hSubKey = NULL;
RegCreateKeyEx(HKEY_CURRENT_USER, key.c_str(), 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hSubKey, NULL);
DWORD dwValue = 3;
RegSetValueEx(hSubKey, L"LoadBehavior", 0, REG_DWORD,
reinterpret_cast<const BYTE*>(&dwValue), sizeof (dwValue));
return S_OK;
}
STDAPI DllUnregisterServer()
{
...
// 删除 Office 插件信息
subKey = L"Software\\Microsoft\\Office\\Word\\Addins\\" + VersionIndependentProgID;
RegDeleteTree(HKEY_CURRENT_USER, subKey.c_str());
// 删除类型库信息
subKey = L"TypeLib\\" + GetGUIDString(LIBID_MyWordAddinLib);
RegDeleteTree(HKEY_CLASSES_ROOT, subKey.c_str());
return S_OK;
}
到此一个Word插件就完成了,用管理员启动命令行,切换到MyWordAddin.dll所在目录,执行regsvr32 MyWordAddin.dll以注册组件:
> cd G:\projects\vs\MyWordAddin\Debug
> regsvr32 MyWordAddin.dll
Win+R打开运行,输入 winword 启动Word,这时会弹出提示框,表示Word插件已经加载成功了。
到此一个Word插件就实现完成了,不过要在Ribbon上添加自己的控件,还需要实现另一个接口,IRibbonExtensibility
#实现 IRibbonExtensibility
IRibbonExtensibility定义在Office.dll的类型库中,类型库的ID为 {2DF8D04C-5BFA-101B-BDE5-00AA0044DE52},该接口只有一个函数GetCustomUI,用来加载用户自定义的Ribbon界面:
struct __declspec(uuid("000c0396-0000-0000-c000-000000000046"))
IRibbonExtensibility : IDispatch
{
//
// Raw methods provided by interface
//
virtual HRESULT __stdcall GetCustomUI (
/*[in]*/ BSTR RibbonID,
/*[out,retval]*/ BSTR * RibbonXml ) = 0;
};
修改MyWordAddin.h,修改的内容如下:
...
#import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52" version("2.5") auto_rename auto_search raw_interfaces_only rename_namespace("Office")
// Office 插件必须实现 ADO::_IDTExtensibility2
// 实现 IRibbonExtensibility 以加载自定义Ribbon界面
class MyWordAddin : public ADO::_IDTExtensibility2, public Office::IRibbonExtensibility, public IConnect
{
...
// IRibbonExtensibility 实现
public:
STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR *RibbonXml) override;
...
};
在 MyWordAddin.cpp 中增加函数实现,另外需要修改QueryInterface使其支持新的接口,注意继承多个接口,并且每个接口都有相同基类时,从孙类向爷类转换时应先转换为父类。
STDMETHODIMP MyWordAddin::QueryInterface(REFIID riid, void **ppvObject)
{
//
// 根据不同的riid获取不同的接口指针并增加引用计数
//
if (riid == IID_IUnknown)
{
// 由于多个基类都继承自IUnknown,因此有多个IUnknown指针,
// 所以这里先转换成IConnect基类指针,再转换为IUnknown指针
IUnknown *pIUnknown = static_cast<IUnknown*>(static_cast<IConnect*>(this));
*ppvObject = pIUnknown;
pIUnknown->AddRef();
return S_OK;
}
else if (riid == IID_IDispatch)
{
// 和IUnknown一样,需要先转换为IConnect指针,在转换为IDispatch指针
IDispatch *pIDispatch = static_cast<IDispatch*>(static_cast<IConnect*>(this));
*ppvObject = pIDispatch;
pIDispatch->AddRef();
return S_OK;
}
else if (riid == __uuidof(ADO::_IDTExtensibility2))
{
ADO::_IDTExtensibility2 *pIDTExt2 = static_cast<ADO::_IDTExtensibility2*>(this);
*ppvObject = pIDTExt2;
pIDTExt2->AddRef();
return S_OK;
}
else if (riid == __uuidof(Office::IRibbonExtensibility))
{
Office::IRibbonExtensibility *pIRibbonExt = static_cast<Office::IRibbonExtensibility*>(this);
*ppvObject = pIRibbonExt;
pIRibbonExt->AddRef();
return S_OK;
}
else if (riid == IID_IConnect)
{
IConnect *pIConnect = static_cast<IConnect*>(this);
*ppvObject = pIConnect;
pIConnect->AddRef();
return S_OK;
}
else
{
*ppvObject = nullptr;
return E_NOINTERFACE;
}
}
STDMETHODIMP MyWordAddin::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
...
// 利用 ITypeInfo 实现 Invoke
// 注意这里的指针需要先转换为父类IConnect,在转换为爷类IDispatch
return m_pITypeInfo->Invoke(static_cast<IDispatch*>(static_cast<IConnect*>(this)),
dispIdMember, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
}
STDMETHODIMP MyWordAddin::GetCustomUI(BSTR RibbonID, BSTR *RibbonXml)
{
return S_OK;
}
目前该函数的实现是空的,我们需要将自定义界面的XML字符串赋给RibbonXml作为传出参数供Word加载Ribbon界面。为此我们可以建立一个XML文件,在该函数中读取该文件的内容,并将读到的字符串赋值给RibbonXml。
在工程目录下新建XML文件,命名为ribbon.xml,格式为UTF-8,内容如下:
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
<ribbon>
<tabs>
<tab id="Test" label="Test">
<group id="TestGroup" label="插件测试">
<button id="NewCustomButton"
imageMso="WebPagePreview"
size="large"
label="测试按钮"
onAction="ButtonClicked"/>
</group>
</tab>
</tabs>
</ribbon>
</customUI>
工程名称右键,添加,资源,导入该ribbon.xml,资源类型为XML:
修改GetCustomUI函数,加入读取xml文本的代码:
STDMETHODIMP MyWordAddin::GetCustomUI(BSTR RibbonID, BSTR *RibbonXml)
{
extern HMODULE g_hModule;
HRSRC hRsrc = FindResource(g_hModule, MAKEINTRESOURCE(IDR_XML1), L"XML");
if (hRsrc == NULL)
{
return HRESULT_FROM_WIN32(GetLastError());
}
HGLOBAL hGlobal = LoadResource(g_hModule, hRsrc);
if (hGlobal == NULL)
{
return HRESULT_FROM_WIN32(GetLastError());
}
DWORD size = SizeofResource(g_hModule, hRsrc);
char *data = static_cast<char*>(LockResource(hGlobal));
int wcSize = MultiByteToWideChar(CP_UTF8, 0, data, size, NULL, NULL);
std::unique_ptr<wchar_t[]> wcData(new wchar_t[wcSize + 1]());
MultiByteToWideChar(CP_UTF8, 0, data, size, wcData.get(), wcSize);
*(wcData.get() + wcSize) = L'\0';
*RibbonXml = SysAllocString(wcData.get());
return S_OK;
}
如何处理Ribbon界面的响应呢?Word会根据Ribbon XML中的onAction来通过IDispatch调用相应的函数,比如上面XML中的onAction="ButtonClicked"
表示该按钮按下时,会通过IDispatch来调用ButtonClicked这个函数,因此我们导出的接口必须继承自IDispatch,并且该接口必须定义ButtonClicked这个函数。Office规定这个回调函数的形式必须为 HRESULT ButtonClicked(IDispatch *ribbonControl)
。
当前我们在MyWordAddin.idl中定义的接口为IUnknown接口,并且没有导出任何函数。为了满足以上需求,需要做若干修改,修改后的MyWordAddin.idl如下:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(8789AA77-C75C-474C-8AB0-0CD53E295948),
pointer_default(unique)
]
interface IConnect : IDispatch
{
HRESULT ButtonClicked([in] IDispatch *ribbtonControl);
};
[
uuid(9CEA336F-7263-4844-BFA2-E6AA3CD316DC),
version(1.0)
]
library MyWordAddinLib
{
importlib("stdole32.tlb");
[
uuid(91A7F0E2-1235-45FE-AEB1-F3C5D0C20DB1)
]
coclass MyWordAddin
{
[default] interface IConnect;
};
};
在MyWordAddin.h中添加如下代码:
// IConnect 实现
public:
STDMETHOD(ButtonClicked)(IDispatch *ribbonControl) override;
在MyWordAddin.cpp中添加如下代码:
STDMETHODIMP MyWordAddin::ButtonClicked(IDispatch *ribbonControl)
{
MessageBox(NULL, TEXT("测试按钮被按下"), TEXT(""), MB_OK | MB_TOPMOST);
return S_OK;
}
到此IRibbonExtensibility接口就实现完了,重新编译整个工程。再次通过regsvr32注册组件:
> regsvr32 MyWordAddin.dll
运行Word,会看到我们自定义的Ribbon界面已经出现在导航栏中了,点击按钮,会弹出提示,如下如:
到此一个完整的Word插件就完成了。
#总结
- Word 插件是一个实现了_IDTExtensibility2接口的COM组件
- 若要实现自己的Ribbon界面, 需要实现IRibbonExtensibility接口