C++开发Excel的com加载项(一)
当前的项目是为Excel开发一个加载项以实现金融相关的业务,综合很多方面因素考虑后,决定放弃C#,而用C++进行开发。用C++开发Excel加载项目前有两种方式,一是Excel加载项xll,另一种是使用ATL制作com加载项。xll方式的好处是它接近Excel的底层,执行速度很快,而且不需要修改注册表,但使用它的复杂度也较高,需要学习一下其专用的数据结构和api调用方式。我本来打算完全用xll进行开发,只是很可惜卡在最后一步,没能找到用xll的api实现ribbon菜单的方法。com加载项开发起来较容易一些,而且对很多功能都提供了接口,只需要简单实现一下就好。最后的决定是采用com加载项的方式进行开发,今后若有必要的话,再考虑其与xll混合开发的方式。以下只是我在开发过程中遇到一些事情的解决方法,并不意味其就是最优方法,以后会随着工作和学习的深入而进行修正。
创建Excel的com加载项的方式在VS2008及以前是很容易的,只需新建项目时在扩展性里选择共享的外接程序,然后按照导航操作即可。我目前正在使用的VS2012并没有这个模板,所以只能新建一个ATL项目并自己添加实现接口。在MSDN上有一篇文章为 Outlook 2010 构建 C++ 加载项非常详细的说明了这个过程,这里只做简要摘录并说明一下在某些步骤可能遇到的问题及我如何解决的。
一、创建项目
1、新建一个ATL项目。由于我想使用MFC控件,所以勾选了支持MFC,其他采用默认设置即可。
2、为该项目添加一个类,从ATL栏中选择ATL简单对象。这里有一个惯例,此类一般命名为CConnect。填一下ProgID,在注册表中注册本加载项时需要使用。
3、为CConnect类实现接口。选择Microsoft Add-In Designer<1.0>类型库中的_IDTExtensibility2接口,此为所有Office加载项都必须实现的接口。将继承声明中的&LIBID_AddInDesignerObjects改为&__uuidof(__AddInDesignerObjects),将此接口的5个方法的返回值都改为S_OK。
4、添加注册表文件。在Connect.rgs文件中,追加注册表信息,可以从MSDN中直接复制的,但需注意要将Outlook改为Excel,并将NativeAddin.Connect改为自己命名的ProgID。
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 } } } } } } }
5、设置调试。将调试的命令属性设为预启动的Excel,则在调试项目时会自动启动Excel并加载本加载项。
以上步骤完成后,即可完成项目创建。我们可以在OnConnection函数中增加一个弹窗,用以证明加载项的确已被成功加载。调试程序时,可能会出现注册表写入失败,因为WIN7的权限设置问题,需要在DllRegisterServer和DllUnregisterServer函数的最上方添加一行代码ATL::AtlSetPerUserRegistration(true);
二、自定制ribbon菜单
自定制ribbon菜单需要实现IRibbonExtensibility接口,此接口在程序库Microsoft Office 12.0 Object Library<2.4> 中找到(名称的版本号会随Office安装版本的不同而略有区别)。
1、实现接口。将继承声明中的&LIBID_Office改为&__uuidof(__Office),在stdafx.h中对命名空间和方法重命名以避免冲突。如下所示
#import "C:\Program Files (x86)\Common Files\DESIGNER\MSADDNDR.DLL" auto_rename auto_search raw_interfaces_only rename_namespace("AddinDesign") #import "C:\Program Files (x86)\Common Files\Microsoft Shared\OFFICE12\MSO.DLL" auto_rename auto_search raw_interfaces_only rename_namespace("Office") rename("RGB","MsoRGB") rename("SearchPath","MsoSearchPath") using namespace AddinDesign; using namespace Office;
2、添加ribbon描述xml。可用MSDN中的例子修改。
3、实现GetCustomUI接口。这个直接用MSDN中的源码即可。
STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml) { if(!RibbonXml) return E_POINTER; *RibbonXml = GetXMLResource(IDR_XML1); return S_OK; } HRESULT CConnect::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 CConnect::GetXMLResource(int nId) { LPVOID pResourceData = NULL; DWORD dwSizeInBytes = 0; HRESULT hr = HrGetResource(nId, _T("XML"), &pResourceData, &dwSizeInBytes); if (FAILED(hr)) return NULL; CComBSTR cbstr(dwSizeInBytes, reinterpret_cast<LPCSTR>(pResourceData)); return cbstr.Detach(); }
4、自定制按钮图片和按钮事件。先将IConnect设为默认的响应接口。对自定义图片,需要在按钮上用image属性标志自定义图片,在根节点上用loadImage回调来加载自定义图片。对按钮事件,需在按钮上用onAction回调。所有回调函数的签名可以参见Customizing the 2007 Office Fluent Ribbon for Developers (Part 3 of 3),在项目的idl文件中加入回调函数接口的声明,并在Connect.h中将其实现
BEGIN_COM_MAP(CConnect)
COM_INTERFACE_ENTRY(IConnect)
COM_INTERFACE_ENTRY2(IDispatch, IConnect)
COM_INTERFACE_ENTRY(_IDTExtensibility2)
COM_INTERFACE_ENTRY(IRibbonExtensibility)
END_COM_MAP()
interface IConnect : IDispatch{ HRESULT HistoryButtonClicked([in] IDispatch* ribbon); HRESULT GetImage([in] BSTR *pbstrImageId, [out, retval] IPictureDisp ** ppdispImage); };
STDMETHOD(HistoryButtonClicked)(IDispatch* ribbon);
STDMETHOD(GetImage)(BSTR *pbstrImageId,IPictureDisp ** ppdispImage);
按钮事件的具体实现代码就很简单了,这里说一下加载图片的接口的实现。第一个参数表示图片的名称,是image属性的值,第二个参数是将图片的信息输出给Excel。这里可以将图片放入资源中,利用GDI+将其转为Bitmap并通过OleCreatePictureIndirect将其存入ppdispImage。
STDMETHODIMP CConnect::GetImage(BSTR *pbstrImageId,IPictureDisp ** ppdispImage) { int pngId(0); try { pngId = lexical_cast<int>(*pbstrImageId); } catch(...) { return E_UNEXPECTED; } using namespace Gdiplus; LPVOID pResourceData = NULL; DWORD len = 0; HRESULT hr = HrGetResource(pngId,_T("PNG"), &pResourceData, &len); BYTE* lpRsrc = reinterpret_cast<BYTE*>(pResourceData); if (!lpRsrc) { return E_UNEXPECTED; } HGLOBAL m_hMem = GlobalAlloc(GMEM_FIXED, len); BYTE* pmem = (BYTE*)GlobalLock(m_hMem); memcpy(pmem,lpRsrc,len); GlobalUnlock(m_hMem); IStream* pstm; CreateStreamOnHGlobal(m_hMem,FALSE,&pstm); PICTDESC pic; memset(&pic, 0, sizeof pic); Bitmap *png = Bitmap::FromStream(pstm); HBITMAP hMap = NULL; png->GetHBITMAP(Color(),&hMap); pic.picType = PICTYPE_BITMAP; pic.bmp.hbitmap = hMap; OleCreatePictureIndirect(&pic,IID_IPictureDisp,true,(LPVOID*)ppdispImage); return S_OK; }
因为资源中图片的ID是数字,而xml中image的属性值是字符串,所有将图片的ID以字符串的方式放在image中,在回调中再转为数字以查找资源,方法比较笨,等以后发现更好的方法再做修改。
经过上述步骤 即可完成自定义ribbon菜单的全部过程。
三、操作Excel
想在加载项中访问并操作Excel,需要在stdafx.h中加入如下语句,注意实际路径和本机安装Office的路径相关
#import "C:\\Program Files (x86)\\Common Files\\Microsoft Shared\\VBA\\VBA6\\VBE6EXT.OLB" #import "D:\\Program Files (x86)\\Microsoft Office\\Office12\\EXCEL.EXE" rename( "DialogBox", "ExcelDialogBox" ) rename( "RGB", "ExcelRGB" ) rename( "CopyFile", "ExcelCopyFile" ) rename( "ReplaceText", "ExcelReplaceText" ) exclude( "IFont", "IPicture" ) no_dual_interfaces using namespace Excel;
然后在OnConnection中加入初始化代码即可访问Excel了
Excel::_ApplicationPtr pExcel; STDMETHOD(OnConnection)(LPDISPATCH Application, ext_ConnectMode ConnectMode, LPDISPATCH AddInInst, SAFEARRAY * * custom) { pExcel.GetActiveObject("Excel.Application");
return S_OK; }
做完上述步骤,则基本上完成了一个com加载项的初始化过程,下面可以开始根据实际业务需求进行开发了