當前的項目是為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加載項的初始化過程,下面可以開始根據實際業務需求進行開發了
