開發支援 ActiveX 文件外掛的應用程式時,傳統做法是掃描註冊表、逐一建立元件實例並查詢特定介面 (如 IMyAppPlugin)。這不僅效率低落,在大量元件安裝時更會導致啟動緩慢與資源浪費。Windows 提供了更優雅的解決方案:元件分類。
// 使用 GUIDGEN 產生
// {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
#define CATID_AcmePlugin { ... }
// DllRegisterServer 中
CComPtr<ICatRegister> pcr;
pcr.CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC);
CATEGORYINFO catinfo = {0};
catinfo.catid = CATID_AcmePlugin;
catinfo.lcid = 0x0409; // 英文
wcscpy(catinfo.szDescription, L"My Acme Plugin");
pcr->RegisterCategories(1, &catinfo);
CATID catid = CATID_AcmePlugin;
pcr->RegisterClassImplCategories(CLSID_MyPluginObj, 1, &catid);
class ICatRegister : public IUnknown {
public:
virtual HRESULT RegisterCategories(ULONG cCategories, CATEGORYINFO rgCategoryInfo[]);
virtual HRESULT UnRegisterCategories(ULONG cCategories, CATID rgcatid[]);
virtual HRESULT RegisterClassImplCategories(REFCLSID rclsid, ULONG cCategories, CATID rgcatid[]);
virtual HRESULT UnRegisterClassImplCategories(REFCLSID rclsid, ULONG cCategories, CATID rgcatid[]);
virtual HRESULT RegisterClassReqCategories(REFCLSID rclsid, ULONG cCategories, CATID rgcatid[]);
virtual HRESULT UnRegisterClassReqCategories(REFCLSID rclsid, ULONG cCategories, CATID rgcatid[]);
};
class ICatInformation : public IUnknown {
public:
virtual HRESULT EnumCategories(LCID lcid, IEnumCATEGORYINFO** ppenum);
virtual HRESULT GetCategoryDesc(REFCATID rcatid, LCID lcid, LPWSTR* pszDesc);
virtual HRESULT EnumClassesOfCategories(
ULONG cImplemented, CATID rgcatidImpl[],
ULONG cRequired, CATID rgcatidReq[],
IEnumGUID** ppenumClsid);
virtual HRESULT IsClassOfCategories(REFCLSID rclsid, ULONG cImpl, CATID rgcatidImpl[], ULONG cReq, CATID rgcatidReq[]);
virtual HRESULT EnumImplCategoriesOfClass(REFCLSID rclsid, IEnumGUID** ppenumCatid);
virtual HRESULT EnumReqCategoriesOfClass(REFCLSID rclsid, IEnumGUID** ppenumCatid);
};
// CoolCat.h
#pragma once
#include <comcat.h>
inline CString CStringFromGuid(GUID& guid) {
LPOLESTR pstr = NULL;
StringFromCLSID(guid, &pstr);
return CString(pstr);
}
class CCatInformation : public CComPtr<ICatInformation> {
public:
CCatInformation() {
CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC);
ATLASSERT(p != NULL);
}
};
class CCatIterator {
CComPtr<IEnumCATEGORYINFO> m_spEnum;
CCatInformation m_spCatInfo;
public:
CCatIterator(LCID lcid = GetUserDefaultLCID()) {
m_spCatInfo->EnumCategories(lcid, &m_spEnum);
}
BOOL Next(CATEGORYINFO& info) {
ULONG fetched = 0;
return SUCCEEDED(m_spEnum->Next(1, &info, &fetched)) && fetched == 1;
}
};
class CCatClassIterator {
CComPtr<IEnumGUID> m_spEnum;
CCatInformation m_spCatInfo;
public:
CCatClassIterator(CATID* impl, ULONG nImpl, CATID* req = NULL, ULONG nReq = 0) {
m_spCatInfo->EnumClassesOfCategories(nImpl, impl, nReq, req, &m_spEnum);
}
BOOL Next(CLSID& clsid) {
ULONG fetched = 0;
return SUCCEEDED(m_spEnum->Next(1, &clsid, &fetched)) && fetched == 1;
}
};
雙窗格 MFC 應用程式:
CLeftView 列舉所有系統分類 (呼叫 CCatIterator)CRightView 顯示實現所選分類的類別清單 (呼叫 CCatClassIterator)
void CLeftView::PopulateCategoryList() {
CListCtrl& lc = GetListCtrl();
lc.DeleteAllItems();
CATEGORYINFO info;
CCatIterator it;
while (it.Next(info)) {
CString name = info.szDescription;
if (name.IsEmpty()) name = _T("(無描述)");
int idx = lc.InsertItem(0, name);
lc.SetItemText(idx, 1, CStringFromGuid(info.catid));
}
}
BOOL CRightView::ShowCategory(CATID& catid) {
CListCtrl& lc = GetListCtrl();
lc.DeleteAllItems();
CCatClassIterator it(&catid, 1);
CLSID clsid;
while (it.Next(clsid)) {
// 解析 ProgID、描述等註冊表資訊
int idx = lc.InsertItem(0, clsidStr);
lc.SetItemText(idx, 1, progId);
}
return TRUE;
}
早期 Visual Basic 需識別可插入表單的物件,採用 HKCR\CLSID\{CLSID}\Insertable 註冊表鍵值。微軟後將此機制泛化為通用的分類系統。現今 Insertable 鍵值仍保留以支援 16/32 位元互通,但新開發應全面採用 CATID 機制。
元件分類機制解決了「如何高效發現實現特定契約的 COM 元件」的核心問題。透過 ICatRegister 註冊與 ICatInformation 列舉,應用程式可在啟動時快速建立外掛選單,避免大量 CoCreateInstance + QueryInterface 的效能惡夢。建議所有 COM 外掛架構採用此標準機制。