前言
接上一篇文章QCefView入门及环境配置之后,我们已经了解了QCefView的基本编译以及环境的配置,这一节我们依据管网案例来实现QCefView的入门功能,具体包括:
- 在Qt控件中嵌入一个网页并正常显示
- 通过网页发送消息给Qt界面
- 通过Qt界面按钮来发送消息给网页
主要参考官网的说明以及源码中的QCefViewTest项目代码。
一、效果展示
- 新建cef窗口,其中*.html文件自己简单写的。
- 点击“修改颜色”可以修改html网页部件颜色。
- 点击“Invoke Method”发送消息到Qt桌面端。
- 点击“Query”可以将网页控件中的一段消息发送到Qt桌面端,并返回消息到web端。
二、创建Qt项目及UI
我使用的是VS创建Qt项目方式,本来想用QtCreator,但后期使用过程中总出现*.lib文件找不到问题,具体原因也没有查到。但用VS打开Qt项目编译又正常,因此索性直接用VS创建项目实现代码功能吧。
三、源码
1、初始化QCefContext实例:使用QCefView的第一步必须初始化一个QCefContext的实例,跟QApplication一样, 在应用程序的生命周期内必须有且仅有一个QCefContext实例;可以看到初始化QCefContext实例的时候需要传入一个QCefConfig。你可以通过QCefConfig设置一些CEF的配置参数,例如可以设置日志级别,调试端口等。更多详细参数请参考API文档QCefConfig。不要试图主动析构QCefContext实列,该实例跟随Application的生命周期存在和销毁,如果提前销毁则会导致CEF内部状态错误。
main.cpp
#include <QApplication>
#include <QCefContext.h>
#include "MainWindow.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);
MainWindow w;
w.show();
return a.exec();
}
2、创建QCefView实例
一旦初始化QCefContext完成,就可以创建QCefView对象了。
#ifndef QCEFVIEWTEST_H
#define QCEFVIEWTEST_H
#include <QMainWindow>
#include "ui_MainWindow.h"
#include "CefViewWidget.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget* parent = 0);
~MainWindow();
protected:
void createCefView();
// QCefView slots
protected slots:
void onInvokeMethod(int browserId, int64_t frameId, const QString& method, const QVariantList& arguments);
void onQCefQueryRequest(int browserId, int64_t frameId, const QCefQuery& query);
// ui slots
protected slots:
void onBtnNewBrowserClicked();
void onBtnChangeColorClicked();
private:
Ui::MainWindow ui;
CefViewWidget* cefViewWidget = nullptr;
QRegion draggableRegion_;
QRegion nonDraggableRegion_;
};
#endif // QCEFVIEWTEST_H
#include "MainWindow.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QJsonDocument>
#include <QJsonValue>
#include <QMessageBox>
#include <QRandomGenerator>
#include <QTextCodec>
#include <QCefContext.h>
#pragma execution_character_set("utf-8")
#define URL_ROOT "http://QCefViewDir"
#define INDEX_URL URL_ROOT "/index.html"
#define TUTORIAL_URL URL_ROOT "/tutorial.html"
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent /*, Qt::FramelessWindowHint*/)
{
ui.setupUi(this);
connect(ui.btn_newBrowser, &QPushButton::clicked, this, &MainWindow::onBtnNewBrowserClicked);
connect(ui.btn_quitApp, &QPushButton::clicked, qApp, &QCoreApplication::quit);
connect(ui.btn_changeColor, &QPushButton::clicked, this, &MainWindow::onBtnChangeColorClicked);
QVBoxLayout* layout = new QVBoxLayout();
layout->setContentsMargins(2, 2, 2, 2);
layout->setSpacing(3);
layout->addWidget(ui.nativeContainer);
layout->addWidget(ui.cefContainer);
centralWidget()->setLayout(layout);
// build the path to the web resource
QDir dir = QCoreApplication::applicationDirPath();
QString webResourceDir = QDir::toNativeSeparators(dir.filePath("webApp"));
// add a local folder to URL map (global)
QCefContext::instance()->addLocalFolderResource(webResourceDir, URL_ROOT);
createCefView();
}
MainWindow::~MainWindow() {}
void MainWindow::createCefView()
{
///*
// build settings for per QCefView
QCefSetting setting;
setting.setPlugins(false);
setting.setWindowlessFrameRate(60);
setting.setBackgroundColor(QColor::fromRgba(qRgba(255, 255, 220, 255)));
// setting.setBackgroundColor(Qt::blue);
// create the QCefView widget and add it to the layout container
cefViewWidget = new CefViewWidget(INDEX_URL, &setting);
ui.cefContainer->layout()->addWidget(cefViewWidget);
// allow show context menu for both OSR and NCW mode
cefViewWidget->setContextMenuPolicy(Qt::DefaultContextMenu);
// connect the invokeMethod to the slot
connect(cefViewWidget, &QCefView::invokeMethod, this, &MainWindow::onInvokeMethod);
// connect the cefQueryRequest to the slot
connect(cefViewWidget, &QCefView::cefQueryRequest, this, &MainWindow::onQCefQueryRequest);
}
void MainWindow::onBtnNewBrowserClicked()
{
QMainWindow* w = new QMainWindow(nullptr);
w->setAttribute(Qt::WA_DeleteOnClose);
QCefSetting settings;
QCefView* view = new QCefView(INDEX_URL, &settings, w);
w->setCentralWidget(view);
w->resize(1024, 768);
w->show();
}
void MainWindow::onBtnChangeColorClicked()
{
if (cefViewWidget)
{
// create a random color
QColor color(QRandomGenerator::global()->generate());
// create the cef event and set the arguments
QCefEvent event("colorChange");
event.arguments().append(QVariant::fromValue(color.name(QColor::HexArgb)));
// broadcast the event to all frames in all browsers created by this QCefView widget
cefViewWidget->broadcastEvent(event);
}
}
void MainWindow::onInvokeMethod(int browserId, int64_t frameId, const QString& method, const QVariantList& arguments)
{
// extract the arguments and dispatch the invocation to corresponding handler
if (0 == method.compare("TestMethod"))
{
QString sTitle("消息提醒");
QString text = QString("Current Thread: QT_UI\r\n"
"BrowserId: %1\r\n"
"Frame: %2\r\n"
"Method: %3\r\n"
"Arguments:\r\n")
.arg(browserId)
.arg(frameId)
.arg(method);
for (int i = 0; i < arguments.size(); i++)
{
auto jv = QJsonValue::fromVariant(arguments[i]);
// clang-format off
text.append(
QString("%1 Type:%2, Value:%3\r\n")
.arg(i).arg(arguments[i].typeName()).arg(arguments[i].toString())
);
// clang-format on
}
auto jsonValue = QJsonDocument::fromVariant(arguments);
auto jsonString = QString(jsonValue.toJson());
text.append(QString("\r\nArguments List in JSON format:\r\n%1").arg(jsonString));
QMessageBox::information(this->window(), sTitle, text);
}
}
void MainWindow::onQCefQueryRequest(int browserId, int64_t frameId, const QCefQuery& query)
{
QMetaObject::invokeMethod(
this,
[=]()
{
QString sTitle("消息提醒");
QString text = QString("Current Thread: QT_UI\r\n"
"Query: %1")
.arg(query.request());
QMessageBox::information(this->window(), sTitle, text);
QString response = query.request().toUpper();
query.setResponseResult(true, response);
cefViewWidget->responseQCefQuery(query);
},
Qt::QueuedConnection);
}
3、创建一个简单的Web页面
创建一个简单的Web页面,内容如下:
<html>
<head>
<meta charset="utf-8">
</head>
<body onload="onLoad()" id="main" class="noselect">
<h1 align="center" style="font-size: 12pt">Web Area</h1>
<label> InvokeMethod方法测试:向桌面端发送消息 </label>
<br />
<input
type="button" value="Invoke Method"
onclick="onInvokeMethodClicked('TestMethod', 1, false, 'arg3')"/>
<br />
<br />
<label> QCefQuery方法测试:向桌面端发送消息 </label>
<br />
<textarea id="message" style="width: 320px; height: 120px">
this message will be processed by native code.
</textarea>
<br />
<input type="button" value="Query"
onclick="onCallBridgeQueryClicked()" />
</body>
<script>
function onLoad()
{
if (typeof CallBridge == "undefined")
{
alert("Not in CefView context");
return;
}
CallBridge.addEventListener("colorChange", function (color)
{document.getElementById("main").style.backgroundColor = color;});
}
function onInvokeMethodClicked(name, ...arg)
{
// invoke C++ code
window.CallBridge.invokeMethod(name, ...arg);
}
function onCallBridgeQueryClicked()
{
var query = {
request: document.getElementById("message").value,
onSuccess: function (response) {
alert(response);
},
onFailure: function (error_code, error_message) {
alert(error_message);
},
};
window.CefViewQuery(query);
}
</script>
</html>
三、功能分析
1、加载WebApp资源
QCefView提供4种加载Web资源的方式。
- 加载在线Web内容
在QCefView的构造函数中直接传递在线Web内容的URL
// build settings for per QCefView
QCefSetting setting;
// create the QCefView widget and add it to the layout container
QCefView* cefView = new QCefView("https://baidu.com", &setting, nullptr);
- 通过本地文件路径加载
在QCefView的构造函数中直接传递本地Web资源文件的全路径,注意路径必须是以file://为schema的格式。
// build the path to the web resource
QDir dir = QCoreApplication::applicationDirPath();
QString webResourceDir = QDir::toNativeSeparators(dir.filePath("webres/index.html"));
// build settings for per QCefView
QCefSetting setting;
// create the QCefView widget and add it to the layout container
QCefView* cefView = new QCefView(INDEX_URL, &setting, nullptr);
- 添加本地文件目录到URL的映射
如果你的WebApp资源文件较多,并且在一个本地目录中,你可以通过如下方法添加一个本地文件目录到URL的映射:
public void addLocalFolderResource(const QString & path,const QString & url,int priority);
例如WebApp经过编译后输出到webres目录中,其目录结构如下:
full\path\to\webres
│ index.html
├───assets
├───docs
├───img
可以通过以下方法添加映射:
// add a local folder to URL map
QCefContext::instance()->addLocalFolderResource(
"full\\path\\to\\webres",
"https://domainname" // This could be any URL you need
);
// build settings for per QCefView
QCefSetting setting;
// create the QCefView widget and add it to the layout container
QCefView* cefView = new QCefView(
"https://domainname/index.html",
&setting,
this
);
映射添加之后,便可以通过URL拼接资源相对路径的方式访问所有资源。
- 添加本地Zip文件到URL的映射
除了添加本地文件目录到URL的映射,还可以添加本地Zip文件到URL的映射,通过以下方法实现:
public void addArchiveResource(const QString & path,const QString & url,const QString & password);
例如以下结构的Zip文件
full\path\to\webres.zip
│ index.html
├───assets
├───docs
├───img
通过以下代码添加映射:
// add a local zip file to URL map
QCefContext::instance()->addArchiveResource(
"full\\path\\to\\webres.zip",
"https://domainname",
"password" // pass the password of the zip file if needed
);
// build settings for per QCefView
QCefSetting setting;
// create the QCefView widget and add it to the layout container
QCefView* cefView = new QCefView(
"https://domainname/index.html",
&setting,
this
);
使用场景
如果你使用比较流行的前端框架(Rect,Vue或者其他框架)开发你的WebApp,那么上述方法3和4非常有用,特别是SAP WebApp。这些前端项目编译之后输出的静态资源结构比较复杂,所以可以通过添加映射的方式来加载。
2、C++/Javascript互操作
QCefView提供C++/Javascript互操作的能力,因此开发者可以从C++中调用Javascript代码,反之亦然。
互操作能力是通过在QCefView所创建的所有Browser和Frame中插入桥接对象实现的。桥接对象提供一些方法用于原生代码和Web代码进行通信,更多详细介绍请参考WebAPIs。
桥接对象被挂载在window对象上,并且可以通过QCefConfig::setBridgeObjectName来设置桥接对象的名字。默认的对象名为CefViewClient
从Javascript中调用C++
桥接对象提供以下方法来从Javascript中调用C++:
invokeMethod(name, ...args),
当该方法在Javascript中调用后,下面的Qt signal将被触发:
void invokeMethod(int browserId,int frameId,const QString & method,const QVariantList & arguments)
⚠ 注意: Javascript方法invokeMethod(name, …args)是 异步操作,这意味着该方法的调用会立即返回,无论对应的C++ Qt slot是否已经执行完毕。
现在让我们编写一段代码来演示如何从Javascript中调用C++。
添加Javascript代码
添加如下代码在Web页面的<script>代码块中:
function onInvokeMethodClicked(name, ...arg) {
// invoke C++ code
window.CallBridge.invokeMethod(name, ...arg);
}
添加如下HTML代码:
<label> Test Case for InvokeMethod </label>
<br />
<input
type="button"
value="Invoke Method"
onclick="onInvokeMethodClicked('TestMethod', 1, false, 'arg3')"
/>
添加C++代码
然后添加如下C++代码用于处理调用请求:
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent)
{
// ...
// connect the invokeMethod to the slot
connect(cefViewWidget,
SIGNAL(invokeMethod(int, int, const QString&, const QVariantList&)),
this,
SLOT(onInvokeMethod(int, int, const QString&, const QVariantList&)));
// ...
}
void MainWindow::onInvokeMethod(int browserId, int frameId, const QString& method, const QVariantList& arguments)
{
// extract the arguments and dispatch the invocation to corresponding handler
if (0 == method.compare("TestMethod")) {
QMetaObject::invokeMethod(
this,
[=]() {
QString title("QCef InvokeMethod Notify");
QString text = QString("Current Thread: QT_UI\r\n"
"Method: %1\r\n"
"Arguments:\r\n")
.arg(method);
for (int i = 0; i < arguments.size(); i++) {
// clang-format off
text.append(QString("%1 Type:%2, Value:%3\r\n")
.arg(i)
.arg(arguments[i].typeName())
.arg(arguments[i].toString()));
// clang-format on
}
QMessageBox::information(this->window(), title, text);
},
Qt::QueuedConnection);
} else {
}
}