首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 組合語言

Win32 程式設計實戰:從零手寫 ActiveX 控件容器(無 MFC/ATL/IDE 向導)

     編輯:組合語言

核心概念:ActiveX 控件與容器關係

ActiveX 技術源於 COM,核心在於嵌入:容器應用程式(如 Word、IE、自寫 Win32 視窗)載入 ActiveX 控件(伺服器端物件),並在視窗區域內顯示、互動。雙方透過標準介面溝通:

  • 容器端 實作:IOleClientSite、IOleInPlaceSite、IOleWindow 等。
  • 控件端 實作:IOleObject、IOleInPlaceObject、IViewObject 等。

本文示範最精簡容器實作,僅用純 Win32 API + COM,無 MFC/ATL/#import/IDE 向導。

最小容器需實作的介面

建立 ActiveX 控件的標準流程

  1. 實作容器類別(繼承上述介面)。
  2. CoCreateInstance 建立控件實例,取得 IOleObject。
  3. IOleObject::SetClientSite 傳入容器指標。
  4. IOleObject::DoVerb(OLEIVERB_SHOW) 啟動並顯示控件。
  5. 視窗大小變更時,呼叫 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 等鍵可能無響應。

常見問題與解決

介面關鍵方法用途
IUnknownQueryInterface、AddRef、ReleaseCOM 基礎,所有介面繼承此
IOleClientSiteSaveObject、GetMoniker、GetContainer、ShowObject、OnShowWindow、RequestNewObjectLayout容器基本協調:儲存、單體、顯示通知
IOleInPlaceSiteCanInPlaceActivate、OnInPlaceActivate、OnUIActivate、GetWindowContext、Scroll、OnUIDeactivate、OnInPlaceDeactivate、DiscardUndoState、DeactivateAndUndo、OnPosRectChange就地啟用:視窗管理、選單/工具列合併、捲動、座標映射
IOleWindowGetWindow、ContextSensitiveHelp取得容器視窗控制代碼 (HWND)
問題原因解決
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。掌握此最小原型,即可按需擴展至生產級容器(如瀏覽器、報表檢視器、編輯器宿主)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved