本节利用CEF的进程间通信机制,实现在两个Renderer进程间,使用JavaScript进行通信。即实现两个窗口: MasterWindow 和 SlaverWindow,MasterWindow 使用JavaScript发送消息,SalverWindow监听处理消息。
其实现思路为:
- Browser进程中创建两个窗口,MasterWindow,加载本地 index.html, SlaverWindow加载本地 slaver.html,并保存两个窗口对应的browser对象在 Browser进程的内存中。
- MasterWindow对应的Renderer进程向 Browser进程发送消息,Browser进程收到消息后找到 SlaverWindow对应的browser对象,然后将消息再转发到SlaverWindow对应的Renderer进程
- SlaverWindow 对应的Renderer进程收到消息后,找到注册的JavaScript回调函数然后调用
可见两个Renderer进程之间实现JavaScript的通信实通过Brower进程中转消息实现的。
1. Browser进程侧
创建浏览器窗口,这里使用通过判断机器上如果有两块屏幕则创建slaverBrowser。主要用于主屏和副屏的应用场景。
1.1 创建Browser对象
主要代码 mainwindow.cpp如下:
// mainwindow.cpp
// 创建浏览器对象
bool MainWindow::createBrowser(QString url, QWidget* parentWin) {
//获取实例对象
CefRefPtr<QyCefClientHandler> handler(QyCefClientHandler::getInstance());
// 浏览器配置,
CefBrowserSettings browser_settings;
//browser_settings.universal_access_from_file_urls = STATE_DISABLED;
CefWindowInfo window_info;
RECT winRect;
QRect qtRect = parentWin->rect();
winRect.left = qtRect.left();
winRect.top = qtRect.top();
winRect.right = qtRect.right();
winRect.bottom = qtRect.bottom();
HWND wnd = (HWND)parentWin->winId();
window_info.SetAsChild(wnd, winRect);
// Create the browser window.
return CefBrowserHost::CreateBrowser(window_info, handler, url.toStdString(), browser_settings,
nullptr, nullptr);
}
/// <summary>
/// 创建浏览器窗体,并与QT 窗体集成
/// </summary>
void MainWindow::createBrowserWindow() {
//运行目录
QDir dir = QCoreApplication::applicationDirPath();
// 要打开的网址
QString uriMaster = QDir::toNativeSeparators(dir.filePath("resources/index.html"));
// 创建MasterBrowser
createBrowser(uriMaster, this->centralWidget());
// 操作系统桌面
QDesktopWidget* desktop = QApplication::desktop();
if (desktop->screenCount() <= 1) {
qDebug() << QString("无第二屏幕:%1").arg(desktop->screenCount());
return;
}
QString uriSlaver = QDir::toNativeSeparators(dir.filePath("resources/slaver.html"));
m_slaverWin = new SlaverWindow(NULL);
m_slaverWin->setGeometry(desktop->screenGeometry(1));
// 创建浏览器窗口
bool ret = createBrowser(uriSlaver, m_slaverWin);
m_slaverWin->show();
m_slaverWin->showFullScreen();
//qDebug() << QString("SlaverWindow::createBrowserWindow======:%1").arg(ret);
}
1.2 保存Browser对象
当browser对象被创建后,会被QyCefClientHandler的 OnAfterCreated 监测到,此时可以在这里保存browser对象。
主要代码:
/// <summary>
/// 每弹出一个浏览器窗体,就会有一个新的browser
/// </summary>
/// <param name="browser"></param>
void QyCefClientHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
//const std::string type = getBrowserType(browser);
//保存浏览器对象
if (browserMap.size() == 0) {
masterBrowser = browser;
}
else {
slaverBrowser = browser;
}
browserMap[browser->GetIdentifier()] = browser;
//qDebug() << QString("OnAfterCreated=======>frameID:%1 , total: %2 browserID: %3")
// .arg(browser->GetMainFrame()->GetIdentifier())
// .arg(browserMap.size())
// .arg(browser->GetIdentifier());
}
参数 browser 中好像没有携带加载的页面信息,通过它找到的 mainFrame的url是空的,这里用了一个QMap来保存browser对象,第一次进来的 browser被认为是 maserBrowser。
如果有更好的办法,请留言
1.3 转发消息
当收到渲染进程发送的消息PROCESS_MESSAGE_TO_SLAVER_SCREEN
后,先找到 slaverBrowser浏览器对象,然后将消息转发给 SlaverWindow对应的Renderer进程:代码
// 收到渲染进程消息
bool QyCefClientHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) {
CefString messageName = message->GetName();
std::string msgName = messageName;
if (Share::PROCESS_MESSAGE_TO_SLAVER_SCREEN == msgName) {
// 使用slaver browser对象转发消息
if (slaverBrowser.get()) {
slaverBrowser->GetMainFrame()->SendProcessMessage(PID_RENDERER, message);
return true;
}
}
return false;
}
2. Renderer进程侧
Renderer进程侧主要注册 JavaScript Native函数:
-
index.html 页面对应的
app.sendToSlaver(data)
app.cleanSlaver()
-
slaver.html页面对应的
app.onReceiveFromMaster(function(data){ ... })
app.onCleanData(function(){.....})
CEF中发送消息的时候,都只能发送基本数据类型,如果要发送一个JavaScript 对象,就需要在JavaScript中使用
JSON.stringify
将对象转换成字符串进行传递收到后,需要用
JSON.parse
将字符串重新转换为 JavaScript 对象。如有更好办法,请留言
2.1定义函数处理器
ScreenCommunicationV8Handler
为自定义的 V8Handler
函数处理器。用来处理 上面 4 个 JavaScript函数的注册和回调。
要点:
- index.html 页面对应的
app.sendToSlaver(data)
和app.cleanSlaver()
,当他们执行后,需要将消息转发到 Browser进程。不能直接执行 slaver.html中注册的回调函数,因为这两个页面在不同的Render进程中,无法直接通信,需要借助 Browser进程来进行中转。
// 给Browser 进程发送消息
void ScreenCommunicationV8Handler::handleMaster(const CefString& name,
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception
) {
// 发送消息给browser 进程
//创建消息
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create(Share::PROCESS_MESSAGE_TO_SLAVER_SCREEN);
// 参数
CefRefPtr<CefListValue> args = msg->GetArgumentList();
//枚举转换为字符串
qDebug() << "=====发送消息给Browser进程==function name:" << QString::fromStdString(name);
if (name == "sendToSlaver") {
args->SetSize(2); //2个参数
args->SetString(0, name); //被调用的函数名称
args->SetString(1, arguments[0]->GetStringValue());//参数内容
}
if (name == "cleanSlaver") {
args->SetSize(1); //1个参数
args->SetString(0, name); //被调用的函数名称
}
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
// 发送消息
context->GetFrame()->SendProcessMessage(PID_RENDERER, msg);
}
- slaver.html页面对应的
app.onReceiveFromMaster(function(data){ ... })
和app.onCleanData(function(){.....})
注册后,要保存回调函数的引用到内存中,等待消息转发过来后,在从消息中取出数据,最后执行回调。
// 注册 JavaScript函数,保存回调函数
void ScreenCommunicationV8Handler::handleSlaver(const CefString& name,
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) {
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
JavaScriptCallBack callback = {
name,context,context->GetFrame(),context->GetBrowser(),arguments[0]
};
// 回调函数加入到map中
mCallBackMap.insert(name, callback);
qDebug() << QString("ScreenCommunicationV8Handler::handleSlaver name:%1 mCallBackMapSize: %2")
.arg(QString::fromStdString(name)).arg(mCallBackMap.size());
}
- 当收到从Browser进程转发过来的消息后,需要执行回调,所以提供一个public的 方法:
handleSlaverCallback
// 调用注册的回调函数
bool ScreenCommunicationV8Handler::handleSlaverCallback(CefRefPtr<CefProcessMessage> message) {
// 解析message
CefString name = message->GetName();
if (name != Share::PROCESS_MESSAGE_TO_SLAVER_SCREEN) {
return false;
}
CefRefPtr<CefListValue> args = message->GetArgumentList();
CefString functionName = args->GetString(0);
qDebug() << QString("ScreenCommunicationV8Handler::handleSlaverCallback : %1 functionName:%2 callbackSize:%3 ::::::%4")
.arg(QString::fromStdString(name))
.arg(QString::fromStdString(functionName))
.arg(mCallBackMap.size())
.arg(functionName == "sendToSlaver");
if (functionName == "sendToSlaver") {
CefString callbackParam = args->GetString(1);
// 解析成JSON
//CefRefPtr<CefValue> jsonObject = CefParseJSON(callbackParam, JSON_PARSER_ALLOW_TRAILING_COMMAS);
//onReceiveFromMaster
qDebug() << QString("调用callback之前 : ********%1").arg(mCallBackMap.contains("onReceiveFromMaster"));
if (mCallBackMap.contains("onReceiveFromMaster")) {
JavaScriptCallBack callback = mCallBackMap["onReceiveFromMaster"];
CefV8ValueList arguments;
qDebug() << QString("调用callback之前 : FrameID :%1 url:%2 callbackParam:%3")
.arg(callback.frame->GetIdentifier())
.arg(QString::fromStdString(callback.frame->GetURL()))
.arg(QString::fromStdString(callbackParam));
//回调函数参数
arguments.push_back(CefV8Value::CreateString(callbackParam));
// 执行回调
callback.callbackFun->ExecuteFunctionWithContext(callback.context, NULL, arguments);
return true;
}
return false;
}
if (functionName == "cleanSlaver") {
//onCleanData
if (mCallBackMap.contains("onCleanData")) {
JavaScriptCallBack callback = mCallBackMap["onCleanData"];
// 进入Contxt
//callback.context->Enter();
CefV8ValueList arguments;
// 执行回调
callback.callbackFun->ExecuteFunctionWithContext(callback.context, NULL, arguments);
//callback.context->Exit();
return true;
}
return false;
}
return false;
}
2.2 收到Browser进程转发的消息
QyAppRenderer 中的OnProcessMessageReceived
收到从Browser进程转发的消息后,执行 Slaver.html注册的回调函数:
bool QyAppRenderer::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) {
// 收到 browser进程发送的消息
CEF_REQUIRE_RENDERER_THREAD();
DCHECK_EQ(source_process, PID_BROWSER);
qDebug() << QString("收到browser 进程消息 : %1").arg(frame->GetIdentifier());
if (mScreenCommunicationV8Handler) {
return mScreenCommunicationV8Handler->handleSlaverCallback(message);
}
return false;
}
3. 页面端
3.1 MasterWindow 主屏
<input type="text" id="txtMessage" />
<button id="btnSendSecondScreen">发送数据给副屏</button>
<button id="btnCleanSecondScreen">副屏清屏</button>
<div>主副屏进程间通信</div>
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/index.js"></script>
window.onload = () => {
console.log(window.secondScreen);
if(window.app && window.app.sendToSlaver){
$("#btnSendSecondScreen").click(()=>{
// 发送数据 sendData 发送的是字符串
app.sendToSlaver(JSON.stringify({
time:new Date(),
content:$("#txtMessage").val()
}));
});
$("#btnCleanSecondScreen").click(()=>{
// 清屏
window.app.cleanSlaver();
});
}
}
3.2 SlaverWindow 副屏
<div style="font-size: 25px;">这是副屏 本地HTML</div>
<hr>
收到的数据: <br />
<div id="receivedData">
</div>
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/slaver.js"></script>
window.onload=function(){
if(window.app && window.app.onReceiveFromMaster){
// 接收主屏发送数据 data是字符串
window.app.onReceiveFromMaster((data)=>{
var htmlData=$("#receivedData").html();
htmlData+="<br />"+data;
$("#receivedData").html(htmlData);
});
//清除数据
window.app.onCleanData(()=>{
$("#receivedData").html("");
});
}
}