views:

268

answers:

1

I have embedded a web browser control in my c++ application. I want javascript running in the web browser control to be able to call a c++ function/method.

I have found mentions of three ways to do this:

  1. Implement an ActiveX component that acts as a middle man. (Implementation details here: http://blogs.msdn.com/b/nicd/archive/2007/04/18/calling-into-your-bho-from-a-client-script.aspx)
  2. Use window.external. (Also discussed in the link above, but no implementation provided)
  3. Add a custom object to the window object

I want to go with the third option, but I haven't found any working examples on how to do that. Can someone please show me how to do it, or link to a working example on the net somewhere.

The closest to an example that I have found is the first reply by Igor Tandetnik in a thread in the webbrowser_ctl news group. But I'm afraid I need more help than that.

I'm embedding an IWebBrowser2 control and I am not using MFC, ATL or WTL.

EDIT:

Going by the pseudo-code given by Igor in the thread I linked earlier, and code found in the codeproject article "Creating JavaScript arrays and other objects from C++" I've produced some code.

void WebForm::AddCustomObject(IDispatch *custObj, std::string name)
{
    IHTMLDocument2 *doc = GetDoc();
    IHTMLWindow2 *win = NULL;
    doc->get_parentWindow(&win);

    if (win == NULL) {
        return;
    }

    IDispatchEx *winEx;
    win->QueryInterface(&winEx);

    if (winEx == NULL) {
        return;
    }

    int lenW = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, name.c_str(), -1, NULL, 0);
    BSTR objName = SysAllocStringLen(0, lenW);
    MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, name.c_str(), -1, objName, lenW);

    DISPID dispid; 
    HRESULT hr = winEx->GetDispID(objName, fdexNameEnsure, &dispid);

    SysFreeString(objName);

    if (FAILED(hr)) {
        return;
    }

    DISPID namedArgs[] = {DISPID_PROPERTYPUT};
    DISPPARAMS params;
    params.rgvarg = new VARIANT[1];
    params.rgvarg[0].pdispVal = custObj;
    params.rgvarg[0].vt = VT_DISPATCH;
    params.rgdispidNamedArgs = namedArgs;
    params.cArgs = 1;
    params.cNamedArgs = 1;

    hr = winEx->InvokeEx(dispid, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT, &params, NULL, NULL, NULL); 

    if (FAILED(hr)) {
        return;
    }
}

The code above runs all the way through, so everything looks alright that far.

I call AddCustomObject when I receive the DISPID_NAVIGATECOMPLETE2 DWebBrowserEvents2 event passing this as *custObj:

class JSObject : public IDispatch {
private:
    long ref;

public:
    // IUnknown
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv);
    virtual ULONG STDMETHODCALLTYPE AddRef();
    virtual ULONG STDMETHODCALLTYPE Release();

    // IDispatch
    virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *pctinfo);
    virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid,
        ITypeInfo **ppTInfo);
    virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid,
        LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);
    virtual HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid,
        LCID lcid, WORD wFlags, DISPPARAMS *Params, VARIANT *pVarResult,
        EXCEPINFO *pExcepInfo, UINT *puArgErr);
};

Noteworthy implementations might be

HRESULT STDMETHODCALLTYPE JSObject::QueryInterface(REFIID riid, void **ppv)
{
    *ppv = NULL;

    if (riid == IID_IUnknown || riid == IID_IDispatch) {
        *ppv = static_cast<IDispatch*>(this);
    }

    if (*ppv != NULL) {
        AddRef();
        return S_OK;
    }

    return E_NOINTERFACE;
}

and

HRESULT STDMETHODCALLTYPE JSObject::Invoke(DISPID dispIdMember, REFIID riid,
    LCID lcid, WORD wFlags, DISPPARAMS *Params, VARIANT *pVarResult,
    EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
    MessageBox(NULL, "Invoke", "JSObject", MB_OK);
    return DISP_E_MEMBERNOTFOUND;
}

Unfortunately I never get the "Invoke" message box when I try to use the "JSObject" object from the javascript code.

JSObject.randomFunctionName();  // This should give me the c++ "Invoke" message
                                // box, but it doesn't

EDIT 2:

I implemented GetIDsOfNames like so:

HRESULT STDMETHODCALLTYPE JSObject::GetIDsOfNames(REFIID riid,
    LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
{
    HRESULT hr = S_OK;

    for (UINT i = 0; i < cNames; i++) {
        std::map<std::wstring, DISPID>::iterator iter = idMap.find(rgszNames[i]);
        if (iter != idMap.end()) {
            rgDispId[i] = iter->second;
        } else {
            rgDispId[i] = DISPID_UNKNOWN;
            hr = DISP_E_UNKNOWNNAME;
        }
    }

    return hr;
}

and this is my constructor

JSObject::JSObject() : ref(0)
{
    idMap.insert(std::make_pair(L"execute", DISPID_USER_EXECUTE));
    idMap.insert(std::make_pair(L"writefile", DISPID_USER_WRITEFILE));
    idMap.insert(std::make_pair(L"readfile", DISPID_USER_READFILE));
}

with the DISPID_USER_* constants defined as private class members

class JSObject : public IDispatch {
private:
    static const DISPID DISPID_USER_EXECUTE = DISPID_VALUE + 1;
    static const DISPID DISPID_USER_WRITEFILE = DISPID_VALUE + 2;
    static const DISPID DISPID_USER_READFILE = DISPID_VALUE + 3;

    // ...
};

EDIT 3, 4 and 5:

Moved to a separate question

EDIT 6:

Made a separate question out of the "returning a string" edits. That way I can accept Georg's reply as that answers the original question.

+2  A: 

You need to implement GetIDsOfNames() to do something sensible as that function will be called by client code before Invoke().
If you have your interfaces in a type library see here for an example. If you want to use late-binding instead, you can use DISPIDs greater DISPID_VALUE and less than 0x80010000 (all values <= 0 and in the range 0x80010000 through 0x8001FFFF are reserved):

HRESULT GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, 
                      LCID lcid, DISPID *rgDispId)
{
    HR hr = S_OK;
    for (UINT i=0; i<cNames; ++i) {
        if (validName(rgszNames)) {
            rgDispId[i] = dispIdForName(rgszNames);
        } else {
            rgDispId[i] = DISPID_UNKNOWN;
            hr = DISP_E_UNKNOWNNAME;
        }
    }
    return hr;
}

HRESULT Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, 
               DISPPARAMS *Params, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, 
               UINT *puArgErr)
{
    if (wFlags & DISPATCH_METHOD) {
       // handle according to DISPID ...
    }

    // ...

Note that the DISPIDs are not supposed to change suddenly, so e.g. a static map or constant values should be used.

Georg Fritzsche
That example, and other examples I've found, make use of the DispGetIDsOfNames function that needs an "ITypeInfo" pointer. What is this ITypeInfo thing, and how do I create/get one I can use in my case?
Tobbe
The IDs (DISPID) I return in GetIDsOfNames, are they only used in Invoke()? Or are they used in more places. What I really want to know is if I can just make them up myself, or if I have to use something like GetDispID to generate them for me. (Using made up/random IDs seems to work, but I just want to know if doing that is a bad idea or not)
Tobbe