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

ActiveX 控件調用網頁 JavaScript 函數完整實現指南

     編輯:VC++

場景與挑戰

在網頁中嵌入 ActiveX 控件 (COM 對象) 時,控件往往需要回調頁面腳本函數 (如通知完成、觸發 UI 更新)。由於控件運行在瀏覽器進程中,需跨越 COM 邊界獲取 `window` 對象的 `IDispatch`,進而調用 JS 函數。本文梳理完整調用鏈路並提供生產級代碼模式。

核心調用鏈路:4 步走戰略

  1. 獲取 Client Site:實現 IOleObject::SetClientSite(IOleClientSite*),瀏覽器調用此方法傳遞站點指針。
  2. 查詢服務鏈:IOleClientSite → IServiceProvider → SID_STopLevelBrowser → IServiceProvider → SID_SWebBrowserApp → IWebBrowser2
  3. 獲取 Document & Script:IWebBrowser2::get_Document → IHTMLDocument2 → get_Script → IDispatch (腳本引擎)
  4. 調用腳本函數:IDispatch::GetIDsOfNames → Invoke(DISPATCH_METHOD)

關鍵代碼實現 (ATL 專案)

1. 接口聲明

// .h 文件
STDMETHOD(SetClientSite)(IOleClientSite* pClientSite);

2. 實現 SetClientSite 獲取瀏覽器與文檔對象

STDMETHODIMP CMyActiveX::SetClientSite(IOleClientSite* pClientSite) {
    HRESULT hr = S_OK;
    m_spClientSite = pClientSite; // 保存以便後續使用

    if (!pClientSite) {
        m_spBrowser.Release();
        m_spDoc.Release();
        return S_OK;
    }

    CComPtr spSP1, spSP2;
    try {
        // 步驟 1: ClientSite -> IServiceProvider
        hr = pClientSite->QueryInterface(IID_IServiceProvider, (void**)&spSP1);
        if (FAILED(hr)) throw hr;

        // 步驟 2: 獲取頂級瀏覽器服務提供者
        hr = spSP1->QueryService(SID_STopLevelBrowser, IID_IServiceProvider, (void**)&spSP2);
        if (FAILED(hr)) throw hr;

        // 步驟 3: 獲取 IWebBrowser2 (核心瀏覽器接口)
        hr = spSP2->QueryService(SID_SWebBrowserApp, IID_IWebBrowser2, (void**)&m_spBrowser);
        if (FAILED(hr)) throw hr;

        // 步驟 4: 獲取文檔對象
        hr = m_spBrowser->get_Document((IDispatch**)&m_spDoc);
        if (FAILED(hr)) throw hr;

    } catch (HRESULT err) {
        hr = err;
        m_spBrowser.Release();
        m_spDoc.Release();
    }
    return hr; // 即使失敗也返回 S_OK 阻止瀏覽器禁用控件
}

關鍵點: 使用 `CComPtr` 自動管理引用計數;`try/catch(HRESULT)` 簡化錯誤處理流;失敗時釋放接口但返回 `S_OK` 避免瀏覽器標記控件初始化失敗。

3. 封裝通用調用 JS 函數方法

HRESULT CMyActiveX::CallScriptFunction(LPCOLESTR funcName, VARIANT* args, int argCount, VARIANT* pResult) {
    if (!m_spDoc) return E_UNEXPECTED;

    CComPtr spScript;
    HRESULT hr = m_spDoc->get_Script(&spScript);
    if (FAILED(hr) || !spScript) return hr;

    // 獲取 DISPID
    DISPID dispid;
    CComBSTR bstrName(funcName);
    hr = spScript->GetIDsOfNames(IID_NULL, &bstrName, 1, LOCALE_USER_DEFAULT, &dispid);
    if (FAILED(hr)) return hr;

    // 準備參數 (注意:Invoke 參數順序為逆序,即 args[0] 是最後一個參數)
    DISPPARAMS dp = {0};
    dp.cArgs = argCount;
    dp.rgvarg = new VARIANT[argCount];
    for (int i = 0; i < argCount; i++) {
        dp.rgvarg[i] = args[argCount - 1 - i]; // 逆序複製
    }
    dp.cNamedArgs = 0;

    EXCEPINFO ei = {0};
    UINT nArgErr = 0;
    hr = spScript->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dp, pResult, &ei, &nArgErr);

    delete[] dp.rgvarg;
    return hr;
}

4. 使用示例:調用 `window.calert("Hello from ActiveX")`

void CMyActiveX::NotifyPageReady() {
    VARIANT arg;
    VariantInit(&arg);
    arg.vt = VT_BSTR;
    arg.bstrVal = SysAllocString(L"Hello from ActiveX!");

    VARIANT result;
    VariantInit(&result);
    HRESULT hr = CallScriptFunction(L"calert", &arg, 1, &result);

    VariantClear(&arg);
    VariantClear(&result);
    // 檢查 hr...
}

網頁端配合代碼

<object id="MyActiveX" classid="clsid:YOUR-CLSID-HERE" width="0" height="0"></object>
<script>
    function calert(msg) {
        alert("JS Received: " + msg);
        // 可以在此操作 DOM、更新 UI
    }
    // 頁面加載完成後調用控件方法觸發回調
    window.onload = function() {
        try { MyActiveX.NotifyPageReady(); } catch(e) {}
    };
</script>

常見坑點與解決方案

問題原因解決
SetClientSite 未被調用控件未正確實現 IOleObject 或註冊類別缺少標志ATL 專案確認 IOleObjectImpl 在繼承鏈中;.rgs 註冊含 `Control` `Insertable` 標志
獲取 IWebBrowser2 失敗 (0x80004002)服務鏈路斷裂,常見於非 IE 內核或安全設置阻攔嘗試 `SID_SWebBrowserApp` 直接從 `spSP1` 查詢;檢查站點是否支持 `IServiceProvider`
get_Script 返回 NULL頁面腳本未加載完成或腳本引擎未初始化在 `DocumentComplete` 事件 (沉浸式 `DWebBrowserEvents2`) 後調用;或輪詢重試
Invoke 返回 DISP_E_UNKNOWNNAME函數名拼寫錯誤、大小寫不匹配、函數未定義在全局作用域確認 JS 函數掛載在 `window` 上;使用 `GetIDsOfNames` 返回的 DISPID 調試
事件沉鉤後 JS 調用失效實現 SetClientSite 可能干擾連接點容器調用基類實現:`return IOleObjectImpl<...>::SetClientSite(pClientSite);` 後再執行自定義邏輯

現代替代方案提示

ActiveX 技術已淘汰 (Edge/Chrome/Firefox 不再支持)。新項目建議採用:

  • Native Messaging + WebExtension:瀏覽器擴展與本地宿主進程通信
  • WebAssembly:將 C++ 邏輯編譯為 WASM 直接在頁面運行
  • WebView2 (Edge Chromium):在桌面應用嵌入現代 Webview,雙向通信用 `PostWebMessageAsJson` / `AddHostObjectToScript`

總結

ActiveX 調用 JS 的核心在於 `IOleClientSite → IServiceProvider → IWebBrowser2 → Document → Script → IDispatch::Invoke` 這條完整的 COM 服務查詢鏈。掌握 `SetClientSite` 截獲站點、正確處理接口引用計數、參數逆序傳遞、錯誤碼診斷,即可構建穩健的控件-頁面雙向交互機制。雖技術老舊,但理解其 COM 互操作本質對維護舊系統、學習 WebView2 等現代宿主模型仍有極高參考價值。

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