此前,主管让我调研cef3的使用,但是cef3比较复杂,太难理解了;偶然间,在网上看到有QT第三方库QCefView,这个库封装了cef3,使得可以很简单的在桌面应用加载显示html,并与其进行通信;经过几天的调研,现在将调研结果记录下来!
QCefView是什么?
QCefView是为Qt框架开发的一个封装集成了Chromium Embedded Framework库的Wdiget UI组件。使用QCefView可以充分发挥CEF丰富强大的Web能力,快速开发混合架构的应用程序。
使用Qt开发者熟悉的Forms,signal/slot来开发应用
方便直观的Javascript/C++互操作方式
为何选择QCefView而不用Electron?
从设计思路和最终形态来讲QCefView和Electron是完全不同的技术。
QCefView只是一个为Qt框架开发的UI组件,Electron则是一个功能完备的应用开发框架
QCefView是为Native系统开发者设计的,Electron对前端开发者更友好
QCefView使用C++作为主要开发语言,Electron全部基于Javascript
QCefView提供便捷直观的Javascript/C++互操作方式,Electron通过编写插件实现Web/Native互操作
QCefView适合开发何种类型的应用?
如果你打算使用Web前端技术来开发你的应用UI,同时保持使用Native方式编写核心业务/功能逻辑,QCefView是最佳选择。
例如:
音乐/视频播放器
游戏平台
工具类应用
等等……
以上场景中的应用几乎都是基于内容的平台,他们都需要展示很多列表,表格或者有各种复杂特效的页面。基于此种目的,Web前端技术是目前的最好的选择,把UI当作Web前端App来开发,而核心的功能和逻辑仍然使用Native的方式来编写,然后通过QCefView整合,能极大的提升生产效率,并且一份UI代码适配所有主流桌面平台。
如果你打算开发一款浏览器,QCefView并不是较好的选择,因为QCefView设计的目的是UI组件,并不提供作为浏览器的全部特性,该类需求应该使用原生CEF来实现较好。
上面内容出自QCefView官网
官网链接:https://cefview.github.io/QCefView/
注意:Window环境编译QCefView依赖VS2019或VS2022 和 QT6以上版本,还有CMake!
官网:https://cefview.github.io/QCefView/zh/docs/intros
下载解压后
将CefViewCode-main中的全部文件拷贝到QCefView-main/CefViewCode 目录下:
在 QCefView-main 路径下新建文件夹build
编辑QCefView-main 路径下的QtConfig.cmake文件
设置QT6的环境变量
下载安装CMake
下载链接:https://cmake.org/download/
打开CMake
根据下图步骤进行编译!
插入小道消息-------------------------------------------------------------------------------------------------------------
如果网络不好,网上说可以自行下载一个cef版本,然后将cef拷贝到路径/QCefView-main/CefViewCore/dep,然后去下图二文件中进行修改一些操作,CMake编译就不用下载了,具体我没有操作成功!
cef官网:https://cef-builds.spotifycdn.com/index.html
小道消息结束-------------------------------------------------------------------------------------------------------------
下面接着开始下载的步骤,下图显示的是已经将cef下载完毕并解压好了的
在路径/QCefView-main/CefViewCore/dep下,有CMake帮我们下载的CEF包
编译VS工程
查看编译好的库
至此,QCefView已经编译完毕!
有些朋友的项目可能需要使用到Curl,并且,curl得带有openssl,否则无法使用https;请按照下面步骤进行编译!不需要可跳过此步骤!
下载和安装
下载其他人做的便捷版安装包
下载链接:http://slproweb.com/products/Win32OpenSSL.html
下载后安装, 一直狂点下一步就行了。
安装时如果你没有修改安装路径,默认是安装在:C:\Program Files\OpenSSL-Win64
OpenSSL安装完毕!
注意:编译带有openssl的curl库,编译Debug失败,编译Release成功!
下载地址:https://curl.se/download.html
不知为何,这里只有Release版本的下载,所以我们待会编译带有openssl的curl库时也只能编译Release的库,编译Debug的库会失败!
当然,如果只是编译curl没有包含openssl的话,debug的库貌似是可以编得过的!
选择 DLL Release – DLL OpenSSL 和 x64
如果编译带有openssl的curl库,这里一定要选择 DLL Release – DLL OpenSSL ,否则会编译失败!
包含openssl的库文件
如果只是需要使用curl的http,而不需要使用https,那么,编译也就不需要带有openssl,可以直接跳到下方第4步骤进行编译;当然,在第二步骤需要选择,DLL Debug或者DLL Release项进行编译即可!
A. 右键libcurl - 属性 - VC++目录 - 包含目录
添安装的OPenSSL库的头文件路径进来
C:\Program Files\OpenSSL-Win64\include
B. VC++目录 - 库目录
将安装的OPenSSL库的lib添加进来
C:\Program Files\OpenSSL-Win64\lib
至此,Curl编译完毕!
另外,如果编译的是带有openssl的curl库,那么此库应该是Release版本的;如果此库需要和QCefView结合一起使用,得将QCefView库也编译成Release版本的才可以一起使用,否则会编译报错!
下面将根据本人写的一个小案例进行讲解代码知识点。此案例是QT使用QCefView显示html,并与之进行通信,互相发送消息!
具体案例代码放在下面总结处,有需要的可以去下载!
上方显示的是html页面
下方显示的是QT页面
当然,也可以直接整个窗体都进行显示html页面,具体根据自己的需求来就好!
因为我对HTML不熟悉,所以搞得html显示的有点奇怪,但不影响我们立即如何使用。
文件名:QCefViewTest.html
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Logintitle>
<link rel="stylesheet" type="text/css" href="Login.css"/>
head>
<body onload="onLoad()" id="main" class="noselect">
<div id="login">
<form method="post">
<input id="account" type="text" required="required" placeholder="请输入" name="u">input>
<button id="loginBtn" class="but" type="button" onclick="onCallBridgeQueryClicked('html')">发送button>
<textarea id="output" type="text" required="required" placeholder="内容" name="t">textarea>
form>
<button id="loginBtn" class="but" type="button" onclick="onInvokeMethodClicked('message1', '标题', '这是message1', 'a', 1.1)">Message1button>
<button id="loginBtn" class="but" type="button" onclick="onInvokeMethodClicked('message2', '标题', '这是message2', 'B', 2.2)">Message2button>
div>
<script>
function ap(flag, ...arg) {
// flag 是第一个参数,后续的参数都存在arg中
// 获取QT传过来的数据,多个可以继续使用索引进行获取,例如arg[1];arg[2];等
var mess = arg[0];
if (mess != '') {
var txt = document.getElementById('output').value; //获取textarea的值
var text = flag + ": " + mess;
if (txt == '') {
document.getElementById('output').value = text; //设置textarea的值
} else {
document.getElementById('output').value = txt + "\n" + text; //设置textarea的值
}
}
//var t1 = arg.length; // arg记录有多少个参数
//var t2 = ap.length; // ap函数有多少个参数,固定只会显示一个,就是flag
}
// 使用事件方式给QT发送失败后,QT给html发送失败消息
function sendFail(flag, ...arg) {
var mess = arg[0];
var txt = document.getElementById('output').value; //获取textarea的值
var text = flag + ": " + mess;
if (txt == '') {
document.getElementById('output').value = text; //设置textarea的值
} else {
document.getElementById('output').value = txt + "\n" + text; //设置textarea的值
}
}
function onLoad() {
if (typeof CallBridge == "undefined") {
alert("Not in CefView context");
return;
}
// 注册一个叫apChange的事件,该事件绑定名为ap的函数,当有接收到apChange事件,ap函数调用
CallBridge.addEventListener("apChange", ap);
// 还可以注册多个事件,绑定不同的函数
CallBridge.addEventListener("sendFailChange", sendFail);
}
function onInvokeMethodClicked(name, ...arg) {
// invoke C++ code // 给QT发射信号
window.CallBridge.invokeMethod(name, ...arg);
}
// 给QT发送成功后,QT给html发送成功消息
function on_success(response) {
// response是主程序返回的数据
var txt = document.getElementById('output').value; //获取textarea的值
var text = response;
//document.getElementById('output').value = txt + "\n" + text; //设置textarea的值
}
// 给QT发送失败后,QT给html发送失败消息
function on_failure(error_code, error_message) {
// error_message是主程序返回的数据
var txt = document.getElementById('output').value; //获取textarea的值
var text = response;
document.getElementById('output').value = txt + "\n" + text; //设置textarea的值
}
function onCallBridgeQueryClicked(name, ...arg) {
var message = document.getElementById("account").value;
//var name = 'html';
var str = name + "|" + message; // 由于只能传递一个字符串参数,如果想要传递多个,可以使用一个字符进行隔开组合到一起,qt就收后再进行分割获取即可
document.getElementById("account").value = ''; // 把输入框清空
if (message != '') {
var query = {
request: str, // 参数
onSuccess: on_success, // 主程序返回成功信号,执行此函数
onFailure: on_failure, // 主程序返回失败信号,执行此函数
};
// 给QT发射信号
window.CefViewQuery(query);
var txt = document.getElementById('output').value; //获取textarea的值
var text = name + ": " + message;
if (txt == '') {
document.getElementById('output').value = text; //设置textarea的值
} else {
document.getElementById('output').value = txt + "\n" + text; //设置textarea的值
}
}
}
script>
body>
html>
文件名:Login.css
html{
width: 100%;
height: 100%;
overflow: hidden;
font-style: sans-serif;
}
body{
width: 100%;
height: 100%;
font-family: 'Open Sans',sans-serif;
margin: 0;
background-color: #4A374A;
}
#login{
position: absolute;
top: 50%;
left:50%;
margin: -150px 0 0 -150px;
width: 300px;
height: 300px;
}
#login h1{
color: #fff;
text-shadow:0 0 10px;
letter-spacing: 1px;
text-align: center;
}
h1{
font-size: 2em;
margin: 0.67em 0;
}
input{
width: 278px;
height: 18px;
margin-bottom: 10px;
outline: none;
padding: 10px;
font-size: 13px;
color: #fff;
//text-shadow:1px 1px 1px;
border-top: 1px solid #312E3D;
border-left: 1px solid #312E3D;
border-right: 1px solid #312E3D;
border-bottom: 1px solid #56536A;
border-radius: 4px;
background-color: #2D2D3F;
}
.but{
width: 300px;
min-height: 20px;
display: block;
background-color: #4a77d4;
border: 1px solid #3762bc;
color: #fff;
padding: 9px 14px;
font-size: 15px;
line-height: normal;
border-radius: 5px;
margin: 0;
}
textarea{
width: 300px;
height: 200px;
margin-bottom: 10px;
outline: none;
resize: none;
pointer-events: none;
font-size: 13px;
border-top: 1px solid #312E3D;
border-left: 1px solid #312E3D;
border-right: 1px solid #312E3D;
border-bottom: 1px solid #56536A;
border-radius: 4px;
}
新建一个项目名为QCefView_Test,将路径**/QCefView-main/include/的头文件拷贝到项目代码路径中;还有将编译好的QCefView.lib拷贝到项目代码路径**中。
右键项目 - C/C++ - 常规 - 附加包含目录
添加我们的项目代码路径和include路径进来
例如我的是:
E:\Code\vs2022Code\QCefView_Test\QCefView_Test
E:\Code\vs2022Code\QCefView_Test\QCefView_Test\include
右键项目 - 链接器 - 输入 - 附加依赖项
添加lib进来:QCefView.lib
在构造函数中进行如下操作:
添加头文件:
#include “QCefView.h”
#include “QCefContext.h”
定义QCefView对象
// 这个应该要在头文件中进行定义
QCefView* cefViewWidget;
添加一个本地文件夹到URL映射
QDir dir = QCoreApplication::applicationDirPath();
QString path = QDir::toNativeSeparators(dir.filePath("html")); // 获取运行路径,并拼接上html
// 添加一个本地文件夹到URL映射:参数一是文件夹路径,参数二应该是自定义协议和域名
QCefContext::instance()->addLocalFolderResource(path, "my://cpp_learners"); // 自定义协议:my 自定义域名:cpp_learners
//QCefContext::instance()->addLocalFolderResource(path, "https://cpp_learners");
实例化QCefView对象
// 设置QCefView的
QCefSetting setting;
//setting.setBackgroundColor(QColor::fromRgb(100, 80, 60)); // 设置HTML背景颜色
// 创建一个QCfView窗体
cefViewWidget = new QCefView("my://cpp_learners/QCefViewTest.html", &setting, this);
添加到窗体布局中
// 定义一个网格布局,并将QCefView窗体设置在此
QGridLayout* layout = new QGridLayout(this);
layout->addWidget(cefViewWidget, 0, 0, 1, 1);
// 将布局添加到widget中
ui.widgetHtml->setLayout(layout);
ui.widgetHtml是我们在ui界面拖动的一个widget部件
main.cpp文件添加如下代码
一定要添加,否则运行报错!
#include "QCefContext.h"
#include "QCefConfig.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// build QCefConfig
QCefConfig config;
config.setUserAgent("QCefViewTest");
config.setLogLevel(QCefConfig::LOGSEVERITY_DEFAULT);
config.setBridgeObjectName("CallBridge");
config.setRemoteDebuggingPort(9000);
config.setBackgroundColor(Qt::lightGray);
// add command line args
// config.addCommandLineSwitch("allow-universal-access-from-files");
config.addCommandLineSwitch("enable-media-stream");
config.addCommandLineSwitch("use-mock-keychain");
config.addCommandLineSwitch("allow-file-access-from-files");
config.addCommandLineSwitch("disable-spell-checking");
config.addCommandLineSwitch("disable-site-isolation-trials");
config.addCommandLineSwitch("enable-aggressive-domstorage-flushing");
config.addCommandLineSwitchWithValue("renderer-process-limit", "1");
config.addCommandLineSwitchWithValue("disable-features", "BlinkGenPropertyTrees,TranslateUI,site-per-process");
// initialize QCefContext instance with config
QCefContext cefContext(&a, argc, argv, &config);
QCefView_Test w;
w.show();
return a.exec();
}
设置好ui部件后开始编译
此时不出意外的话,应该是编译报错的,报错说缺少什么dll等。
需要将我们编译好的,/QCefView-main/build/output/Debug/bin/ 路径下的所有文件都拷贝到项目的运行路径,也就是.exe所在的路径。
另外,上图html文件夹是我们另外新建的,新建好后,将上面的html代码和css代码,新建相同名字文件后放到html文件夹中。
再次运行
不出意外的话,应该是可以正常运行的了!而且也显示出了我们指定的html页面!
在按钮的槽函数中进行操作
QVariantList 这是一个链表,将我们需要穿个html的数据存到此链表中
然后定义事件QCefEvent ,且绑定事件apChange,注意,这个apChange事件需要和html代码中定义的事件一致!
然后使用setArguments()方法将链表绑定到事件中
最后使用broadcastEvent(event);将事件进行广播
void QCefView_Test::on_btnSend_clicked()
{
if (cefViewWidget) {
// 设置传输的数据
QVariantList list;
list.emplace_back("qt");
list.emplace_back("字符串");
list.emplace_back(123);
list.emplace_back(3.14);
// 绑定事件
QCefEvent event("apChange");
// 绑定参数
event.setArguments(list);
//cefViewWidget->triggerEvent(event);
cefViewWidget->broadcastEvent(event);
}
}
在html的javascript代码中注册事件
function ap(flag, ...arg) {
// flag 是第一个参数,根据上面个代码传过来的值,他是qt,后续的参数都存在arg中
// 获取QT传过来的数据,多个可以继续使用索引进行获取,例如arg[1];arg[2];等
var mess = arg[0];
// ...省略了
//var t1 = arg.length; // arg记录有多少个参数
//var t2 = ap.length; // ap函数有多少个参数,固定只会显示一个,就是flag
}
function onLoad() {
if (typeof CallBridge == "undefined") {
alert("Not in CefView context");
return;
}
// 注册一个叫apChange的事件,该事件绑定名为ap的函数,当有接收到apChange事件,ap函数调用
CallBridge.addEventListener("apChange", ap);
// 还可以注册多个事件,绑定不同的函数
CallBridge.addEventListener("sendFailChange", sendFail);
}
注册的apChange事件,把绑定名为ap的函数,当有此事件传过来时,就会调用ap()函数,就可以在ap函数中接收QT传过来的数据,进行相应的操作了,其实也就相当于QT调用Javascript的代码
还可以注册更多的事件,让QT去调用!
有两种方式给QT发射信号:
invokeMethod 和 CefViewQuery
invokeMethod(name, ...args)
当该方法在Javascript中调用后,下面的Qt signal将被触发:
void invokeMethod(int browserId,int frameId,const QString & method,const QVariantList & arguments);
继续在html中添加Javascript代码
function onInvokeMethodClicked(name, ...arg) {
// invoke C++ code
window.CallBridge.invokeMethod(name, ...arg);
}
然后可以在html代码中添加两个按钮,在按钮的点击事件中调用此方法即可!
根据需求进行添加参数即可!
<body onload="onLoad()" id="main" class="noselect">
<div id="login">
<button id="loginBtn" class="but" type="button" onclick="onInvokeMethodClicked('message1', '标题', '这是message1', 'a', 1.1)">Message1button>
<button id="loginBtn" class="but" type="button" onclick="onInvokeMethodClicked('message2', '标题', '这是message2', 'B', 2.2)">Message2button>
div>
body>
然后再来到QT代码中
新建一个槽函数
private slots:
// 用这个槽函数去接收html传过来的消息
void onInvokeMethod(int browserId, int frameId, const QString& method, const QVariantList& arguments);
然后可以在构造函数中进行绑定
connect(cefViewWidget, &QCefView::invokeMethod, this, &QCefView_Test::onInvokeMethod);
之后实现槽函数onInvokeMethod
第三个参数method,就是在html中onInvokeMethodClicked的第一个参数,之后参数都放在链表arguments中;第一个和第二个参数暂时我也还没搞懂有啥用。
使用第三个参数进行判断,进行相应的操作,可以直接再次槽函数中进行操作,也可以另外定义一个函数进行操作。
当传输过来的第三个参数,在QT这边没有定义,那么可以给html广播事件回去,告诉它没有找到
void QCefView_Test::onInvokeMethod(int browserId, int frameId, const QString& method, const QVariantList& arguments)
{
if (0 == method.compare("message1")) {
/* 可以在这里做处理 */
QString str1 = arguments.at(0).toString();
QString str2 = arguments.at(1).toString();
QString str3 = arguments.at(2).toString();
float f1 = arguments.at(3).toFloat();
int in1 = arguments.size();
} else if (0 == method.compare("message2")) {
/* 也可以定义函数去做处理 */
// void _message2(const QString& method, const QVariantList& arguments)
_message2(method, arguments);
} else {
// 当传输过来的第三个参数,在QT这边没有定义,那么可以给html广播事件回去,告诉它没有找到
if (cefViewWidget) {
// 设置传输的数据
QVariantList list;
list.emplace_back("qt");
list.emplace_back(QString::fromLocal8Bit("没有找到与之对应的事件:") + method);
// 绑定事件
QCefEvent event("sendFailChange");
// 绑定参数
event.setArguments(list);
// 官网例子使用的是broadcastEvent,在网上看到有人使用triggerEvent,测试都是可以的
//cefViewWidget->triggerEvent(event);
cefViewWidget->broadcastEvent(event);
}
}
}
window.CefViewQuery(query)
当从Javascript中调用该方法时,以下Qt signal会被触发:
void cefQueryRequest(int browserId,int frameId,const QCefQuery & query)
继续在html中添加Javascript代码
// 给QT发送成功后,QT给html发送成功消息
function on_success(response) {
// response是主程序返回的数据
alert(response);
}
// 给QT发送失败后,QT给html发送失败消息
function on_failure(error_code, error_message) {
// error_message是主程序返回的数据
alert(error_message);
}
function onCallBridgeQueryClicked(name, ...arg) {
// 设置参数,传给QT
var str1 = '这个是传给QT的数据'
var str2 = 123
var str3 = 3.14
var str = name + str1 + '|' + str2 + '|' + str3; // 因为只能传一个参数,所以可以使用拼接的方式传给QT,QT接收后,再进行分割即可
var query = {
request: str, // 参数
onSuccess: on_success, // 主程序返回成功信号,执行此函数
onFailure: on_failure, // 主程序返回失败信号,执行此函数
};
// 给QT发射信号
window.CefViewQuery(query);
}
然后可以在html代码中添加个按钮,在按钮的点击事件中调用此方法即可!
<button id="loginBtn" class="but" type="button" onclick="onCallBridgeQueryClicked('html')">发送button>
然后再来到QT代码中
新建一个槽函数
private slots:
// 用这个槽函数去接收html传过来的消息
void onQCefQueryRequest(int browserId, int frameId, const QCefQuery& query);
然后可以在构造函数中进行绑定
connect(cefViewWidget, &QCefView::cefQueryRequest, this, &QCefView_Test::onQCefQueryRequest);
之后实现槽函数onQCefQueryRequest
根据参数三query的request方法获得字符串,然后进行分割,就拿到传过来的参数了
根据需求做完操作后,需要调用方法query.setResponseResult,将操作的结果返回给html,true为成功,false为失败,还可以传递字符串信息回去。
最后调用cefViewWidget->responseQCefQuery(query);即可
void QCefView_Test::onQCefQueryRequest(int browserId, int frameId, const QCefQuery& query) {
// 获得html传过来的数据
std::vector<std::string> vec = _split(query.request().toStdString(), "|"); // 分割获取数据
if (1 == vec.size() || true == vec.at(1).empty()) {
QMessageBox::information(this, QString::fromLocal8Bit("提示"), QString::fromLocal8Bit("消息为空!"));
// 设置结果为false,给html返回结果
query.setResponseResult(false, QString::fromLocal8Bit("qt接收失败,数据为空!"));
} else {
// 设置结果为true,给html返回结果
query.setResponseResult(true, QString::fromLocal8Bit("qt接收成功!"));
}
// 给js返回结果(字符串)
cefViewWidget->responseQCefQuery(query);
}
到此为止,QT与html通信流程就结束了!
也是经过了好几天的潜心去研究,虽说没有什么特别的技术在这里,但是基本的操作还是写下来了,都是参考官网给出的例子去完成的!
后续还得再花时间去深入研究一下!
案例源码:
https://download.csdn.net/download/cpp_learner/86266086