从 C++ 创建 JavaScript 数组和其他对象
如何从 C++ 代码创建 JavaScript 数组和其他对象,并将它们传递给脚本。
引言
JavaScript 具有一些内置类,如 Object
、Array
和 Date
。人们经常询问如何从 C++ 创建这些类的实例。假设您熟悉 COM,您应该能够理解这个解决方案。
它是如何工作的?
要创建新的实例,您通常会编写如下 JavaScript 代码:
var o = new Array();
如果您在 C++ 应用程序中托管 JScript(通过直接使用脚本宿主或使用 WebBrowser 控件显示嵌入了 JavaScript 代码的网站),您不能直接从 C++ 代码调用 new
。此外,由于您没有 CLSID
,因此无法通过 CoCreateInstance
创建数组。相反,您必须手动执行 JScript 在构造新对象时所做的事情。
当您在 JScript 中使用 new
运算符时,会发生以下情况:
- 调用
new
的对象将被请求一个具有您想要创建实例的类的名称的属性(例如,“Array
”)。 - 返回的属性将被请求
IDispatchEx
接口。 - 在返回的接口上使用
InvokeEx
方法,其中DISPID
为 0,标志为DISPATCH_CONSTRUCT
。这将构造实例并返回新创建的对象。
完成这些步骤后,您将获得您的 Array
。
获取脚本对象
通常有两种情况需要从 C++ 应用程序处理 JavaScript。一种是托管 WebBrowser 控件来显示 HTML 页面,另一种是直接使用 JScript 脚本宿主。在后一种情况下,您会有一个 IActiveScript
指针。只需在该接口上调用 GetScriptDispatch()
即可获取脚本对象。
// pScriptEngine is of type IActiveScript
// SCRIPT_ITEMNAME is the name you specified in IActiveScript::AddNamedItem
// can be NULL to retreive the the object containing all global members
CComPtr<IDispatch> pScriptDisp;
HRESULT hr = pScriptEngine->GetScriptDispatch(SCRIPT_ITEMNAME, &pScriptDisp);
if (FAILED(hr))
return hr;
// pScriptDisp is now the scripting object
如果您托管 WebBrowser 控件,您将拥有一个 IWebBrowser2
指针。请求当前的 IHTMLDocument2
,然后从该文档请求父窗口(IHTMLWindow2
接口)。这将是脚本对象。
// spBrowser is of type IHTMLWindow2
CComPtr<IDispatch> spDoc;
hr = spBrowser->get_Document(&spDoc);
if (FAILED(hr))
return hr;
CComQIPtr<IHTMLDocument2> spHTMLDoc(spDoc);
if (!spHTMLDoc)
return E_NOINTERFACE;
CComPtr<IHTMLWindow2> spWindow;
hr = spHTMLDoc->get_parentWindow(&spWindow);
if (FAILED(hr))
return hr;
CComQIPtr<IDispatch> pScriptDisp(spWindow);
if (!pScriptDisp)
return E_NOINTERFACE;
// pScriptDisp should now be scripting object
获取对象构造函数
在 JavaScript 中使用 new
运算符时,首先会请求在调用 new
的对象中包含的属性。因此,如果您在 HTML 页面的 JavaScript 中说 new Array()
,您请求的属性是 window
对象上的名为“Array”的属性。实际上,JavaScript 中的所有内容都是某个对象的属性。如果您在 HTML 页面中编写以下 JavaScript 代码:
function doSomething()
{
// ...
}
您将向您的 window
对象添加一个名为“doSomething”的 function
类型的属性。
如果您随后通过说 var o = new doSomething();
来构造新实例,您将以一种“特殊方式”调用此属性,这与仅说 var o = doSomething();
不同。后者将获取 doSomething
的 DISPID,并在具有此 DISPID 和 DISPATCH_METHOD
标志的 window 对象上调用 Invoke
。使用 var o = new doSomething();
会查询名为“doSomething
”的**属性**(该属性也是一个对象),然后在此对象上使用 DISPATCH_CONSTRUCT
标志调用 InvokeEx
。但首先,让我们获取名为“Array”的属性。
// get DISPID for "Array"
DISPID did = 0;
LPOLESTR lpNames[] = {L"Array"};
hr = pScriptDisp->GetIDsOfNames(IID_NULL, lpNames, 1,
LOCALE_USER_DEFAULT, &did);
if (FAILED(hr))
return hr;
// invoke pScriptDisp with DISPATCH_PROPERTYGET for "Array"
CComVariant vtRet;
DISPPARAMS params = {0};
CComVariant vtResult;
hr = pScriptDisp->Invoke(did, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET, ¶ms
, &vtResult, NULL, NULL);
if (FAILED(hr))
return hr;
// vtResult should be of type VT_DISPATCH and contain the creator for Array
现在我们有了一个类似 Array
对象创建器的东西。该创建器现在将请求一个新的 Array
。这首先是通过从创建器获取 IDispatchEx
接口来完成的。
// get IDispatchEx on returned IDispatch
CComQIPtr<IDispatchEx> creator(vtResult.pdispVal);
if (!creator)
return E_NOINTERFACE;
IDispatchEx
接口扩展了 IDispatch
,并支持可以动态添加新属性的对象。这正是我们的新属性 doSomething
添加到 window 的方式,通过使用其 IDispatchEx
接口。(有关 IDispatchEx
的详细信息,请参阅 http://msdn.microsoft.com/en-us/library/sky96ah7%28VS.85%29.aspx。)在这里,我们只需要 InvokeEx
方法,它如下所示:
HRESULT InvokeEx(
DISPID id,
LCID lcid,
WORD wFlags,
DISPARAMS *pdp,
VARIANT *pVarRes,
EXCEPINFO *pei,
IServiceProvider *pspCaller
);
使用 id = DISPID_VALUE
和 wFlags = DISPATCH_CONSTRUCT
可以完成构造函数的神奇操作。
// creator is of type IDispatchEx
DISPPARAMS params = {0};
CComVariant vtResult;
HRESULT hr = creator->InvokeEx(DISPID_VALUE, LOCALE_USER_DEFAULT, DISPATCH_CONSTRUCT
, ¶ms, &vtResult, NULL, NULL);
// vtResult should contain the newly created object
如果一切顺利,您现在将获得新创建的对象,无论是内置对象还是您在 JavaScript 中定义的某个对象。是的,当然,您也可以构造在 JS 代码中定义的那些对象。
function MyObject()
{
this.foo = "bar";
}
通过请求名为“MyObject
”的属性。大致就是这样。
哦,对了,您可能想向构造函数传递一些参数。在 InvokeEx
调用中使用 DISPPARAMS
参数来传递参数。请记住,参数必须按从右到左的顺序给出,因此到达构造函数的第一个参数是 DISPPARAMS::rgvarg
中的最后一个。
添加值
如果您创建了新的 Object
或 Array
,您可能想向新对象添加值。这也通过 IDispatchEx
接口完成,使用 GetDispID
和 InvokeEx
。首先,我们需要要添加的属性的名称(对于数组是索引)的 DISPID
。IDispatchEx
提供了 GetDispID
方法来实现这一点。
HRESULT GetDispID(
BSTR bstrName,
DWORD grfdex,
DISPID *pid
);
此属性尚不存在。要创建它,请在 grfdex
中传递 fdexNameEnsure
标志,如果名为 bstrName
的属性尚不存在,它将确保创建该属性。最初,这个新属性将是一个类型为 VT_EMPTY
的 VARIANT
。在获得新的 DISPID
后,您只需调用 InvokeEx
并使用 DISPATCH_PROPERTYPUT
来添加值。
// theObject is of type IDispatchEx and is the js-Array
DISPID did = 0;
hr = theObject->GetDispID(CComBSTR(L"0"), fdexNameEnsure, &did);
if (FAILED(hr))
return hr;
CComVariant data(_T("bar"));
DISPID namedArgs[] = {DISPID_PROPERTYPUT};
DISPPARAMS params = {&data, namedArgs, 1, 1};
hr = theObject->InvokeEx(did, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT, ¶ms,
NULL, NULL, NULL);
正如您所看到的,这里使用了 DISPPARAMS
的 rgdispidNamedArgs
成员;否则,调用将因 DISP_E_PARAMNOTOPTIONAL
而失败。
总而言之,我们的 PutJsArray
现在看起来如下。我使用 CAtlArray<CComVariant>
(已键入为 CAtlVariantArray
)来传递值,并使用一个名为 lpsName
的 JS 函数来将新创建的数组传递给 JavaScript。
HRESULT CJsArrayView::PutJsArray(LPOLESTR lpsName, CAtlVariantArray& data)
{
// get script dispatch
CComPtr<IDispatch> scriptDispatch;
HRESULT hr = GetScriptDispatch(&scriptDispatch);
if (FAILED(hr))
return hr;
ATLASSERT(scriptDispatch);
// get DISPID for "Array"
DISPID did = 0;
LPOLESTR lpNames[] = {L"Array"};
hr = scriptDispatch->GetIDsOfNames(IID_NULL, lpNames, 1,
LOCALE_USER_DEFAULT, &did);
if (FAILED(hr))
return hr;
// invoke scriptdispatch with DISPATCH_PROPERTYGET for "Array"
CComVariant vtRet;
DISPPARAMS params = {0};
CComVariant vtResult;
hr = scriptDispatch->Invoke(did, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET, ¶ms, &vtResult, NULL, NULL);
if (FAILED(hr))
return hr;
// check result: should be a VT_DISPATCH
if ((VT_DISPATCH != vtResult.vt) || (NULL == vtResult.pdispVal))
return DISP_E_TYPEMISMATCH;
// get IDispatchEx on returned IDispatch
CComQIPtr<IDispatchEx> prototype(vtResult.pdispVal);
if (!prototype)
return E_NOINTERFACE;
// call InvokeEx with DISPID_VALUE
// and DISPATCH_CONSTRUCT to construct new array
vtResult.Clear();
hr = prototype->InvokeEx(DISPID_VALUE, LOCALE_USER_DEFAULT,
DISPATCH_CONSTRUCT, ¶ms, &vtResult, NULL, NULL);
if (FAILED(hr))
return hr;
// vtresult should contain the new array now.
if ((VT_DISPATCH != vtResult.vt) || (NULL == vtResult.pdispVal))
return DISP_E_TYPEMISMATCH;
// get IDispatchEx on returned IDispatch
CComQIPtr<IDispatchEx> theObject(vtResult.pdispVal);
if (!theObject)
return E_NOINTERFACE;
// add values by invoking InvokeEx
CString sName;
for(size_t n = 0; n < data.GetCount(); n++)
{
sName.Format(_T("%i"), n);
hr = theObject->GetDispID(CComBSTR(sName), fdexNameEnsure, &did);
if (FAILED(hr))
break;
DISPID namedArgs[] = {DISPID_PROPERTYPUT};
DISPPARAMS p = {&data[n], namedArgs, 1, 1};
hr = theObject->InvokeEx(did, LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYPUT, &p, NULL, NULL, NULL);
if (FAILED(hr))
break;
}
// now call the js-function in lpsName with the array as parameter
params.cArgs = 1;
params.rgvarg = &vtResult;
hr = CallJs(scriptDispatch, lpsName, ¶ms);
return hr;
}
示例代码
Zip 文件包含一个基于 WTL 和 ATL 的示例项目。所有重要的代码都包含在视图类 CJsArrayView
中。它可以轻松地更改为 MFC,或用于 MFC 或 ATL,或改编为创建除 Array
或 Object
之外的其他对象。