在網頁中嵌入 ActiveX 控件 (COM 對象) 時,控件往往需要回調頁面腳本函數 (如通知完成、觸發 UI 更新)。由於控件運行在瀏覽器進程中,需跨越 COM 邊界獲取 `window` 對象的 `IDispatch`,進而調用 JS 函數。本文梳理完整調用鏈路並提供生產級代碼模式。
IOleObject::SetClientSite(IOleClientSite*),瀏覽器調用此方法傳遞站點指針。IOleClientSite → IServiceProvider → SID_STopLevelBrowser → IServiceProvider → SID_SWebBrowserApp → IWebBrowser2IWebBrowser2::get_Document → IHTMLDocument2 → get_Script → IDispatch (腳本引擎)IDispatch::GetIDsOfNames → Invoke(DISPATCH_METHOD)// .h 文件
STDMETHOD(SetClientSite)(IOleClientSite* pClientSite);
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` 避免瀏覽器標記控件初始化失敗。
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;
}
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 不再支持)。新項目建議採用:
ActiveX 調用 JS 的核心在於 `IOleClientSite → IServiceProvider → IWebBrowser2 → Document → Script → IDispatch::Invoke` 這條完整的 COM 服務查詢鏈。掌握 `SetClientSite` 截獲站點、正確處理接口引用計數、參數逆序傳遞、錯誤碼診斷,即可構建穩健的控件-頁面雙向交互機制。雖技術老舊,但理解其 COM 互操作本質對維護舊系統、學習 WebView2 等現代宿主模型仍有極高參考價值。