菜鸟与 cef 的邂逅之旅(三):Cef3 中 C++ 与 JavaScript 的互相调用

一、引言

我们要实现一个强大的浏览器控件,必须要能够实现 C++ 与 JavsScript 的互相调用。

于是,在我研究了:

菜鸟与 cef 的邂逅之旅(一):cef 源码获取与编译
菜鸟与 cef 的邂逅之旅(二):Soui 中接入 Cef3 的实现

这些主题之后,研究 Cef3 中 C++ 与 JavaScript 之间的互相调用也是顺水推舟的事情了。

这里,我依然参照了大神 蓝先生 的示例代码(根据此示例代码在上一篇博客中自行建立的项目),认真研究了下如何在基于 Soui 界面库的 Cef3 机制中实现 C++ 和 JavaScript 互相调用的方法。

二、Cef3: C++ 调用 JavaScript

C++ 调用 JavaScript 可以说是比较简单的。看一下 Cef3 官方文档:

CefBrowser 和 CefFrame 对象被用来发送命令给浏览器以及在回调函数里获取状态信息。每个 CefBrowser 对象包含一个主 CefFrame 对象,主 CefFrame 对象代表页面的顶层 frame;同时每个 CefBrowser 对象可以包含零个或多个的 CefFrame 对象,分别代表不同的子 Frame。

CefBrowser 和 CefFrame 对象在 Browser 进程和 Render 进程都有对等的代理对象。

根据文档的了解,我们发现,我们可以通过 CBrowser 的实例得到其顶层 CefFrame 对象,然后通过它来调用 JavaScript 代码。

以下是大神 蓝先生封装的 SCefWebView 控件中的 ExecJavaScript 方法:

void SCefWebView::ExecJavaScript(const SStringW& js)
{
    if (!m_pBrowserHandler)
    {
        return;
    }

    CefRefPtr<CefBrowser> pb = m_pBrowserHandler->GetBrowser();
    if ( pb.get() )
    {   
        CefRefPtr<CefFrame> frame = pb->GetMainFrame();
        if ( frame )
        {
            frame->ExecuteJavaScript((LPCWSTR)js, L"", 0);
        }
    }
}

我们了解了 C++ 调用 JavaScript 方法的原理之后,只需要在指定的事件里找到这个 Cef 控件,通过控件调用 ExecJavaScript 方法即可:

// 执行 JavaScript 代码
bool CMainDlg::OnRunJavaScript(SOUI::EventArgs *pEvt)
{
    SOUI::SEdit *pEditURL = FindChildByName2<SOUI::SEdit>(L"edit_input_url");
    SOUI::SCefWebView *pCefBrowser = FindChildByName2<SOUI::SCefWebView>(L"cef_browser");
    if (pEditURL && pCefBrowser) {
        SStringW strJs = pEditURL->GetWindowText();
        pCefBrowser->ExecJavaScript(strJs);
    }
    return true;
}

这里,用户通过在地址栏输入 JavaScript 代码之后,点击 Run Js 按钮之后执行该处代码(具体绑定过程看 SOUI 教程)。

以下是运行结果:

C++ Call Js

三、Cef3: JavaScript 调用 C++

上面我们已经实现了 C++ 调用 JavaScript 的功能。

而如何实现 JavaScript 对于 C++ 的调用呢?

通过认真参看代码逻辑,我了解到了这样的实现:

1. 定义 SOUI::SCefWebView 的窗口通知事件

以下是文件 ExtendEvents.h 的内容:

namespace SOUI
{
#define EVT_CEFWEBVIEW_BEGIN        (EVT_EXTERNAL_BEGIN + 900)
#define EVT_WEBVIEW_NOTIFY          (EVT_CEFWEBVIEW_BEGIN+0)

    class EventWebViewNotify : public TplEventArgs<EventWebViewNotify>
    {
        SOUI_CLASS_NAME(EventWebViewNotify, L"on_webview_notify")
    public:
        EventWebViewNotify(SObject *pSender) :TplEventArgs<EventWebViewNotify>(pSender) {}
        enum { EventID = EVT_WEBVIEW_NOTIFY };

        SStringW         MessageName;
        SArray<SStringW> Arguments;
    };


};// namespace SOUI

这里定义了通知消息类型 EventWebViewNotify,其中定义了通知事件 Id 为 EVT_WEBVIEW_NOTIFY,并且声明了两个成员变量,一个用于存储响应 JavaScript 函数名称的 MessageName,另一个则是存储 JavaScript 函数调用的参数的数组变量。

2. 子进程 WebProcess 中初始化声明全局函数(JavaScript 调用),JavaScript 可以调用该函数达到调用 C++ 函数的目的

以下是 SubProcessClientApp.cpp 中对于 JavaScript 可调用全局函数的定义:

void SubProcessClientApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefRefPtr<CefV8Context> context)
{
    CefRefPtr<CefV8Value> object = context->GetGlobal();

    CefRefPtr<CefV8Handler> handler = new HtmlEventHandler();
    CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction("HandleEvent", handler);
    object->SetValue("HandleEvent", func, V8_PROPERTY_ATTRIBUTE_NONE);
}

