ActiveX 控件嵌入網頁時,常需雙向通訊:網頁 JS 調用控件方法,控件反向調用 JS 函數。本文詳解 IObjectSafety、IServiceProvider、IWebBrowser2、IDispatch 關鍵接口鏈,提供完整 SetClientSite 獲取 window/document、get_Script 獲取腳本引擎、GetIDsOfNames/Invoke 調用 JS 函數的全流程代碼。
IOleClientSite (容器提供)
→ QueryInterface(IID_IServiceProvider)
→ QueryService(SID_STopLevelBrowser, IID_IServiceProvider)
→ QueryService(SID_SWebBrowserApp, IID_IWebBrowser2) → m_spBrowser
→ get_Document() → m_spDoc (IHTMLDocument2)
→ get_Script() → pScript (IDispatch, 對應 window 對象)
→ GetIDsOfNames("jsFuncName") → DISPID
→ Invoke(DISPID, DISPATCH_METHOD, ...) → 執行 JS
// 控件接口需包含 SetClientSite (IOleObject 標準方法)
interface IMyActiveX : IDispatch {
[id(1)] HRESULT SetClientSite(IOleClientSite* pClientSite);
// 其他業務方法...
};
STDMETHODIMP CMyActiveX::SetClientSite(IOleClientSite* pClientSite) {
HRESULT hr = S_OK;
// 釋放舊引用
m_spBrowser.Release();
m_spDoc.Release();
if (!pClientSite) return S_OK; // 容器釋放站點
CComPtr spSP, spSP2;
try {
// 1. 從 ClientSite 獲取 IServiceProvider
hr = pClientSite->QueryInterface(IID_IServiceProvider, (void**)&spSP);
if (FAILED(hr)) throw hr;
// 2. 獲取頂級瀏覽器的 ServiceProvider
hr = spSP->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. 獲取 Document (IHTMLDocument2)
hr = m_spBrowser->get_Document((IDispatch**)&m_spDoc);
if (FAILED(hr)) throw hr;
} catch (HRESULT err) {
hr = err;
// 可選:記錄日誌或 MessageBox
}
return hr; // 即使失敗也返回 S_OK 避免容器報錯,內部標記失敗狀態
}
HRESULT CMyActiveX::CallJScriptFunction(LPCOLESTR pszFuncName, VARIANT* pVarResult, int nArgs, ...) {
if (!m_spDoc) return E_UNEXPECTED; // 未初始化
CComPtr spScript;
HRESULT hr = m_spDoc->get_Script(&spScript);
if (FAILED(hr) || !spScript) return hr;
// 1. 獲取函數 DISPID
CComBSTR bstrName(pszFuncName);
DISPID dispid;
hr = spScript->GetIDsOfNames(IID_NULL, &bstrName, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
if (FAILED(hr)) return hr; // 函數不存在
// 2. 準備參數 (可變參數列表)
DISPPARAMS dp = {0};
dp.cArgs = nArgs;
if (nArgs > 0) {
dp.rgvarg = new VARIANT[nArgs];
va_list vl;
va_start(vl, nArgs);
// 注意:COM 參數順序為逆序 (最右參數在 rgvarg[0])
for (int i = nArgs - 1; i >= 0; --i) {
VARIANTARG* pArg = &dp.rgvarg[nArgs - 1 - i];
VariantInit(pArg);
// 簡化:假設調用者傳入已初始化的 VARIANT*
// 實際應根據類型複製: *pArg = *va_arg(vl, VARIANT*);
// 此處演示傳 BSTR:
CComBSTR bstrParam = va_arg(vl, BSTR);
pArg->vt = VT_BSTR;
pArg->bstrVal = bstrParam.Detach();
}
va_end(vl);
}
dp.cNamedArgs = 0;
// 3. 執行調用
EXCEPINFO excep = {0};
UINT nArgErr = 0;
CComVariant varResult;
hr = spScript->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_METHOD, &dp, &varResult, &excep, &nArgErr);
// 4. 清理參數數組
if (dp.rgvarg) {
for (int i = 0; i < nArgs; ++i) VariantClear(&dp.rgvarg[i]);
delete[] dp.rgvarg;
}
if (pVarResult) *pVarResult = varResult.Detach();
return hr;
}
// 控件內部某事件中 (如按鈕點擊)
void CMyActiveX::OnInternalButtonClick() {
// 調用網頁中定義的 function myAlert(msg) { alert(msg); }
CComVariant varResult;
HRESULT hr = CallJScriptFunction(L"myAlert", &varResult, 1, CComBSTR(L"來自 ActiveX 的消息!"));
if (FAILED(hr)) {
ATLTRACE("調用 JS 失敗: 0x%08X\n", hr);
}
}
<object id="myActiveX" classid="clsid:YOUR-CLSID-HERE" width="300" height="200">
<param name="DataPath" value="data.xml" />
</object>
<script type="text/javascript">
// 必須定義供控件調用的函數
function myAlert(msg) {
alert("JS 收到: " + msg);
document.getElementById("log").innerText += msg + "\n";
}
// 調用控件方法 (正向)
function testCallControl() {
try {
myActiveX.SomeMethod(123, "參數");
} catch (e) {
console.error("控件調用失敗", e);
}
}
</script>
<input type="button" value="測試調用控件" onclick="testCallControl()" />
<pre id="log"></pre>
| 問題 | 原因 | 解決方案 |
|---|---|---|
| SetClientSite 中 QueryService 失敗 | 容器不支持 SID_STopLevelBrowser (非 IE 宿主) | 檢查宿主類型;HTA/IE/WebBrowser 控件支持;Chrome/Firefox 不支持 ActiveX |
| get_Script 返回 NULL | 文檔未就緒或無腳本引擎 | 在 DocumentComplete 事件後調用;確保頁面有
Copyright © 程式碼之家 All Rights Reserved
|