Qt中基于QWebEngineView和QWebChannel实现与web的交互

4,136 阅读4分钟

前言

首先看一下效果(因为掘金不支持上传视频预览,所以贴出 github 的视频链接): github.com/1111mp/ForQ…

也可以直接访问 github 源码查看: github.com/1111mp/ForQ…

图片:

截屏2023-05-29 10.37.22.png

截屏2023-05-29 10.37.39.png

目标

  1. 实现 LoginWindowMainWindow,LoginWindow 中点击登录关闭 LoginWindow,打开跳转到MainWindow
  2. 通过 QWebEngineView 实现在 QMainWindow 中加载 web 页面
  3. 基于 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>

以上就是最简单的实现了。