【转】【学习笔记】《北塔教你做插件 从RibbonX开始》第三讲:再建Ribbon——ATL的实现方法
原文链接:https://blog.csdn.net/northtower/java/article/details/41316687
本文将介绍如何使用ATL标准模板库实现Office 插件的开发工作,重点为Addin插件连接以及Ribbon菜单载入。
PS:本章示例的实现环境为VS2010。
目录:
实现MSOffice_Addin插件的基础
用ATL创建WordAddin插件
WordAddin插件定义Ribbon菜单
第一节、实现MSOffice_Addin插件的基础
做Office插件,你可曾想过:是什么来维护插件的生命周期?又是什么控制了插件的创建、卸载与更新行为?答案就是IDTExtensibility2 接口!
该接口中定义了五个方法,具体如下:
从实例化插件到被卸载,整个生命流程如图所示。
我们有了实现MSOffice_Addin插件的理论基础,加之上一章中介绍的IRibbonExtensibility接口,自己动手开发插件已水到渠成。
备注:
MSDN中对该接口的定义如下:
http://msdn.microsoft.com/en-us/library/extensibility.idtextensibility2.aspx
IDTExtensibility2是个接口,控制插件连接到宿主程序的生命周期,而宿主程序并不一定为MSOffice!
第二节、用ATL创建WordAddin插件
2.1、创建ATL项目
启动VS,新建ATL工程"ATLAddin",所有设置均保持默认选项,唯一需要注意的就是"Application type"选择DLL。
通过添加类(Add Class),加入ATL简单对象(ATL Simple Object)。[Short Name]键入:“MSConnect”,ProgID定义为"ATLAddin.MSConnect"。
此对象将用于维护所有与MSOffice相关的连接工作。
2.2、引入IDTExtensbility2接口
根据第一节中的理论基础,在ATL对象已经创建完备的前提下,下一步工作就是实现与MSOffice的互连。
在类视图中选择"CMSConnect"节点,右键添加实现接口(Implement Interface)。
在接口向导对话框中,类型库选择引入"Microsoft Add-In Designer <1.0>"的支持,在接口列表框中引入对“_IDTExtensbility2"接口的支持。
对_IDTExtensbility2接口引入成功后,我们查看MSConnect.h文件,CMSConnect类的声明与接口映射均发生了变化,加入了对_IDTExtensibility2及其方法的引用;
public IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &LIBID_AddInDesignerObjects, /* wMajor = */ 1> // _IDTExtensibility2 Methods public: STDMETHOD(OnConnection)(LPDISPATCH Application, ext_ConnectMode ConnectMode, LPDISPATCH AddInInst, SAFEARRAY * * custom) { return E_NOTIMPL; } STDMETHOD(OnDisconnection)(ext_DisconnectMode RemoveMode, SAFEARRAY * * custom) { return E_NOTIMPL; } STDMETHOD(OnAddInsUpdate)(SAFEARRAY * * custom) { return E_NOTIMPL; } STDMETHOD(OnStartupComplete)(SAFEARRAY * * custom) { return E_NOTIMPL; } STDMETHOD(OnBeginShutdown)(SAFEARRAY * * custom) { return E_NOTIMPL; }
五种事件的响应,我们插件中已然实现了,添加断点以直观地了解各个方法的调用顺序,不过在此之前我们还剩最后一步,就是注册插件!
2.3、向MSWord中注册插件
到目前为止,我们用ATL编写的ActiveX样例虽然引入了“_IDTExtensbility2"接口的支持,但宿主程序究竟是谁?我们没有定义。
连接MSWord的方法也很简单,我们打开注册表“HKEY_CURRENT_USER\Software\Microsoft\Office\Word\Addins”。
“Addins”键值下定义了当前系统中,所有已经被注册的插件。MSWord在启动实例化插件时,会根据此键值指向的内容,分别实例化所有插件。
截图中的“WordAddIn1”即为上一章中我们用C#_VSTO编写的工程,该插件的注册行为是在“WordAddIn1.dll.manifest”文件中描述的,内容如下:
<vstov4:appAddIn application="Word" loadBehavior="3" keyName="WordAddIn1">
<vstov4:friendlyName>WordAddIn1</vstov4:friendlyName>
<vstov4:description>WordAddIn1</vstov4:description>
</vstov4:appAddIn>
MSOffice-Addins插件的注册,有最少三项内容是必须存在的:
FriendlyName(String) :插件名称
Description(String) :插件描述
LoadBehavior(DWORD) :插件的加载方式,有以下几种。
扩展:MSOffice中其他应用,如Outlook、Execl和PowerPoint的插件加载方式也与此相同,请自行消化,本章不再作进一步讨论。
注册MSWord插件的理论基础明白以后,写代码就容易多了。
打开“MSConnect.rgs”文件,向文件末尾加入以下代码:
HKCU { NoRemove Software { NoRemove Microsoft { NoRemove Office { NoRemove Word { NoRemove Addins { ATLAddin.MSConnect { val Description = s 'ATLAddin Addin' val FriendlyName = s 'ATLAddin Addin' val LoadBehavior = d 3 } } } } } } }
加入代码以后,编译运行吧。(如果人品不受伤的话,该插件是能通过的!)
控件注册以后,请注意以下两点:
注册表“HKEY_CURRENT_USER\Software\Microsoft\Office\Word\Addins”键值下是否含有“ATLAddin.MSConnect”相应内容?
在MSConnect.h文件的OnConnection方法中加入断点,在已经捕获断点的情况下,为什么该插件在Office整个启动关闭过程中,只进了OnConnection(),其他方法如OnDisconnection、OnStartupComplete全都没执行?
PS:OnConnection() 函数要返回S_OK,否则,插件注册成功,但是Word加载项提示“COM加载项运行错误”
第三节、WordAddin插件定义Ribbon菜单
我们已经用ATL创建并注册了MSWord插件,此时在MSWord的"COM加载项"中我们是能看到自己所编写的"ATLAddin Addin"插件,如果不能看到,请先排查上节中在哪出了问题。如果实在不行,可以从本章的附件里下载样例工程。我已经把第二小节结束时的代码进行了备份上传,可自由选择。
尝试回忆一下”IRibbonExtensibility“接口,对此不太清楚的情况下,可以浏览上一讲中的第二节“Ribbon的酒杯——IRibbonExtensibility”加深一下印象。
传送门:第二讲跳转
3.1 引入IRibbonExtensibility接口
在类视图中选择"CMSConnect"节点,右键添加实现接口(Implement Interface)。
在接口向导对话框中,类型库选择引入"Microsoft Office 14.0 Object Library<2.5>"的支持,在接口列表框中引入对“IRibbonExtensibility"接口的支持。
注意:支持IRibbonExtensibility接口类型库有版本要求,最低版本为Office2007支持的‘Microsoft Office 12.0 Object Library<2.4>”,最新版本Office2013为“Microsoft Office 15.0 Object Library<2.7>” ,切记!!
对IRibbonExtensibility接口的引入成功后,我们查看MSConnect.h文件:
以加入对IRibbonExtensibility及其方法的引用;
public IDispatchImpl<IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &LIBID_Office, /* wMajor = */ 2, /* wMinor = */ 5> // IRibbonExtensibility Methods STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml) { return E_NOTIMPL; }
由于类型库版本的不同,此时编译工程,将会出现一些列编译错误,如图所示:
根据报错信息,我们将stdafx.h文件中导入类型库语句,修改为:
#import "C:\Program Files\Common Files\Microsoft Shared\OFFICE14\MSO.DLL" \ raw_interfaces_only, raw_native_types, no_namespace, named_guids, auto_search,\ exclude( "IAccessible"),exclude("DocumentProperties" )
备注:以上修改信息,要根据你电脑中的具体错误进行相应修改!
3.2 生成Ribbon菜单
基于GetCustomUI()方法,可以读入工程中指定Ribbon资源,使之在MSOffice的功能区中显示。
我们将上一讲“WordAddIn1”工程中的Ribbon.xml文件拷贝到当前工程中的“ATLAddin”目录。在“ATLAddin”工程资源管理器中 Add-〉Existing Item ,将刚才拷贝的Ribbon.xml文件引入当前解决方案。
再通过资源视图(Resource View),导入Ribbon.xml文件,类型定义为“XML”
自此,Ribbon资源已经引入至当前工程,下一步需要的仅仅是读取该文件,交由MSOffice解析并绘制。
完善GetCustomUI()方法中的读取文件行为,将以下内容替换MSConnect.h文件中的GetCustomUI()方法。
// IRibbonExtensibility Methods STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml) { if(!RibbonXml) return E_POINTER; *RibbonXml = GetXMLResource(IDR_XML1); return S_OK; } 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; }
编译运行吧,此节的修改不会引起编译错误。唯一需要注意的地方就是资源文件的声明,如“IDR_XML1”定义是否完整。
注意:
1、字符按编码问题:在Ribbon.xml中如果要定义中文信息,如中文标签名等,字符集一定要设置为 encoding="gb2312"。
2、xmlns命名空间问题:
Office2007版本 :xmlns="http://schemas.microsoft.com/office/2006/01/customui"
Office2010及以上版本:xmlns="http://schemas.microsoft.com/office/2009/07/customui"
3.3 Ribbon菜单回调与响应
由于微软没有明确支持“RibbonCommand”响应的类型库,我们用MFC/ATL编写MSAddin插件时,要费一些精力。
本章onButtonClick响应的方法,是改变IDispatch派发的接口映射,实现Ribbon菜单中命令的响应。
按照上述指导思想,我和大家一起,一步一步完善ATLAddin最后一部分功能。
1)修改Ribbon.xml文件,添加"onAction",代码如下:
<?xml version="1.0" encoding="gb2312"?> <customUI onLoad="Ribbon_Load" xmlns="http://schemas.microsoft.com/office/2006/01/customui"> <ribbon> <tabs> <tab id ="TabAddIns" label ="测试"> <group id ="group1" label ="New Group"> <button id ="button1" label="button1" imageMso="HappyFace" onAction="onButton1_Click" size="large" /> <button id ="button2" label="button2" onAction="onButton2_Click" showImage="false" /> </group> </tab> </tabs> </ribbon> </customUI>
2)修改COM接口映射表。将代码中,派发至_IDTExtensibility2的方法转而指向IMSConnect。
修改后的接口映射表如下:
BEGIN_COM_MAP(CMSConnect) COM_INTERFACE_ENTRY(IMSConnect) // COM_INTERFACE_ENTRY2(IDispatch, _IDTExtensibility2) COM_INTERFACE_ENTRY2(IDispatch, IMSConnect) COM_INTERFACE_ENTRY(_IDTExtensibility2) COM_INTERFACE_ENTRY(IRibbonExtensibility) END_COM_MAP()
3)在类视图“IMSConnect”节点,添加方法onButton1_Click和onButton2_Click,如图所示:
在MSConnect.cpp中找到刚刚添加的两个方法,加入MessageBox测试代码。
STDMETHODIMP CMSConnect::onButton1_Click(IDispatch* ribbonControl) { // TODO: Add your implementation code here MessageBoxW(NULL, L "Button1", L"ATL Addin" , MB_OK); return S_OK; } STDMETHODIMP CMSConnect::onButton2_Click(IDispatch* ribbonControl) { // TODO: Add your implementation code here MessageBoxW(NULL, L "Button2", L"ATL Addin" , MB_OK); return S_OK; }
4)在MSConnect.h文件CMSConnect类中,重载Invoke方法,实现命令的响应。
STDMETHOD(Invoke)(DISPID dispidMember, const IID &riid, LCID lcid, WORD wFlags, DISPPARAMS *pdispparams, VARIANT *pvarResult, EXCEPINFO *pexceptinfo, UINT *puArgErr) { if(dispidMember == 1) onButton1_Click(NULL); else onButton2_Click(NULL); return S_OK; }
5)ATLAddin.idl ,添加如下代码响应
interface IMSConnect : IDispatch{ [id(1)] HRESULT onButton1_Click([in] IDispatch* ribbonControl);// 添加 // [id(2)] HRESULT onButton2_Click([in] IDispatch* ribbonControl); };
编译后,效果
虽然,这个例子可以创建成功,运行正常,但是想开发一个插件还是比较困难的,而且环境问题比较挑剔,#import出错后,根本无法解决,知道的人很少,网上资料也很少,
还是回归VSTO吧
================================20200611 更新 已解决import问题转 这里