前言
首先看一下效果(因为掘金不支持上传视频预览,所以贴出 github 的视频链接): github.com/1111mp/ForQ…
也可以直接访问 github 源码查看: github.com/1111mp/ForQ…
图片:
目标
- 实现
LoginWindow和MainWindow,LoginWindow 中点击登录关闭 LoginWindow,打开跳转到MainWindow - 通过
QWebEngineView实现在QMainWindow中加载 web 页面 - 基于
QWebChannel实现混合开发场景下最基本的JsBridge(Native中调用Js方法,Js调用Native方法),实现js中调用native方法并通过Promise执行 native 的回调函数
前置知识
QWebChannel
QWebChannel 主要作用就是向 Web 端共享出 QObject。QWebChannel 官方文档
QWebChannel填补了C++应用程序和HTML/JavaScript应用程序之间的空白。通过将QObject派生的对象发布到QWebChannel并在HTML端使用QWebChannel.js,可以透明地访问QObject的属性、公共槽和方法。不需要手动传递消息和序列化数据,C++端的属性更新和信号发射会自动传输到可能远程运行的HTML客户端。在客户端,将为任何已发布的C++QObject创建一个JavaScript对象。它反映了C++对象的API,因此可以直观地使用。
Web页面中需加载 qwebchannel.js。<script src="qrc:/qtwebchannel/qwebchannel.js"></script> Or 可以在文件夹中找到 Qt/Examples/Qt-6.6.0/webchannel/shared/qwebchannel.js。
核心代码:
m_WebChannel = new QWebChannel(this);
m_WebChannel->registerObject("Context", m_Context); // QObject pointer
m_View = new QWebEngineView(this);
m_View->load(QUrl("qrc:/main.html"));
m_View->page()->setWebChannel(m_WebChannel);
QObject 中通过 宏 Q_PROPERTY 共享属性和方法给 Web:
#ifndef CONTEXT_H
#define CONTEXT_H
#include <QObject>
#include <QVariantMap>
#include <QDebug>
class Context : public QObject
{
Q_OBJECT
// m_Info 做为别名 info 暴露给 web 端
Q_PROPERTY(QVariantMap info MEMBER m_Info NOTIFY onInfoChanged)
public:
explicit MainContext(const QVariantMap& info, QObject *parent = nullptr);
public slots:
// slots web端可直接通过 Context.setInfo 调用
void setInfo(const QVariantMap& info);
void request(qint32 id, QString message);
signals:
// web 端可以通过 Context.onInfoChanged.connect(callback)绑定一个js 的方法
// native中 通过 emit onInfoChanged(info) 可触发js中绑定的方法 并传递参数
void onInfoChanged(const QVariantMap& info);
void onResponse(qint32 id, const QVariantMap& data);
};
#endif // CONTEXT_H
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MainWindow</title>
// web 端 需要加载 qwebchannel.js
<script src="qrc:/qtwebchannel/qwebchannel.js"></script>
</head>
<body>
<div>
<h2>Hello QWebChannel</h2>
<p id="info"></p>
<button onclick="onChangeInfo()">change info</button>
<p id="result"></p>
<button onclick="onTest()">Call native method async & callback</button>
</div>
</body>
<script>
console.log(navigator.userAgent);
const manager = new Map();
let id = 0; // should use uuid
const ele = document.querySelector("#info");
const result = document.querySelector("#result");
// 实例化之后 可拿到 QObject 共享出来的信息
new QWebChannel(qt.webChannelTransport, function (channel) {
// channel.objects.Context 即为 m_WebChannel->registerObject("Context", m_Context) 时的 QObject
window.Context = channel.objects.Context;
// 直接获取 QObject 的 info 属性
ele.innerText = JSON.stringify(window.Context.info);
// connect之后 native 可通过 emit onResponse() 触发js方法
window.Context.onResponse.connect(function (id, data) {
const { resolve = undefined } = manager.get(id);
resolve && resolve(data);
});
// connect之后 native 可通过 emit onInfoChanged() 触发js方法
window.Context.onInfoChanged.connect(function (info) {
alert(JSON.stringify(info));
ele.innerText = JSON.stringify(info);
});
});
function onChangeInfo() {
window.Context.setInfo({ name: "from web", version: 6.6 });
}
function makePromise(callback) {
id += 1;
return new Promise((resolve, reject) => {
callback(id);
manager.set(id, { resolve, reject });
});
}
function onTest() {
makePromise((id) => {
// 直接调用 QObject 的 slots
window.Context.request(id, "from html");
}).then((res) => {
result.innerText = JSON.stringify(res);
});
}
</script>
</html>
实现
程序入口 main.cpp:
#include <QApplication>
#include <QDir>
#include "src/application.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// QApplication::setQuitOnLastWindowClosed(false);
Application app;
return a.exec();
}
Application.h & Application.cpp(管理 LoginWindow 和 MainWindow):
#ifndef APPLICATION_H
#define APPLICATION_H
#include <QObject>
#include <QSharedPointer>
#include <QVariantMap>
#include <QDebug>
#include "LoginWindow/loginwindow.h"
#include "LoginWindow/logincontext.h"
#include "MainWindow/mainwindow.h"
#include "MainWindow/maincontext.h"
class Application : public QObject
{
Q_OBJECT
public:
explicit Application(QObject *parent = nullptr);
~Application();
public slots:
void onLoginSuccessful(const QVariantMap& info);
private:
QSharedPointer<LoginContext> m_LoginContext = nullptr;
QSharedPointer<LoginWindow> m_Login = nullptr; // LoginWindow
QSharedPointer<MainWindow> m_Main = nullptr; // MainWindow
QSharedPointer<MainContext> m_MainContext = nullptr;
};
#endif // APPLICATION_H
#include "application.h"
Application::Application(QObject *parent)
: QObject{parent}
{
// 初始化 Login 窗口,LoginContext 用于 QWebChannel
m_LoginContext = QSharedPointer<LoginContext>(new LoginContext(this));
m_Login = QSharedPointer<LoginWindow>(new LoginWindow(m_LoginContext.data()));
m_Login->show();
// 连接 signal & slots 页面登录按钮调用 onLogin 会 emit LoginContext::onLoginHandler 方法
// 然后通知到 Application::onLoginSuccessful 方法
QObject::connect(m_LoginContext.data(), &LoginContext::onLoginHandler, this, &Application::onLoginSuccessful);
}
Application::~Application()
{
m_Login = nullptr;
m_Main = nullptr;
}
// 登录成功之后 通知到这里 然后执行页面切换的逻辑
void Application::onLoginSuccessful(const QVariantMap& info)
{
qInfo() << "[Application]: onLoginSuccessful";
m_Login->hide();
// 初始化 MainContext,用于 QWebChannel,相当于保存着后续web调用的所有信息的地方
m_MainContext = QSharedPointer<MainContext>(new MainContext(info, this));
m_Main = QSharedPointer<MainWindow>(new MainWindow(m_MainContext.data()));
m_Main->show();
m_Login->close();
m_Login = nullptr;
}
登录窗口 LoginWindow.h & LoginWindow\.cpp:
#ifndef LOGINWINDOW_H
#define LOGINWINDOW_H
#include <QMainWindow>
#include <QWebEngineView>
#include <QWebChannel>
#include <QDebug>
#include "logincontext.h"
QT_BEGIN_NAMESPACE
namespace Ui { class LoginWindow; }
QT_END_NAMESPACE
class LoginWindow : public QMainWindow
{
Q_OBJECT
public:
LoginWindow(LoginContext* context, QWidget *parent = nullptr);
~LoginWindow();
private:
Ui::LoginWindow *ui;
QWebEngineView* m_View;
QWebChannel* m_WebChannel;
LoginContext* m_Context;
};
#endif // LOGINWINDOW_H
#include "loginwindow.h"
#include "./ui_loginwindow.h"
LoginWindow::LoginWindow(LoginContext* context, QWidget *parent)
: m_Context(context), QMainWindow(parent), ui(new Ui::LoginWindow)
{
ui->setupUi(this);
// 如果要打开控制台调试 web 页面,设置 QTWEBENGINE_REMOTE_DEBUGGING port
// 浏览器 打开 http://localhost:8080 点击进去之后就出现调试页面了
qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "8080");
m_WebChannel = new QWebChannel(this);
// m_Context 就是 LoginContext
m_WebChannel->registerObject("Context", m_Context);
m_View = new QWebEngineView(this);
// QString path = QCoreApplication::applicationDirPath() + "/login.html";
// m_View->load(QUrl("file:///" + path));
m_View->load(QUrl("qrc:/login.html"));
m_View->page()->setWebChannel(m_WebChannel);
setCentralWidget(m_View);
}
LoginWindow::~LoginWindow()
{
// m_WebChannel->deregisterObject(m_Context);
delete ui;
m_Context = nullptr;
}
LoginContext代码:
#ifndef LOGINCONTEXT_H
#define LOGINCONTEXT_H
#include <QObject>
#include <QVariantMap>
#include <QDebug>
class LoginContext : public QObject
{
Q_OBJECT
public:
explicit LoginContext(QObject *parent = nullptr);
public slots:
void onLogin(const QVariantMap& info); // web端可以直接调用的方法 必须是 slots
signals:
// signal web端可以通过 context.onLoginHandler.connect((info) => {console.log(info);}) 调用并传递一个callback
// 然后 native 中 emit onLoginHandler(info) 来触发
// 实际就相当于 native 调用了 web的方法
void onLoginHandler(const QVariantMap& info);
};
#endif // LOGINCONTEXT_H
#include "logincontext.h"
LoginContext::LoginContext(QObject *parent)
: QObject{parent}
{
}
void LoginContext::onLogin(const QVariantMap& info)
{
qInfo() << "Context: onLogin";
emit onLoginHandler(info);
}
login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LoginWindow</title>
</head>
// 一定需要加载 qwebchannel.js
<script src="qrc:/qtwebchannel/qwebchannel.js"></script>
<body>
<div>
<h2>Hello QWebChannel</h2>
<p>{ name: "QWebChannel", version: 6.6 }</p>
<button onclick="onLogin()">Login</button>
</div>
</body>
<script>
console.log(navigator.userAgent);
const data = { name: "QWebChannel", version: 6.6 };
// channel.objects.Context 保存着所有来自 QObject 的共享信息:属性,slots, signals
new QWebChannel(qt.webChannelTransport, function (channel) {
window.Context = channel.objects.Context;
});
function onLogin() {
window.Context.onLogin(data);
}
</script>
</html>
MainWindow 代码,和 LoginWindow 代码差不多:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QWebEngineView>
#include <QWebChannel>
#include "maincontext.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(MainContext* context, QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QWebEngineView* m_View;
QWebChannel* m_WebChannel;
MainContext* m_Context;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "./ui_mainwindow.h"
MainWindow::MainWindow(MainContext* context, QWidget *parent) :
m_Context(context), QMainWindow(parent), ui(new Ui::MainWindow)
{
// qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "8080");
ui->setupUi(this);
setAttribute(Qt::WA_QuitOnClose, false);
m_WebChannel = new QWebChannel(this);
m_WebChannel->registerObject("Context", m_Context);
m_View = new QWebEngineView(this);
m_View->load(QUrl("qrc:/main.html"));
m_View->page()->setWebChannel(m_WebChannel);
setCentralWidget(m_View);
}
MainWindow::~MainWindow()
{
delete ui;
m_Context = nullptr;
}
MainContext代码:
#ifndef MAINCONTEXT_H
#define MAINCONTEXT_H
#include <QObject>
#include <QVariantMap>
#include <QDebug>
class MainContext : public QObject
{
Q_OBJECT
// 共享给 webchannel 的属性 js中直接可以通过 context.info 获取
Q_PROPERTY(QVariantMap info MEMBER m_Info NOTIFY onInfoChanged)
public:
explicit MainContext(const QVariantMap& info, QObject *parent = nullptr);
public slots:
void setInfo(const QVariantMap& info);
void request(qint32 id, QString message);
signals:
void onInfoChanged(const QVariantMap& info);
void onResponse(qint32 id, const QVariantMap& data);
private:
QVariantMap m_Info;
};
#endif // MAINCONTEXT_H
#include "maincontext.h"
MainContext::MainContext(const QVariantMap& info, QObject *parent)
: m_Info(info), QObject{parent}
{
}
void MainContext::setInfo(const QVariantMap &info)
{
foreach (auto key, info.keys()) {
qInfo() << key << ":" << info.value(key);
}
m_Info = info;
emit onInfoChanged(info);
}
void MainContext::request(qint32 id, QString message)
{
qInfo() << "MainContext: request with id" << id;
qInfo() << "message" << message;
// other logic
QVariantMap data;
data["code"] = 200;
data["message"] = "from native";
emit onResponse(id, data);
}
main.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MainWindow</title>
<script src="qrc:/qtwebchannel/qwebchannel.js"></script>
</head>
<body>
<div>
<h2>Hello QWebChannel</h2>
<p id="info"></p>
<button onclick="onChangeInfo()">change info</button>
<p id="result"></p>
<button onclick="onTest()">Call native method async & callback</button>
</div>
</body>
<script>
console.log(navigator.userAgent);
const manager = new Map();
let id = 0; // should use uuid
const ele = document.querySelector("#info");
const result = document.querySelector("#result");
new QWebChannel(qt.webChannelTransport, function (channel) {
window.Context = channel.objects.Context;
ele.innerText = JSON.stringify(window.Context.info);
window.Context.onResponse.connect(function (id, data) {
const { resolve = undefined } = manager.get(id);
resolve && resolve(data);
});
window.Context.onInfoChanged.connect(function (info) {
alert(JSON.stringify(info));
ele.innerText = JSON.stringify(info);
});
});
function onChangeInfo() {
window.Context.setInfo({ name: "from web", version: 6.6 });
}
function makePromise(callback) {
id += 1;
return new Promise((resolve, reject) => {
callback(id);
manager.set(id, { resolve, reject });
});
}
function onTest() {
makePromise((id) => {
// 通过 id 让 native知道执行哪个 Promise 的回调函数
window.Context.request(id, "from html");
}).then((res) => {
result.innerText = JSON.stringify(res);
});
}
</script>
</html>
以上就是最简单的实现了。