在上一个章节《QT集成CEF10-JavaScript与C++互调》 中,JavaScript与C++之间已经能够相互调用了,但是它们只能在renderer进程中进行通信,不能跨进程通信。当JavaScript 也可以与renderer进行实现异步通信,可以参考上一章节中的sayHello 实现.
本章节将通过一个 使用JavaScript读取本地文本文件的案例来探索在CEF中如何实现JavaScript与C++之间跨进程的异步调用,关于CEF的进程间通信,在《QT集成CEF08-多进程通信》已经介绍过,实现本案例就需要用到进程间的通信。收发进程间的消息主要靠以下几个接口实现:
-
renderer进程
发消息通过 CEFFrame::SendProcessMessage 函数来发送消息
收消息通过 CefRenderProcessHandler::OnProcessMessageReceived 方法 来接收消息
-
browser进程
发消息通过 CEFFrame::SendProcessMessage 函数来发送消息
收消息通过 CEFClient::OnProcessMessageReceived 函数来接收消息
1. 基本思路整理
我们要实现的是通过JavaScript调用,打开文件选择对话框,选择文本文件后读取文件中的内容交给JavaScript,JavaScript将内容显示在网页上。
JavaScript 要打开本地对话框,在renderer进程中是无法实现的,需要:
- 将JavaScript调用消息发送给browser进程
- browser进程收到消息后发送QT信号给主窗口
- 主窗口打开文件选择对话框,选择文件后读取文件内容
- 将文件内容通过消息回传给renderer进程
- renderer进程调用JavaScript 注册的回调函数
- JavaScript在回调函数中接收文本内容后,将内容渲染到网页中。
为了简化这种进程间通信开发,从1574版本开始,CEF提供了在Render进程执行的JavaScript和在Browser进程执行的C++代码之间同步通信的转发器,我暂且叫他消息路由(MessageRouter),其中用到的类分布在两侧:
- browser进程侧:
- 消息路由器: CefMessageRouterBrowserSide
- 消息处理器: CefMessageRouterBrowserSide::Handler
- render进程侧:
- 消息路由器: CefMessageRouterRendererSide
通过配置,会为JavaScript window对象绑定一个 cefQuery函数, JavaScript通过调用这个函数来发起消息:
// 发起一个消息,返回消息ID
var request_id = window.cefQuery({
request: 'my_request', // 消息名称
persistent: false, // 消息是否要持久化,就是将消息保存起来
onSuccess: function(response) {}, // 成功情况下的回调, response 为C++ 回传的数据
onFailure: function(error_code, error_message) {} // 失败情况下的回调,error_code 为错误编码,error_message 为错误消息
});
// 通过消息ID取消消息
window.cefQueryCancel(request_id);
每个消息在传递的时候,都携带了以下几个内容:
- CefBrowser 对应浏览器对象
- CefFrame 对应网页中的frame,因为一个网页中如果使用了iframe,就会有多个frame,这个对象指定了具体是哪个frame中发送的消息
- query_id 一个64位的整数,其实就是消息ID。每个消息都有唯一的消息ID。这个query_id 还是比较重要的,因为消息传递过程本身就是异步的,并非同步调用。我们需要将 query_id 和相关联的JavaScript注册的回调暂存起来, 消息在回传的时候,通过 query_id 能够找到JavaScript 注册的回调函数,然后执行这个回调函数将数据传给JavaScript。
- request 消息的名称,一个字符串
- callback JavaScript注册的回调函数
# 消息处理器源码
class Handler {
public:
typedef CefMessageRouterBrowserSide::Callback Callback;
///
// Executed when a new query is received. |query_id| uniquely identifies the
// query for the life span of the router. Return true to handle the query
// or false to propagate the query to other registered handlers, if any. If
// no handlers return true from this method then the query will be
// automatically canceled with an error code of -1 delivered to the
// JavaScript onFailure callback. If this method returns true then a
// Callback method must be executed either in this method or asynchronously
// to complete the query.
///
virtual bool OnQuery(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64 query_id,
const CefString& request,
bool persistent,
CefRefPtr<Callback> callback) {
return false;
}
///
// Executed when a query has been canceled either explicitly using the
// JavaScript cancel function or implicitly due to browser destruction,
// navigation or renderer process termination. It will only be called for
// the single handler that returned true from OnQuery for the same
// |query_id|. No references to the associated Callback object should be
// kept after this method is called, nor should any Callback methods be
// executed.
///
virtual void OnQueryCanceled(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64 query_id) {}
virtual ~Handler() {}
};
思路介绍完了,下面就从JavaScript 代码开始,实现本地文本文件的读取。本节代码参考官方文档
2. JavaScript中发起消息
首先是HTML页面:
<div>
<button id="btnReadTextFile">读取文本文件</button>
</div>
<div id="textFileContent" style="border:1px solid #808080;height:400px;overflow-y:auto">
</div>
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/index.js"></script>
javaScript, 发起消息调用, 消息名称为MSG_READ_TEXT_FILE
,后端 c++会用到。这里很简单,就是将消息发出去,然后等待 c++端回调接收数据,然后将接收的数据渲染到 div 中。
window.onload = () => {
console.log(window.cefQuery);
$("#btnReadTextFile").click(() => {
window.cefQuery({
request: 'MSG_READ_TEXT_FILE', // 消息名称
onSuccess: function (response) {
$("#textFileContent").text(response); // 将结果放到div中
},
onFailure: function (error_code, error_message) {
$("#textFileContent").text("错误:" + error_code + " " + error_message);
}
});
});
}
3. renderer侧
renderer侧加入 消息路由很简单:
-
在
OnWebKitInitialized
函数中创建路由对象 -
在
OnContextCreated
函数中注册 window.cefQuery -
在
OnContextReleased
函数中释放路由对象 -
在
OnProcessMessageReceived
函数中接收 browser进程发送的消息代码很简单,只需要调用路由对象对应的方法即可:
// QyAppRenderer.h
// ...省略代码
// 引入消息路由组件
#include "include/wrapper/cef_message_router.h"
class QyAppRenderer :public CefApp, public CefRenderProcessHandler {
public
// ...省略代码
void OnWebKitInitialized() OVERRIDE;
void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) OVERRIDE;
void OnContextReleased(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) OVERRIDE;
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message);
private:
// 消息路由对象
CefRefPtr<CefMessageRouterRendererSide> m_message_router;
// ...省略代码
}
// QyAppRenderer.cpp
// ...省略代码
void QyAppRenderer::OnWebKitInitialized() {
if (m_message_router == NULL) {
// Create the renderer-side router for query handling.
CefMessageRouterConfig config;
//创建 渲染进程侧消息路由对象
m_message_router = CefMessageRouterRendererSide::Create(config);
}
}
void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) {
// 注册 window.cefQuery
m_message_router->OnContextCreated(browser, frame, context);
}
void QyAppRenderer::OnContextReleased(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) {
// 释放消息路由
m_message_router->OnContextReleased(browser, frame, context);
}
bool QyAppRenderer::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) {
// 收到 browser进程发送的消息
return m_message_router->OnProcessMessageReceived(browser, frame, source_process, message);
}
注意: renderer侧代码写在 QyRender项目中:
4. browser侧
browser 侧相对比较复杂,在 browser侧主要做的就是消息处理。这里我们从消息的处理开始。
4.1 消息处理类
消息处理类需要我们自己定义,从 CefMessageRouterBrowserSide::Handler
继承。 消息处理收到路由过来的消息后,需要打开文件对话框窗体,但这个操作只能在UI线程中完成,而 消息处理并非运行在UI线程,所以这就需要用QT的信号来处理,所以 消息处类需要继承QT 的 QObject ,开启 Q_OBJECT 宏它才能定义 signal,代码如下:
-
继承QObject 与 CefMessageRouterBrowserSide::Handler 并引入Q_OBJECT 宏
-
重写OnQuery 函数和 OnQueryCanceled。 在 OnQuery中先保存
query_id
与 JavaScript注册的回调函数之间的映射,即 JavaScript的onSuccess
和onFailure
, 然后发射信号给UI线程。 -
定义
void onReadFile(qint64 query_id)
信号,发送信号的时候,需要将 消息ID发射出去,因为这里是多线程操作,没有消息ID就无法找到 JavaScript注册的回调函数,这样就没法调用了,所以这个query_id
很重要。注意 这是QT信号, 参数类型必须在QT中注册,这里使用的是 qint64 ,而不是 int64,因为 int64 并没有在QT中注册。
-
定义 handleTextFileContent 函数,用来接收 文件内容,他需要传递
query_id
找到JavaScript 注册的回调。
// cef_query_handler.h
#pragma once
#include "QObject"
#include "include/wrapper/cef_message_router.h"
class CefQueryHandler : public QObject,
public CefMessageRouterBrowserSide::Handler
{
Q_OBJECT
public:
explicit CefQueryHandler() {
}
// 从渲染进程JavaScript 中发出 window.cefQuery 调用,收到渲染进程的消息后进行处理.
bool OnQuery(CefRefPtr<CefBrowser> browser, //哪个浏览器
CefRefPtr<CefFrame> frame, //浏览器中的哪个frame发出的
int64 query_id, //消息ID
const CefString& request, // 消息名称
bool persistent,
CefRefPtr<Callback> callback) override;
// 取消
void CefQueryHandler::OnQueryCanceled(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64 query_id) override;
// 向主线程发送信号,弹出选择文件对话框,然后读取文件
// 读取结束后,调用该方法处理文件读取的结果,回传到渲染进程
void handleTextFileContent(int errorCode, QString fileContent, int64 query_id);
//信号
signals:
// 读取文件
void onReadFile(qint64 query_id);
private:
// 用来保存 query_id 和 callback之间的映射关系
std::map<int64, CefRefPtr<Callback>> m_pendings;
};
// cef_query_handler.cpp
#pragma execution_character_set("UTF-8")
#include "cef_query_handler.h"
#include <QDebug>
namespace {
//请求名称,与JavaScript 代码中的 MSG_READ_TEXT_FILE 保持一致
const char kTestMessageName[] = "MSG_READ_TEXT_FILE";
}
// 从渲染进程JavaScript 中发出 window.cefQuery 调用,收到渲染进程的消息后进行处理.
bool CefQueryHandler::OnQuery(CefRefPtr<CefBrowser> browser, //哪个浏览器
CefRefPtr<CefFrame> frame, //浏览器中的哪个frame发出的
int64 query_id, //消息ID
const CefString& request, // 消息名称
bool persistent, // 是否持久化
CefRefPtr<Callback> callback) { // 回调函数 onSuccess和 onFailure
const std::string& message_name = request;
if (message_name == kTestMessageName) {
//这里需要将 对象和 callback对象暂时保存到map中
//以便线程执行完毕之后,能够找到它们
m_pendings.insert(std::make_pair(query_id, callback));
qDebug() << "========OnQuery========";
// 发射信号给UI线程
emit onReadFile(query_id);
return true;
}
return false;
}
// UI线程读取完文件内容后,回传文件内容和 query_id,然后调用JavaScript回调
void CefQueryHandler::handleTextFileContent(int errorCode, QString fileContent, int64 query_id) {
//获取回调对象
CefRefPtr<Callback> callback = m_pendings[query_id];
if (errorCode == -1) {
//读取文件出错
callback->Failure(-1, QString("读取文件失败").toStdString());
}
else {
callback->Success(fileContent.toStdString());
}
// 清除掉query_id 与callback的映射
auto it = m_pendings.find(query_id);
m_pendings.erase(it);
}
// 取消
void CefQueryHandler::OnQueryCanceled(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int64 query_id)
{
auto it = m_pendings.find(query_id);
if (it != m_pendings.end())
{
it->second->Failure(-1, "canceled");
m_pendings.erase(it);
}
}
4.2 UI 线程读取文件
在 MainWindow中创建 消息处理器CefQueryHandler
,然后连接它的 onReadFile
信号给槽函数,槽函数执行完毕之后,调用 CefQueryHandler
的handleTextFileContent
函数回传数据:
//mainwindow.h
// ...省略代码
class MainWindow : public QMainWindow
{
// ...省略代码
private slots:
// 处理CefQueryHandler中的onReadFile信号的槽函数
void onReadFile(qint64 query_id);
private:
// ...省略代码
CefQueryHandler* m_cef_query_handler;
};
//mainwindow.cpp
// 构造方法中初始化CefQueryHandler,注意程序退出 delete掉CefQueryHandler
MainWindow::MainWindow(SimpleApp* cefApp, QWidget* parent)
: QMainWindow(parent), m_cefApp(cefApp), m_cef_query_handler(new CefQueryHandler)
{
//...省略代码
}
//...省略代码
void MainWindow::createBrowserWindow() {
// SimpleHandler 需要用到CefQueryHandler
CefRefPtr<SimpleHandler> handler(new SimpleHandler(false, m_cef_query_handler));
//...省略代码
// 连接 CefQueryHandler中的onReadFile信号
connect(m_cef_query_handler, &CefQueryHandler::onReadFile, this, &MainWindow::onReadFile);
}
//读取文件
void MainWindow::onReadFile(qint64 query_id) {
qDebug() << "mainWindow========onReadFile";
QString fileName = QFileDialog::getOpenFileName(NULL, "文件对话框", "D:", "文本文件(*txt)");
QFile file(fileName);
if (file.exists()) { // 如果文件存在
// 读取文件内容
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString content = file.readAll();
file.close();
//int errorCode, QString fileContent, int64 query_id
//成功情况下回传数据,errorCode是 0
m_cef_query_handler->handleTextFileContent(0, content, query_id);
return;
}
}
//失败情况下回传数据,errorCode是 -1
m_cef_query_handler->handleTextFileContent(-1, "读取文件失败", query_id);
}
4.3 browser侧路由器
消息需要靠browser侧路由器来完成,SimpleHandler
类中需要持有 CefMessageRouterBrowserSide
和 CefMessageRouterBrowserSide::Handler
对象:
// simple_handler.h
// ...省略代码
class SimpleHandler :public QObject, public CefClient
, public CefLifeSpanHandler
, public CefKeyboardHandler
, public CefRequestHandler { //用到了CefRequestHandler的OnBeforeBrowse
Q_OBJECT
public:
// 实例化它的时候,需要传入 CefQueryHandler,CefQueryHandler在 MainWindow中实例化的
explicit SimpleHandler(bool use_views, CefQueryHandler* m_cef_query_handler);
~SimpleHandler();
// CefLifeSpanHandler methods:
// 在这里创建路由器,并为路由器设置消息处理器
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
// ...省略代码
// 当最后一个浏览器已经关闭,释放消息路由.
virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
// ...省略代码
// 接收渲染进程的消息
virtual bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) OVERRIDE;
//CefRequestHandler methods:
virtual bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
bool user_gesture,
bool is_redirect) OVERRIDE;
// ...省略代码
private:
const bool use_views_;
// ...省略代码
// 消息路由器
CefRefPtr<CefMessageRouterBrowserSide> m_message_router;
// 消息处理器
std::unique_ptr<CefMessageRouterBrowserSide::Handler> m_message_handler;
// ...省略代码
};
// simple_handler.cc
// 注意成员对象 m_message_handler的类型是 std::unique_ptr<CefMessageRouterBrowserSide::Handler> 调用了它的reset方法来包装 CefQueryHandler
SimpleHandler::SimpleHandler(bool use_views, CefQueryHandler* cef_query_handler)
: use_views_(use_views), is_closing_(false) {
// ...省略代码
m_message_handler.reset(cef_query_handler);
}
// 创建路由器,并为路由器设置消息处理器
void SimpleHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
// ...省略代码
if (!m_message_router) {
// browser进程侧 消息路由配置, renderer进程中JavaScript发出消息和取消消息使用
// window.cefQuery 和 window.cefQueryCancel , 如果不想使用这两个名字,可以在这里配置
CefMessageRouterConfig config;
// config.js_query_function = "cefQuery";
// config.js_cancel_function = "cefQueryCancel";
m_message_router = CefMessageRouterBrowserSide::Create(config);
// 为消息路由设置 消息处理器,即从 render进程发送过来的消息交给谁处理。
m_message_router->AddHandler(m_message_handler.get(), false);
}
}
// CefRequestHandler 接口中的方法 当浏览器发出请求资源之前调用。 返回false 浏览器继续发出请求 返回true,浏览器停止导航。
bool SimpleHandler::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
bool user_gesture,
bool is_redirect) {
m_message_router->OnBeforeBrowse(browser, frame);
return false;
}
// 当最后一个浏览器已经关闭,释放消息路由.
void SimpleHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
// ...省略代码
if (browser_list_.empty()) {
// ...省略代码
// 当最后一个浏览器已经关闭,释放消息路由.
m_message_router->RemoveHandler(m_message_handler.get());
m_message_handler.reset();
m_message_router = nullptr;
}
}
// 路由器接收renderer进程发送的消息
bool SimpleHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) {
CEF_REQUIRE_UI_THREAD();
return m_message_router->OnProcessMessageReceived(browser, frame,
source_process, message);
}
5. 编译运行
分别编译 QyRender项目和 QyCefVS项目,运行后选择一个本地的文件,此时已经能够正常工作了
代码请访问 GitHub qt_cef_11分支