C++开发Office插件:实现Word插件

#简介

本文以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接口
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值