可以看到,我们在其中将这个 JavaScript 可以全局调用的函数命名为了 HandleEvent,那么之后我们在 JavaScript 代码中就要使用这个函数进行调用 C++ 的目的。

以下是 HTMLEventHandler.cpp 中对于 JavaScript 执行函数的定义:

bool HtmlEventHandler::Execute(const CefString& name,
    CefRefPtr<CefV8Value> object,
    const CefV8ValueList& arguments,
    CefRefPtr<CefV8Value>& retval,
    CefString& exception)
{
    if (name != "HandleEvent" || arguments.size() == 0)
    {
        return true;
    }

    CefRefPtr<CefBrowser> browser =
        CefV8Context::GetCurrentContext()->GetBrowser();

    CefRefPtr<CefProcessMessage> message =
        CefProcessMessage::Create(arguments[0]->GetStringValue());

    message->GetArgumentList()->SetSize(arguments.size() - 1);
    for (size_t i = 1; i < arguments.size(); ++i)
    {
        message->GetArgumentList()->SetString(i - 1, arguments[i]->GetStringValue());
    }

    browser->SendProcessMessage(PID_BROWSER, message);

    return false;
}

可以看到,同样的,我们仔细甄选了 HandleEvent 的函数调用信息,然后将其发送给了我们的浏览器实例 Browser 进程,之后我们在 Browser 进程中的 BrowserHandler::OnProcessMessageReceived() 函数中可以接收到该调用信息,该函数又将信息发送给了子类实现 SCefWebView::OnBrowserMessage() 函数中:

bool SCefWebView::OnBrowserMessage(CefRefPtr<CefBrowser> browser,
        CefProcessId source_process,
        CefRefPtr<CefProcessMessage> message)
    {
        EventWebViewNotify evt(this);

        evt.MessageName = message->GetName().ToWString().c_str();
        CefRefPtr<CefListValue> arg = message->GetArgumentList();

        for (int i = 0; i < arg->GetSize(); ++i)
        {
            SStringW str = arg->GetString(i).ToWString().c_str();
            evt.Arguments.Add(str);
        }

        return !!FireEvent(evt);
    }

可以看到,我们将收到的 JavaScript 调用信息转化成了我们之前定义的 EventWebViewNotify 通知类型,这里经过一系列的参数处理,然后发送响应到了 UI 层。

3. 使用我们之前定义的全局函数 HandleEvent 实现 JavaScript 对于 C++ 的调用:

以下是 test.htm 的实现:

<html>
<head>
    <meta charset="utf-8" />
</head>

<script>
    function callCpp() {
        window.HandleEvent("ClickButton", document.getElementById("input_text").value);
    }
</script>

<body>
    <input type="text" id="input_text" stype="width:200px;height:50" value="hello cef3" />
    <button type="button" onclick="callCpp()">Call C++</button>
</body>

</html>

可以看到,我们点击 Call C++ 的 button,响应了 callCpp() 的 JavaScript 函数,在这个 JavaScript 函数中,我们调用了全局函数 HandleEvent,并往里面传递了函数名 ClickButton,还传递了页面元素 input_text 的值。

3. 绑定 cef 浏览器控件响应自定义的 EVT_WEBVIEW_NOTIFY 消息,将接收到的 JavaScript 函数传递的函数名和参数名显示出来:

以下是 SOUI 中绑定浏览器消息的实现:

SOUI::SCefWebView *pCefBrowser = FindChildByName2<SOUI::SCefWebView>(L"cef_browser");
    if (pCefBrowser) {
        pCefBrowser->GetEventSet()->subscribeEvent(EVT_WEBVIEW_NOTIFY, Subscriber(&CMainDlg::OnWebViewNotify, this));
    }

以下是界面层中显示 JavaScript 调用信息的实现:

// 获得 Js 的通知信息,显示结果
bool CMainDlg::OnWebViewNotify(SOUI::EventArgs *pEvt)
{
    SOUI::EventWebViewNotify *pev = (SOUI::EventWebViewNotify*)pEvt;
    SStringW args;
    for (size_t i = 0; i < pev->Arguments.GetCount(); ++i) {
        if (i != 0) {
            args += _T(", ");
        }
        args += pev->Arguments[i];
    }

    SOUI::SMessageBox(m_hWnd, args, pev->MessageName, MB_OK);
    return true;
}

最后,让我们看看最后的运行结果吧:

Js Call C++

^_^
完结!撒花!

四、总结

基于 SOUI 的 Cef3 中 C++ 与 JavaScript 的互相调用其实并不复杂,其根本原理还是 Browser 进程与 Render 进程的互相通信。

其中 Browser 进程向 Render 进程发送消息,Render 进程响应,这就是 C++ 调用 JavaScript;
同样的,Render 进程向 Browser 进程发送消息,Browser 进程响应,这就是 JavaScript 调用 C++;
当然内部有着复杂的实现,不过原理就是这样。

最后附上本博客的实验代码:

wangying2016/Cef3-Soui-Demo

Cef3 之路还很漫长,想要灵活运用还有很长的距离要走:

最后再次感谢大神蓝先生对于我的帮助

To be Stronger!

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页