来自VC知识库:http://www.vckbase.com/index.php/wv/159
问题: 我要编写一个支持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接口。解决方法是添加一个专门的键值,HKCRCLSID{CLSID}Insertable,它告诉Visual Basic 类是可插入的(insertable)。后来微软扩展了这个机制变成更一般的概念,它就是我们在这里所说的种类。今天,Insertable键是个遗留下来的东西,对于要在16位应用插入32位对象,Insertable键是必不可少的。