65.9K
CodeProject 正在变化。 阅读更多。
Home

从 C++ 创建 JavaScript 数组和其他对象

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.12/5 (11投票s)

2010年6月21日

CPOL

5分钟阅读

viewsIcon

73223

downloadIcon

1182

如何从 C++ 代码创建 JavaScript 数组和其他对象,并将它们传递给脚本。

引言

JavaScript 具有一些内置类,如 ObjectArrayDate。人们经常询问如何从 C++ 创建这些类的实例。假设您熟悉 COM,您应该能够理解这个解决方案。

它是如何工作的?

要创建新的实例,您通常会编写如下 JavaScript 代码:

var o = new Array();

如果您在 C++ 应用程序中托管 JScript(通过直接使用脚本宿主或使用 WebBrowser 控件显示嵌入了 JavaScript 代码的网站),您不能直接从 C++ 代码调用 new。此外,由于您没有 CLSID,因此无法通过 CoCreateInstance 创建数组。相反,您必须手动执行 JScript 在构造新对象时所做的事情。

当您在 JScript 中使用 new 运算符时,会发生以下情况:

  1. 调用 new 的对象将被请求一个具有您想要创建实例的类的名称的属性(例如,“Array”)。
  2. 返回的属性将被请求 IDispatchEx 接口。
  3. 在返回的接口上使用 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, &params
  , &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_VALUEwFlags = DISPATCH_CONSTRUCT 可以完成构造函数的神奇操作。

// creator is of type IDispatchEx
DISPPARAMS params = {0};
CComVariant vtResult;
HRESULT hr = creator->InvokeEx(DISPID_VALUE, LOCALE_USER_DEFAULT, DISPATCH_CONSTRUCT
  , &params, &vtResult, NULL, NULL);
// vtResult should contain the newly created object

如果一切顺利,您现在将获得新创建的对象,无论是内置对象还是您在 JavaScript 中定义的某个对象。是的,当然,您也可以构造在 JS 代码中定义的那些对象。

function MyObject()
{
  this.foo = "bar";
}

通过请求名为“MyObject”的属性。大致就是这样。

哦,对了,您可能想向构造函数传递一些参数。在 InvokeEx 调用中使用 DISPPARAMS 参数来传递参数。请记住,参数必须按从右到左的顺序给出,因此到达构造函数的第一个参数是 DISPPARAMS::rgvarg 中的最后一个。

添加值

如果您创建了新的 ObjectArray,您可能想向新对象添加值。这也通过 IDispatchEx 接口完成,使用 GetDispIDInvokeEx。首先,我们需要要添加的属性的名称(对于数组是索引)的 DISPIDIDispatchEx 提供了 GetDispID 方法来实现这一点。

HRESULT GetDispID(
   BSTR bstrName,
   DWORD grfdex,
   DISPID *pid
);

此属性尚不存在。要创建它,请在 grfdex 中传递 fdexNameEnsure 标志,如果名为 bstrName 的属性尚不存在,它将确保创建该属性。最初,这个新属性将是一个类型为 VT_EMPTYVARIANT。在获得新的 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, &params,
  NULL, NULL, NULL);

正如您所看到的,这里使用了 DISPPARAMSrgdispidNamedArgs 成员;否则,调用将因 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, &params, &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, &params, &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, &params);

  return hr;
}

示例代码

Zip 文件包含一个基于 WTL 和 ATL 的示例项目。所有重要的代码都包含在视图类 CJsArrayView 中。它可以轻松地更改为 MFC,或用于 MFC 或 ATL,或改编为创建除 ArrayObject 之外的其他对象。

© . All rights reserved.