| 介面 | 關鍵方法 | 用途 |
IUnknown | QueryInterface、AddRef、Release | COM 基礎,所有介面繼承此 |
IOleClientSite | SaveObject、GetMoniker、GetContainer、ShowObject、OnShowWindow、RequestNewObjectLayout | 容器基本協調:儲存、單體、顯示通知 |
IOleInPlaceSite | CanInPlaceActivate、OnInPlaceActivate、OnUIActivate、GetWindowContext、Scroll、OnUIDeactivate、OnInPlaceDeactivate、DiscardUndoState、DeactivateAndUndo、OnPosRectChange | 就地啟用:視窗管理、選單/工具列合併、捲動、座標映射 |
IOleWindow | GetWindow、ContextSensitiveHelp | 取得容器視窗控制代碼 (HWND) |
建立 ActiveX 控件的標準流程
- 實作容器類別(繼承上述介面)。
CoCreateInstance 建立控件實例,取得 IOleObject。
IOleObject::SetClientSite 傳入容器指標。
IOleObject::DoVerb(OLEIVERB_SHOW) 啟動並顯示控件。
- 視窗大小變更時,呼叫
IOleInPlaceObject::SetObjectRects 調整控件矩形。
核心程式碼實作
1. 容器類別宣告
class CControlContainer : public IOleClientSite, public IOleInPlaceSite {
HWND m_hWnd; // 父視窗控制代碼
ULONG m_refCnt; // 參考計數
public:
CControlContainer(HWND hWnd) : m_hWnd(hWnd), m_refCnt(1) {}
// IUnknown
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
if (riid == IID_IUnknown || riid == IID_IOleClientSite || riid == IID_IOleInPlaceSite || riid == IID_IOleWindow)
*ppv = static_cast(this);
else
return E_NOINTERFACE;
AddRef();
return S_OK;
}
STDMETHODIMP_(ULONG) AddRef() { return ++m_refCnt; }
STDMETHODIMP_(ULONG) Release() { ULONG c = --m_refCnt; if (c == 0) delete this; return c; }
// IOleClientSite - 大多返回 S_OK 或 E_NOTIMPL
STDMETHODIMP SaveObject() { return E_NOTIMPL; }
STDMETHODIMP GetMoniker(DWORD, DWORD, IMoniker**) { return E_NOTIMPL; }
STDMETHODIMP GetContainer(IOleContainer**) { return E_NOINTERFACE; }
STDMETHODIMP ShowObject() { return S_OK; }
STDMETHODIMP OnShowWindow(BOOL) { return S_OK; }
STDMETHODIMP RequestNewObjectLayout() { return E_NOTIMPL; }
// IOleWindow
STDMETHODIMP GetWindow(HWND* phwnd) { *phwnd = m_hWnd; return S_OK; }
STDMETHODIMP ContextSensitiveHelp(BOOL) { return S_OK; }
// IOleInPlaceSite
STDMETHODIMP CanInPlaceActivate() { return S_OK; }
STDMETHODIMP OnInPlaceActivate() { return S_OK; }
STDMETHODIMP OnUIActivate() { return S_OK; }
STDMETHODIMP GetWindowContext(IOleInPlaceFrame**, IOleInPlaceUIWindow**, LPRECT, LPRECT, LPOLEINPLACEFRAMEINFO) { return E_NOTIMPL; }
STDMETHODIMP Scroll(SIZE) { return S_OK; }
STDMETHODIMP OnUIDeactivate(BOOL) { return S_OK; }
STDMETHODIMP OnInPlaceDeactivate() { return S_OK; }
STDMETHODIMP DiscardUndoState() { return S_OK; }
STDMETHODIMP DeactivateAndUndo() { return S_OK; }
STDMETHODIMP OnPosRectChange(LPCRECT) { return S_OK; }
};
2. 建立控件封裝函式
HRESULT CreateAxControl(HWND hWnd, const wchar_t* ProgID, IUnknown** ppCtrlUnk, IUnknown** ppContUnk) {
HRESULT hr;
CLSID clsid;
IOleObject* pObj = nullptr;
CControlContainer* pCont = nullptr;
// 1. ProgID -> CLSID
hr = CLSIDFromProgID(ProgID, &clsid);
if (FAILED(hr)) return hr;
// 2. 建立控件實例,取得 IOleObject
hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER,
IID_IOleObject, (void**)&pObj);
if (FAILED(hr)) return hr;
// 3. 建立容器物件
pCont = new CControlContainer(hWnd);
// 4. 設定容器站點
hr = pObj->SetClientSite(pCont);
if (FAILED(hr)) { pObj->Release(); delete pCont; return hr; }
// 5. 啟動顯示控件
hr = pObj->DoVerb(OLEIVERB_SHOW, nullptr, pCont, 0, hWnd, nullptr);
if (FAILED(hr)) { pObj->Release(); delete pCont; return hr; }
// 6. 回傳介面指標供外部使用
pObj->QueryInterface(IID_IUnknown, (void**)ppCtrlUnk);
pCont->QueryInterface(IID_IUnknown, (void**)ppContUnk);
pObj->Release();
pCont->Release();
return S_OK;
}
3. 視窗程式整合:建立與大小調整
IUnknown* g_pControl = nullptr; // 控件 IUnknown
IUnknown* g_pContainer = nullptr; // 容器 IUnknown
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CREATE: {
// 建立 Flash 控件 (ShockwaveFlash.ShockwaveFlash)
HRESULT hr = CreateAxControl(hWnd, L"ShockwaveFlash.ShockwaveFlash", &g_pControl, &g_pContainer);
if (SUCCEEDED(hr) && g_pControl) {
// 設定 Movie 屬性 (透過 IDispatch Invoke)
VARIANT var; var.vt = VT_BSTR; var.bstrVal = SysAllocString(L"https://example.com/flash.swf");
DISPID dispid; OLECHAR* name = L"Movie";
IDispatch* pDisp; g_pControl->QueryInterface(IID_IDispatch, (void**)&pDisp);
pDisp->GetIDsOfNames(IID_NULL, &name, 1, LOCALE_USER_DEFAULT, &dispid);
DISPPARAMS dp = {&var, nullptr, 1, 0};
pDisp->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT, &dp, nullptr, nullptr, nullptr);
pDisp->Release();
SysFreeString(var.bstrVal);
}
break;
}
case WM_SIZE: {
// 調整控件大小填滿客戶區
if (g_pControl) {
RECT rc; GetClientRect(hWnd, &rc);
IOleInPlaceObject* pInPlace = nullptr;
if (SUCCEEDED(g_pControl->QueryInterface(IID_IOleInPlaceObject, (void**)&pInPlace))) {
pInPlace->SetObjectRects(&rc, &rc);
pInPlace->Release();
}
}
break;
}
case WM_DESTROY:
if (g_pControl) g_pControl->Release();
if (g_pContainer) g_pContainer->Release();
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
關鍵技術細節補充
1. ProgID vs CLSID
- ProgID:人類可讀字串(如
"ShockwaveFlash.ShockwaveFlash"),登錄於 HKCR\ProgID\CLSID。
- CLSID:128-bit GUID,COM 真正識別碼。
CLSIDFromProgID 自動查表轉換。
2. 就地啟用 vs 無視窗啟用
DoVerb(OLEIVERB_SHOW):嘗試就地啟用(需容器支援 IOleInPlaceSite)。
- 若容器未實作
IOleInPlaceSite,控件會退回到無視窗模式(IViewObject::Draw 繪製到容器 DC)。
3. 控件屬性/方法呼叫
- ActiveX 控件支援
IDispatch(自動化介面),可透過 GetIDsOfNames + Invoke 動態存取屬性/方法。
- 範例中設定 Flash
Movie 屬性即為此機制。
4. 訊息迴路與鍵盤加速鍵
- 容器需實作
IOleInPlaceFrame::TranslateAccelerator 轉發鍵盤快捷鍵給控件。
- 本文簡化版未實作,故控件內 Tab/Enter 等鍵可能無響應。
常見問題與解決
| 問題 | 原因 | 解決 |
| CoCreateInstance 返回 CLASS_E_CLASSNOTAVAILABLE | 控件未註冊、ProgID 錯誤、位元數不匹配 (x86/x64) | 確認控件已安裝註冊、用 32 位編譯器跑 32 位控件、檢查註冊表 CLSID |
| 控件顯示空白/黑框 | SetObjectRects 未呼叫、矩形錯誤、控件未就地啟用 | WM_SIZE 時呼叫 SetObjectRects、確認 IOleInPlaceSite 實作正確 |
| 控件不接收鍵盤/滑鼠 | 未實作 IOleInPlaceFrame::TranslateAccelerator、焦點問題 | 實作 TranslateAccelerator、WM_SETFOCUS 時呼叫 IOleInPlaceObject::InPlaceActivate |
| 記憶體洩漏/崩潰 | 介面指標未 Release、循環參考、容器物件過早刪除 | 嚴格遵循 AddRef/Release、控件銷毀前呼叫 IOleObject::Close(OLECLOSE_NOSAVE) |
擴展方向
- 實作
IOleInPlaceFrame:提供選單列、工具列、狀態列合併(完整就地啟用體驗)。
- 實作
IAdviseSink:接收控件資料變更、視圖變更、關閉通知。
- 支援
IPersistStreamInit/IPersistStorage:儲存/載入控件狀態。
- 事件處理:實作
IConnectionPointContainer 接收控件事件(如 Flash OnReadyStateChange)。
總結
手寫 ActiveX 容器雖繁瑣,但能深入理解 COM/ActiveX 架構:容器實作 Site 介面、控件實作 Object 介面、雙方透過 SetClientSite/DoVerb 建立連接、視窗管理靠 IOleInPlaceSite/Object、屬性方法靠 IDispatch。掌握此最小原型,即可按需擴展至生產級容器(如瀏覽器、報表檢視器、編輯器宿主)。
- 上一篇文章: Windows Shell AutoComplete 全攻略:從 SHAutoComplete 到自定義 IEnumString 與 IACList
- 下一篇文章: 從組合語言視角理解程式語言本質
Copyright ©
程式碼之家 All Rights Reserved