問題:
我要編寫一個支持ActiveX文檔插件(Plug-ins)的應用程序。為了創建一個已安裝插件的菜單,在程序啟動時我掃描注冊表查找已安裝的ActiveX組件。對於每一個ActiveX組件創建一個實例並查詢一個叫IMyAppPlugin的專門接口。如果這個接口存在,那麼我就認為這個組件就是我的程序所要的插件。這樣做好像行不通,尤其是安裝有多個ActiveX組件時做起來就更困難。有沒有更好的辦法處理這種問題?
解答:
對於這種情況,Windows確實有更好的辦法來解決:既種類(category)。對於開發人員來說,種類是一種ActiveX控件。名字可以隨意取,如“My Acme Plugin”或者“Blue Insertable Thingies”。對於COM而言,種類只是一個GUID——不同的是種類用CATID表示GUID,這有點像表示某個類的GUID叫做CLSID一樣。
那麼在實際編程中如何使用CATID呢?首先要生成一個新的GUID(使用GUIDGEN或其它的同類程序),我們且把這個新生成的GUID叫做CATID_AcmePlugin。然後,用一個專門的COM接口ICatRegister來注冊你的種類。完成這個工作的地方一般是在DllRegisterServer函數中。為了獲得ICatRegister接口,必須調用CoCreateInstance或實現同樣功能的函數。
// 在 DllRegisterServer中
CComPtr spcr;
spcr.CoCreateInstance(CLSID_StdComponentCategoriesMgr,
NULL, CLSCTX_INPROC);
這段代碼使用ATL智能指針;CComPtr::CoCreateInstance還能用ICatRegister的IID調用::CoCreateInstance。一旦有了ICatRegister,便可以調用RegisterCategories。方法是先用自己的種類信息填寫CATEGORYINFO結構。
CATEGORYINFO catinfo;
catinfo.catid = CATID_AcmePlugin;
catinfo.lcid = 0x0409; // locale=english
USES_CONVERSION; // uses A2W
wcscpy(catinfo.szDescription,
A2W("My Acme Plugin."));
pcr->RegisterCategories(1, &catinfo);
接下來的任務是如何告訴COM你的COM類是Acme Plugin。ICatRegister也有相應的方法來做這件事情,它就是RegisterClassImplCategories。
// 也是在DllRegisterServer中
CATID catid = CATID_AcmePlugin;
pcr->RegisterClassImplCategories(
CLSID_MyPluginObj, 1, &catid);
這樣就注冊了你的COM類,實現種類CATID_AcmePlugin。是不是很簡單啊!這些都是此類編程的套路。ICatRegister將有關哪個類實現哪個種類的信息放入注冊表,以便Windows能快速讀到它,而不用像你最開始所做的那樣去實例化每一個組件來查找IMyAcmePlugin接口。
與種類的注冊類似,ICatRegister也有用注銷種類的方法,這兩個方法對於種類而言都是必須的(相對於實現而言),也就是說,你的COM類需要其容器來實現那些種類。當你的組件需要專門的回調接口時,就必須實現種類。下面是完整的ICatRrgister接口:
//
ICatRegister
////////////////////////////////////////////////////////////////
// ICatRegister interface, edited from comcat.h
//
class ICatRegister : public IUnknown {
public:
virtual HRESULT RegisterCategories(
ULONG cCategories, // number of categories to register
CATEGORYINFO rgCategoryInfo[]); // info for each one
virtual HRESULT UnRegisterCategories(
ULONG cCategories, // number of categories to unregister
CATID rgcatid[]); // their CATIDs
virtual HRESULT RegisterClassImplCategories(
REFCLSID rclsid, // COM class ID
ULONG cCategories, // number of categories it implements
CATID rgcatid[]); // their CATIDs
virtual HRESULT UnRegisterClassImplCategories(
REFCLSID rclsid, // COM class ID
ULONG cCategories, // num implemented categories to unreg
CATID rgcatid[]); // their CATIDs
virtual HRESULT RegisterClassReqCategories(
REFCLSID rclsid, // COM class ID
ULONG cCategories, // number of categories it requires
CATID rgcatid[]); // required CATIDs
virtual HRESULT UnRegisterClassReqCategories(
REFCLSID rclsid, // COM class ID
ULONG cCategories, // number of req''''d categories to unreg
CATID rgcatid[]); // CATIDs to unregister
};
//
對於注冊種類編程的實例請參見VC知識庫的另外一篇文章:“編寫可復用性更強的MFC代碼”。
講了那麼多有關注冊的問題。現在假設你寫了一個容器並且你想要產生一個插件(Acme Plugins)清單——既實現CATID_AcmePlugin的組件。Windoews提供了另一個接口,ICatInformation:
//
ICatInformation
class ICatInformation : public IUnknown {
public:
// Enumerate all categories
virtual HRESULT EnumCategories(
LCID lcid,
IEnumCATEGORYINFO ** ppenumCategoryInfo);
// Get locale-specific category descriptor
virtual HRESULT GetCategoryDesc(
REFCATID rcatid,
LCID lcid,
LPWSTR *pszDesc);
// Enumerate classes that implement/require given categories
virtual HRESULT EnumClassesOfCategories(
ULONG cImplemented,
CATID rgcatidImpl[],
ULONG cRequired,
CATID rgcatidReq[],
IEnumGUID **ppenumClsid);
// Determine if class implements/requires given categories
virtual HRESULT IsClassOfCategories(
REFCLSID rclsid,
ULONG cImplemented,
CATID rgcatidImpl[ ],
ULONG cRequired,
CATID rgcatidReq[ ]);
// Enumerate categories implemented by given class
virtual HRESULT EnumImplCategoriesOfClass(
REFCLSID rclsid,
IEnumGUID **ppenumCatid);
// Enumerate categories required by given class
virtual HRESULT EnumReqCategoriesOfClass(
REFCLSID rclsid,
IEnumGUID **ppenumCatid);
};
//
用這接口可以枚舉實現給定種類的類。為了說明ICatInformation接口使用,我寫了一個小程序CatView,用這個程序可以浏覽系統中注冊的種類。如圖一所示:
圖一 CatView 浏覽系統中注冊的種類
下面是CatView 有關的代碼:(全部源代碼可以從本文最前面的鏈接下載)
// CoolCat.h — helper stuff for COMponent categories.
//
#pragma once
#include
//////////////////
// Helper function to get GUID in human-readable format as CString.
//
inline CString CStringFromGuid(GUID& guid)
{
LPOLESTR pstr=NULL;
StringFromCLSID(guid, &pstr);
return CString(pstr);
}
////////////////
// Handy Category Information class. Instantiate and go.
//
class CCatInformation : public CComPtr {
public:
CCatInformation() {
CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,
CLSCTX_INPROC);
ASSERT(p);
}
};
//////////////////
// Handy class to enumerate categories
//
class CCatIterator {
protected:
CComPtr spEnumCatInfo; // IEnumCATEGORYINFO
CCatInformation spCatInfo; // ICatInformation
public:
CCatIterator(LCID lcid = GetUserDefaultLCID()) {
HRESULT hr = spCatInfo->EnumCategories(lcid, &spEnumCatInfo);
ASSERT(SUCCEEDED(hr));
}
BOOL Next(CATEGORYINFO& catinfo) {
ULONG nRet=0;
return SUCCEEDED(spEnumCatInfo->Next(1, &catinfo, &nRet)) &&
nRet==1;
}
};
//////////////////
// Handy class to enumerate classes that implement a category
//
class CCatClassIterator {
protected:
CComPtr spEnumCLSID; // IEnumCLSID
CCatInformation spCatInfo; // ICatInformation
public:
CCatClassIterator(CATID* arImplCatids, ULONG nImpl,
CATID* arReqdCatids=NULL, ULONG nReqd=0) {
HRESULT hr = spCatInfo->EnumClassesOfCategories(
nImpl, // num implemented cats in array
arImplCatids, // array of cats to look for (implement)
nReqd, // num required categories in array
arReqdCatids, // array of required categories to look for
&spEnumCLSID); // IEnum returned
ASSERT(SUCCEEDED(hr));
}
BOOL Next(CLSID& clsid) {
ULONG nRet=0;
return SUCCEEDED(spEnumCLSID->Next(1, &clsid, &nRet)) && nRet==1;
}
};
View.h
#pragma once
//////////////////
// Right pane is a list of controls that implement a category.
//
class CRightView : public CListView {
public:
CRightView();
virtual ~CRightView();
BOOL ShowCategory(CATID& catid);
protected:
virtual void OnInitialUpdate(); // called first time after construct
virtual void OnDraw(CDC* pDC); // overridden to draw this view
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
DECLARE_DYNCREATE(CRightView)
DECLARE_MESSAGE_MAP()
};
//////////////////
// Left pane is a list of categories.
//
class CLeftView : public CListView {
public:
virtual ~CLeftView();
virtual void OnDraw(CDC* pDC); // overridden to draw this view
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
void SetRightPane(CRightView* pRightPane) {
m_pRightPane = pRightPane;
}
protected:
CRightView* m_pRightPane;
CLeftView();
void PopulateCategoryList();
virtual void OnInitialUpdate(); // called first time after construct
afx_msg void OnItemChanged(NMHDR* pNMHDR, LRESULT* pRes);
afx_msg LRESULT OnWinMgr(WPARAM wp, LPARAM lp);
DECLARE_MESSAGE_MAP()
DECLARE_DYNCREATE(CLeftView)
};
LeftView.cpp
//
#include "stdafx.h"
#include "View.h"
#include "WinMgr.h"
#include "CoolCat.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
IMPLEMENT_DYNCREATE(CLeftView, CListView)
BEGIN_MESSAGE_MAP(CLeftView, CListView)
ON_NOTIFY_REFLECT(LVN_ITEMCHANGED,OnItemChanged)
ON_REGISTERED_MESSAGE(WM_WINMGR, OnWinMgr)
END_MESSAGE_MAP()
CLeftView::CLeftView() : m_pRightPane(NULL)
{
}
CLeftView::~CLeftView()
{
}
BOOL CLeftView::PreCreateWindow(CREATESTRUCT& cs)
{
cs.style |= LVS_REPORT | LVS_SORTASCENDING | LVS_NOSORTHEADER;
return CListView::PreCreateWindow(cs);
}
void CLeftView::OnDraw(CDC* pDC)
{
}
//////////////////
// First-time init: add column headers
//
void CLeftView::OnInitialUpdate()
{
CListView::OnInitialUpdate();
const COLWIDTH = 250;
CListCtrl& lc = GetListCtrl();
lc.InsertColumn(0, _T("Category Name"),LVCFMT_LEFT,COLWIDTH);
lc.InsertColumn(1, _T("CATID"),LVCFMT_LEFT,COLWIDTH,1);
PopulateCategoryList();
}
//////////////////
// Populate list of categories.
//
void CLeftView::PopulateCategoryList()
{
CListCtrl& lc = GetListCtrl();
lc.DeleteAllItems();
CATEGORYINFO catinfo;
CCatIterator it;
while (it.Next(catinfo)) {
// add category name to list
CString sName = catinfo.szDescription;
if (sName.IsEmpty()) {
sName = _T("");
}
int iItem = lc.InsertItem(0,sName);
// Add CATID as 1st subitem
lc.SetItemText(iItem,1,CStringFromGuid(catinfo.catid));
}
}
//////////////////
// User selected a new category: show controls in right pane.
//
void CLeftView::OnItemChanged(NMHDR* pNMHDR, LRESULT* pRes)
{
NMLISTVIEW nm = *(NMLISTVIEW*)pNMHDR;
if (nm.iItem>=0 && (nm.uNewState & LVIS_SELECTED)) {
CListCtrl& lc = GetListCtrl();
CString sguid = lc.GetItemText(nm.iItem,1);
CATID catid;
USES_CONVERSION;
if (SUCCEEDED(CLSIDFromString(T2OLE((LPCTSTR)sguid),&catid)))
m_pRightPane->ShowCategory(catid);
else
MessageBeep(0);
}
*pRes= 0;
}
//////////////////
// Handle WinMgr request for size info: compute TOFIT size for list view,
// which is sum of widths of columns.
//
LRESULT CLeftView::OnWinMgr(WPARAM wp, LPARAM lp)
{
ASSERT(lp);
NMWINMGR& nmw = *(NMWINMGR*)lp;
if (nmw.code==NMWINMGR::GET_SIZEINFO && (int)wp==GetDlgCtrlID()) {
CSize sz(0,0);
CListCtrl& lc = GetListCtrl();
int nCols = lc.GetHeaderCtrl()->GetItemCount();
for (int iCol=0; iCol");
lc.SetItemText(iItem,1,sProgID);
}
return TRUE;
}
//
CatView是個典型的將窗口切分成兩個窗格的程序,左邊窗格是種類清單,當單擊其中一條記錄,右邊窗格會顯示相應的實現這個種類的類信息。(CatView程序中使用了一個類CwinMgr,這個類將在另外一篇文章中做專門討論:“創建一個隨心所欲定制窗口尺寸的類”)。圖一所示,選中“Active Scripting Engine with Parsing”列表項,則右邊的窗格將顯示實現它的各個組件:XML,Java,Visual Basic和PerlScript腳本引擎。CatView中的兩個主要的函數是CLeftView::PopulateCategoryList 和 CRightView::ShowCategory。為了簡單起見,我實現了一些有用的輔助類(在頭文件CoolCat.h中)。第一各類是CCatInformation,它用ATL智能指針封裝了ICatInformation接口。
//
class CCatInformation : public CComPtr {
public:
CCatInformation() {
CoCreateInstance(CLSID_StdComponentCategoriesMgr,
NULL, CLSCTX_INPROC);
}
};
有了CCatInformation類,就不用再調用CoCreateInstance——實例化,然後直接使用類對象。
CCatInformation spCatInfo;
spCatInfo->SomeMethod(...);
為了枚舉系統中的組件種類,調用ICatInformation::EnumCategories 。這個函數回傳一個IEnumCATEGORYINFO 接口指針,然後用這個指針枚舉種類。
// IEnumCATEGORYINFO
CCatInformation spCatInfo;
CComPtr spEnumCatInfo;
HRESULT hr = spCatInfo->EnumCategories(
GetUserDefaultLCID(),&spEnumCatInfo);
ASSERT(SUCCEEDED(hr));
// 使用指針枚舉種類
ULONG nRet=0;
CATEGORYINFO catinfo;
while (SUCCEEDED(spEnumCatInfo->Next(1,
&catinfo, &nRet)) && nRet==1) {
// add catinfo to list
}
COM的技術機制實際上就這麼幾招。即使是使用ATL智能指針也是如此,我把這幾招COM編程技術都封裝在一個輔助類CCatIterator中,以便使用起來方便一些。有了CcatIterator輔助類,要做的事情很簡單:
CATEGORYINFO catinfo;
CCatIterator it;
while (it.Next(catinfo)) {
// add catinfo to list
}
CLeftView::PopulateCategoryList用CCatIterator類以名字和每個種類的CATID構造列表視圖。每次調用Next來將下一個種類的信息填入catinfo。在這裡請記住我的一些經驗之談,在進行COM編程時,做好是編寫一些自己的小型輔助類以免去處理那些頭疼的HRESULTs和接口指針,尖括弧以及Release操作。我是個唯美主義者,要求自己的代碼不僅要正確運行,還要求好看。
一旦具備了CATID,就可以用ICatInformation來得到實現種類的COM類清單。例如,實現CATID_AcmePlugin的所有控件。其中最關鍵的部分是ICatInformation::EnumClassesOfCategories以及枚舉器IEnumCLSID。同樣我也寫了一個類來封裝這些東西。
CLSID clsid;
CCatClassIterator it(&catid, 1);
while (it.Next(clsid)) {
// add clsid to list
}
與ICatInformation::EnumClassesOfCategories類似,CCatClassIterator可以使你指定多個實現的種類。如“查找所有AcmePlugin和Blue Insertable Thingies 控件”。在這種情況下,要傳遞一個包含兩個CATIDs的數組。你還能指定一個或多個必須的種類來查找需要一個或多個給定的控件。通過缺省值NULL,CCatClassIterator隱藏了所有額外的參數。
以上內容討論了COM技術中對種類的編程。下面將談談CatView的其余部分,它與Windows及其MFC有關。CatView是一個文檔/視結構的應用,但CDummyDoc只是為MFC而存在的。CMainFrame::OnCreateClient創建由窗格並在執行了通常的CframeWnd之後與左邊窗格關聯起來。在程序中唯有CLeftView::OnWinMgr是比較特殊的東西,它通過添加列寬來報告列表視圖畫面的TOFIT尺寸。(有關WinMgr和TOFIT的內容,請參見另外一篇文章:“創建一個隨心所欲定制窗口尺寸的類”)。
本文附帶的CatView例子可以從文章開始處的鏈接下載。編譯後可以在自己的機器上運行,以觀察機器上注冊的種類。你會注意到一些晦澀難董的種類(如Visual InterDev Web Site Wizards)以及一些通用的控件,自動化對象和可插入種類。從COM的歷史看,可插入種類是整個種類概念的祖先。回溯到早期,Visual Basic需要某種方式來獲得哪個對象能被插入表單(forms),不用實例化每一個在注冊表中的類來查找(QueryInterface)IOleInPlaceObject接口。解決方法是添加一個專門的鍵值,HKCR\CLSID\{CLSID}\Insertable,它告訴Visual Basic 類是可插入的(insertable)。後來微軟擴展了這個機制變成更一般的概念,它就是我們在這裡所說的種類。今天,Insertable鍵是個遺留下來的東西,對於要在16位應用插入32位對象,Insertable鍵是必不可少的。
本文配套源碼