Qt 前后端通信(QWebChannel Js / C++ 互操作):原理、示例、步骤解说

Qt 提供的 QWebEngineView 是一个基于 Chromium 内核的浏览器组件,通过它,开发者可以使用 HTML、CSS、JavaScript 等技术开发 Web 页面并呈现在 Qt 桌面应用中,但与开发纯 Web 页面不同的是,这些页面通常需要和 应用中的其他组件交互,例如获取后端数据进行渲染、将前端用户指令传达给后端执行等,这将不可避免地涉及到前端 Js 和 后端 C++ 之间的交互问题,而 Qt 为此给出的解决方案就是 QWebChannel,通过 QWebChannel 前端 Web 页面和与后端 C++ 程序实现自然而顺畅的交互,甚至前后端的操作风格都极为一致。本文我们将细致地介绍QWebChannel 前后端交互的原理,通过四个详实的示例程序讲解每一步重要的操作步骤,通过本文,你将对 QWebChannel 有一个全面而深入的了解。

1. 工作原理

QWebChannel 的工作原理并不复杂,官方文档只用了很少的文字来解释:QWebChannel 填补了 C++ 应用程序与 HTML/JavaScript 应用程序之间的空白。通过将 一个 QObject 派生对象发布到 QWebChannel,并在 HTML 端使用 qwebchannel.js,就可以透明地访问QObject 的属性、信号和槽方法。无需手动传递消息和序列化数据,C++ 端的属性更新和信号发射会自动传输到可能远程运行的 HTML 客户端。这里,我用更通俗易懂的方式重新描述一下:

你可以提供一个自己的 QObject 对象,在这个 QObject 中,你定义了一些属性(Q_PROPERTY),一些 signals 和 slots 方法,并把它注册给 QWebChannel;然后,在前端,你只需引入 qwebchannel.js 文件,就可以在 Js 环境里得到一个与后端 C++ 对象几乎一模一样的 Js 对象,你可以把这个 Js 对象视作你后端那个 QObject 对象在前端的"代理类",当你在前端读写 Js 对象的属性时实际上读写的是后端 C++ 对象的属性,当你在前端调用的 Js 对象的方法时实际上调用的是后端 C++ 对象的方法,也就是说,你在前端读写这个 Js 对象的属性或调用它的方法都会远程作用到后端的 C++ 对象上。

总得来说,QWebChannel 使得 Qt 应用具有了"跨前后端"、"跨语言"的交互能力,在前端的 Js 环境中可以直接读取、更新后端 C++ 对象中的数据(属性),也可以直接调用 C++ 对象的成员函数,当后后端发生变化需要前端响应时,又可以把信号发送到前端驱动前端的更新。

2. 注册对象

以上介绍的工作原理全部体现在了"注册对象"上,所谓"注册对象"就是在 C++ 环境中用户自定义的一个类,这个类通常有如下特征:

  • 继承自 QObject,声明了 Q_OBJECT (必须)
  • 使用 Q_PROPERTY 声明了属性 (非必须,但通常会有)
  • 使用 signalsslots 声明了信号和槽方法(非必须,但通常会有)

当使用 QWebChannel::registerObject() 方法注册了这个对象后,它就是一个"注册对象"了,完成注册后,用户就能在前端环境中得到一个与注册对象高度相似的 Js 对象,这个 Js 对象有和 C++ 对象一样的属性和信号/槽方法,用户在前端读写这个 Js 对象的属性时,实际上读写的是后端 C++ 对象的属性,用户在前端调用这个 Js 对象的槽函数时,实际会带着前端的参数传递到后端去调用 C++ 对象上的槽函数,用户甚至可以直接将后端 C++ 对象的"信号"绑定到前端 Js 方法中响应。QWebChannel 就是通过这个"注册对象"以及根据它生成的前端 Js 对等对象来实现前后端的互操作的。

3. 互操作机制

前后端的互操作就是 C++ 对象和 Js 对象间的互操作,下表详细地说明了注册对象后,C++ 对象和 Js 对象之间的"互操作"性:

编号 C++ 对象 Js 对象 使用方式 交互形式
机制(1) 所有 Q_PROPERTY 声明的属性 有对等属性 ① 向 Js 对象写入属性值就是向 C++ 对象写入属性值(存在一次 前端 🠚 后端 的远程交互); ② 从 Js 对象读取属性值时,并不会发起远程交互去取后端 C++ 对象的属性值,但 Qt 有透明的自动更新机制保证前端属性值与后端 C++ 对象始终一致^[1](#编号 C++ 对象 Js 对象 使用方式 交互形式 机制(1) 所有 Q_PROPERTY 声明的属性 有对等属性 ① 向 Js 对象写入属性值就是向 C++ 对象写入属性值(存在一次 前端 🠚 后端 的远程交互); ② 从 Js 对象读取属性值时,并不会发起远程交互去取后端 C++ 对象的属性值,但 Qt 有透明的自动更新机制保证前端属性值与后端 C++ 对象始终一致1; ③ 后端 C++ 对象更新属性时,前端 Js 对象能自动同步更新2 前端读写直接作用到后端,后端更新自动同步到前端 机制(2) 所有 singal 方法 有对等方法 在 Js 端会被 connect 到 Js 的事件响应函数上,用于在前端响应/处理后端发来的信号(存在一次 后端 🠚 前端 的远程交互) 后端“跨语言”通知前端 机制(3) 所有 slot 方法 有对等方法 通常是在 Js 的事件响应函数内调用,用于将前端事件/数据传回到后端处理(存在一次 前端 🠚 后端 的远程交互) 前端“跨语言”调用后端)^; ③ 后端 C++ 对象更新属性时,前端 Js 对象能自动同步更新^[2](#编号 C++ 对象 Js 对象 使用方式 交互形式 机制(1) 所有 Q_PROPERTY 声明的属性 有对等属性 ① 向 Js 对象写入属性值就是向 C++ 对象写入属性值(存在一次 前端 🠚 后端 的远程交互); ② 从 Js 对象读取属性值时,并不会发起远程交互去取后端 C++ 对象的属性值,但 Qt 有透明的自动更新机制保证前端属性值与后端 C++ 对象始终一致1; ③ 后端 C++ 对象更新属性时,前端 Js 对象能自动同步更新2 前端读写直接作用到后端,后端更新自动同步到前端 机制(2) 所有 singal 方法 有对等方法 在 Js 端会被 connect 到 Js 的事件响应函数上,用于在前端响应/处理后端发来的信号(存在一次 后端 🠚 前端 的远程交互) 后端“跨语言”通知前端 机制(3) 所有 slot 方法 有对等方法 通常是在 Js 的事件响应函数内调用,用于将前端事件/数据传回到后端处理(存在一次 前端 🠚 后端 的远程交互) 前端“跨语言”调用后端)^ 前端读写直接作用到后端,后端更新自动同步到前端
机制(2) 所有 singal 方法 有对等方法 在 Js 端会被 connect 到 Js 的事件响应函数上,用于在前端响应/处理后端发来的信号(存在一次 后端 🠚 前端 的远程交互) 后端"跨语言"通知前端
机制(3) 所有 slot 方法 有对等方法 通常是在 Js 的事件响应函数内调用,用于将前端事件/数据传回到后端处理(存在一次 前端 🠚 后端 的远程交互) 前端"跨语言"调用后端

上述对三种机制的描述是非常精准的,就不再重复解读了。我们要分析的是:这三种机制是如何把前后端互操作的"闭环"补全的:

➢ 仅通过 机制(2) + 机制(3) 就可以实现前后端的互操作了,机制(1)可以视为前后端"数据同步"的一种便捷方式;
➢ 通过 机制(1) + 机制(3) 使得 Js 对象像是 C++ 对象在前端的一个"远程代理",前端对 Js 对象属性的读写实际会通过远程交互读写 C++ 对象本身,前端对 Js 对象方法(slot或invokable方法)的调用也是通过远程交互调用到 C++ 对象本身,机制(1) + 机制(3) 实现了:前端对后端的完全可操作性 ;
➢ 通过 机制(2)(也包含 机制(1) 中后端数据更新自动同步至前端的功能),以事件驱动的方式实现了:后端对前端的间接可操作性[3](#3)
简单总结一下就是:

属性的前后端自动同步 + 前端跨语言调用后端方法 + 后端"跨语言"通知前端:三种机制联合实现了前端 Js 环境和后端 C++ 环境的互操作

4. 属性同步与 NOTIFY

首先必须要说明:这个章节的内容其实应该是《3. 互操作机制》的一个子章节,因为我们是把属性同步中的一个子场景:"后端属性更新前端自动同步"单独拿出来再深入地解释一下它的工作机制,由于这部分内容又细又难解释,放到第 3 节不利于读者建立整体逻辑框架,所以单独拿出来在这一节介绍。

我们再重新回看一下第3节互操作性表格中关于属性同步的第② ③ 两点的描述:

②:从 Js 对象读取属性值时,并不会发起远程交互去取后端 C++ 对象的属性值,但 Qt 有透明的自动更新机制保证前端属性值与后端 C++ 对象始终一致

③:后端 C++ 对象更新属性时,前端 Js 对象能自动同步更新

上述两点其实说的是一件事,只不过③是②的因,②是③的果。我们把后端属性更新时发生的事情再详细地解释一下:

当后端 C++ 对象属性更新时,QWebChannel 是可以检测到的,一但它发现属性变更了就会自动更新前端 Js 对象中的属性,这样就可以保证前端属性值能始终和后端保持一致,也因为有这样的保障,所以在前端每次读取 Js 属性时,是不用也不会和后端进行远程交互的。以上逻辑全部成立的前提是:QWebChannel 能检测到 C++ 对象的属性发生了变更,而这需要两个条件:(1) 属性的 setter 方法里必须发射属性值变更的信号;(2) 属性的配置声明里必须使用 NOTIFY 指明"标志着属性值发生变更的是哪个信号",两者缺一不可。

而上述解释也是对 NOTIFY 作用的解释,从 NOTIFY 的角度再阐述一遍就是:

NOTIFY 的作用是: 告知属性的相关方(使用者/维护者,例如 QWebChannel),只有在检测到指定的信号发出时,才认定属性发生了变改更!如未设置 NOTIFY 信号,不管发出何种信号都不会认为属性发生了变更。所以,如果我们没有配置 NOTIFY,无论后端 C++ 怎样修改属性,发不发送信号,前端的 Js 属性都不会自动更新!因为 QWebChannel 会认为:属性就是没有"变",因为用户没告诉它:检测到什么信号时才认定属性变了。

上述解释已经非常准确了,如果你还是觉得不太清楚,没关系,在《5.3 Js / C++ 前后端对象属性双向同步》一节中,我们会用示例来演示,到时可以结合程序的运行结果再回过头来重新理解。

5. 示例项目

为了清晰地说明和演示 QWebChannel 的功能,本文特意编写了一个示例项目 qwebchannel-demo,项目包含如下文件:

复制代码
qwebchannel-demo
    ├── CMakeLists.txt
    ├── example1.html
    ├── example2.html
    ├── example3.html
    ├── example4.html
    ├── main.cpp
    ├── mainwindow.cpp
    ├── mainwindow.h
    ├── myobject.cpp
    └── myobject.h

这个示例项目一共设计了四个示例:

  • 示例一:Js / C++ 前后端对象属性双向同步

  • 示例二:前端 Js 调用后端 C++ 函数

  • 示例三:前端 Js 监听后端 C++ 信号

  • 示例四:Js / C++ 前后端双向联动的完整示例

项目中的四个 html 文件分别对应上述四个示例,它们使用同一套 C++ 后端环境。运行时,通过一个命令行参数来指定执行哪一个示例程序,以下是执行四个示例的具体命令:

cmd 复制代码
:: 执行示例一
qwebchannel-demo.exe example1
:: 执行示例二
qwebchannel-demo.exe example2
:: 执行示例三
qwebchannel-demo.exe example3
:: 执行示例四
qwebchannel-demo.exe example4

以下是示例一的界面,要特别提醒一点:我们的示例要在同一个界面上展示 JavaScript 和 C++ 前后端互操作性,要区分清楚哪里是前台 Js 环境,哪里是后台 C++ 环境:

在介绍具体的示例前,我们需要先学习如何创建一个 QObject 并注册给 QWebChannel,这是四个示例能正常工作的前提。

5.1 创建 QObject 对象

作为实现前后端通信的第一步,也是最核心的一步,我们得先创建一个自己的 QObject,它是前后端通信的主要"载体",这个对象的命名最好能体现一定的业务属性,不过在示例中,我们就简单地使用 MyObject 来命名了。这个对象有以个几个值得注意的地方:

  • 继承自 QObject 且声明了 Q_OBJECT 宏
  • 一个 message 属性:将会用于展示属性的前后端同步
  • 一组设置 message 的方法:
    • setMessage,会发射 messageChanged 信号,它同时是 message 属性指定的 setter
    • setMessageWithoutSignal,不会发射 messageChanged 信号
  • 一组处理 message 的方法:
    • processMessage,会发射 messageProcessed 信号,是一个 slot
    • processMessageWithoutSignal,不会发射 messageProcessed 信号
  • 二个信号:
    • messageProcessed:配置在 message 属性的 NOTIFY 中,作为认定 message 属性值变更的信号,在 setMessage 方法中发射
    • messageProcessed:与属性无关,普通信号,在 processMessage 方法中发射

关于 setMessage / setMessageWithoutSignal 和 processMessage / processMessageWithoutSignal 两组方法的说明:只看方法的实现逻辑,其实没有区别,提供这两组方法的原因是:setMessage 并不一个普通方法,而是 message 的 setter,它将主要用于演示前后端的属性同步,为了和前端调用后端 C++ 方法的情形区分开,我们才引入了 processMessage。此外,还应该注意:setMessage 和 processMessage 它们被调用的机制(途径)是不一样的,processMessage 是一个 slot,是在 Js 端被直接调用的,而 setMessage 却不是一个 slot,但它确实也会被调用,它是在进行前后端属性同步时,由 QWebChannel 执行 WRITE 方法时被自动调用的。

以下是 myobject.h 的代码,最为重要的信息都写在了注释中,部分代码与稍后介绍的前端 Js 代码与关,看以在介绍到相关示例时再回看:

cpp 复制代码
#ifndef CPP_PRACTICES_MYOBJECT_H
#define CPP_PRACTICES_MYOBJECT_H

#include <QObject>

// 关键点①: 用于前后端交互的对象必须继承自 QObject, 这样才能使用 property、signal、slot 等 Qt 特性
// 因为只有被 property、signal、slot 标记的属性和方法才会暴露给前端 Js (也就是在 Js 对象中生成对等的属性和方法)
class MyObject : public QObject {
    Q_OBJECT
    // 关键点②: 声明一个 message 属性,该属性也会在前端 Js 对象的中自动生成
    //
    // READ message: 保证了前端 Js 对象可以直接读取到后端 C++ 对象的 message 值。技术细节是:
    //               当 JS 读取 myObject.message 时 → QWebChannel 自动调用 C++ 端该属性绑定的 READ 方法(message()),并返回结果
    //
    // WRITE setMessage: 保证了前端 Js 对象设置的新值会直接同步到后端 C++ 对象的 message 值。技术细节是:
    //                   JS 设置 myObject.message → QWebChannel 自动调用 C++ 端该属性绑定的 WRITE 方法(setMessage()),设置新值
    //
    // NOTIFY messageChanged: 关键点③: NOTIFY 极易被误解为: 让 Qt 在属性被 WRITE 时自动发射它声明的信号(也就是这里的 messageChanged)。
    //                        这是完全错误的!NOTIFY 的真实作用是: 告知属性的相关方(使用者/维护者),只有在检测到指定的信号发出时,才认定
    //                        属性发生了变改更!如未设置 NOTIFY 信号,不管发出何种信号都不会认为属性发生了变更。这有什么用呢?实际上,它只是
    //                        "声明"了一个"规则",这个"规则"单独拿出来没有任何意义,只有放在上下文中才有意义。它其实是一整套自动化属性维护
    //                        机制的中"一环",而这"一还"是需要拿出来让用户"指定"的。以 QWebChannel 为例,它需要维护属性在前后端对象上的
    //                        同步,这是一个自动化机制,用户不能干预,但这里面有一个"环节"是要由用户来"指定"的,那就是:QWebChannel 只在
    //                        检测到属性发生变化时才会把新值同步到前端,那"按什么规则"判定属性发生了变更是应该留给用户来设置的,设置这个规则
    //                        的"窗口"就是 NOTIFY!如果 NOTIFY 缺失,你会发现,即使在 setMessage() 中 emit messageChanged, 前端
    //                        Js 对象的属性都不会自动更新,这一点会在"示例一"中得到体现。
    Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged)

public:
    explicit MyObject(QObject *parent = nullptr);
    QString message() const;
    // 关键点④: setMessage 不是 slots,但是前端设置属性时还是会调用到该方法,
    // 它和 processMessage 的执行途径是一样的,它是被属性配置的 WRITE 方法调用的
    void setMessage(const QString &msg);
    void setMessageWithoutSignal(const QString &msg);
    void processMessageWithoutSignal(const QString &msg);

signals: // 关键点⑤: 所有声明为 signal 的方法,会在前端 Js 对象中生成对等方法
    void messageChanged(const QString &newMessage);
    void messageProcessed(const QString &msg);

public slots: // 关键点⑥: 所有声明为 slots 的方法,会在前端 Js 对象中生成对等方法
    void processMessage(const QString &msg);

private:
    QString m_message;
};

#endif //CPP_PRACTICES_MYOBJECT_H

以下是 myobject.cpp 的代码:

cpp 复制代码
#include "myobject.h"
#include <iostream>

MyObject::MyObject(QObject *parent) : QObject(parent), m_message("我是后端 C++ 对象中的初始消息!") {}

QString MyObject::message() const {
    std::cout << "C++ 后端"消息属性"被读取:[ " << m_message.toStdString()  << " ]" << std::endl;
    return m_message;
}

void MyObject::setMessage(const QString &msg) {
    if (m_message != msg) {
        m_message = msg;
        // 关键点⑦: 在后端变更消息属性后,必须要手动发射 messageChanged 信号,这是保证前端 Js 对象的 message 属性能及时同步
        // 的二个关键点之一,另一个关键点是关键点③,只发射信号而不通过 NOTIFY 指定信号,前端一样不会更新,两个条件缺一不可!
        emit messageChanged(m_message);
        std::cout << "C++ 后端"消息属性"设置了新值:[ " << m_message.toStdString() << " ],并发送了消息变更信号!" << std::endl;
    }
}

void MyObject::setMessageWithoutSignal(const QString &msg) {
    if (m_message != msg) {
        m_message = msg;
        // 关键点⑧: 在后端变更消息属性后,没有发射 messageChanged 信号,前端 Js 对象的 message 属性永远不会更新
        // emit messageChanged(m_message);
        std::cout << "C++ 后端"消息属性"设置了新值:[ " << m_message.toStdString() << " ],但未发送消息变更信号!" << std::endl;
    }
}

void MyObject::processMessage(const QString &msg) {
    // 关键点⑨: 处理消息后发射 messageProcessed 信号。该信号将被前端 js 函数响应,用于展示后端"跨语言"通知前端的交互
    emit messageProcessed(msg);
    std::cout << "C++ 后端处理了新消息:[ " << msg.toStdString() << " ],并发送了消息已处理信号!" << std::endl;
}

void MyObject::processMessageWithoutSignal(const QString &msg) {
    // 关键点⑩: 处理消息但不发射 messageProcessed 信号。前端 js 的响应函数将永远得到机会执行。
    // emit messageProcessed(msg);
    std::cout << "C++ 后端处理了新消息:[ " << msg.toStdString() << " ],但未发送消息已处理信号!" << std::endl;
}

5.2 注册 QObject 对象

准备好自己的 QObject 后,就要把它注册到 QWebChannel 上,这是关键的一步,经过注册,前端才会得到对应的 Js 对象。这一步发生在主窗体的构造函数中。由于主窗体的包含过多 UI 组件的初始化和设置代码,我们把它的 mainwindow.hmainwindow.cpp 代码贴到了附录中。这里只展示需要关注的 MainWindow 的构造函数:

cpp 复制代码
MainWindow::MainWindow(QWidget *parent, char* exampleName) : QMainWindow(parent) {
    initUi(exampleName);
    initConnections();
    // 关键点⑪: 向 WebChannel 注册 myObject. 注册后,前端会生成一个对等 JS 对象
    // 这个 Js 对象将具有 myObject 的属性、信号和槽方法。
    webChannel->registerObject(QStringLiteral("myObject"), myObject);
    webView->page()->setWebChannel(webChannel);
    webView->setUrl(QUrl::fromLocalFile(examples.value(exampleName)));
}

5.3 Js / C++ 前后端对象属性双向同步

现在,我来看示例一:Js / C++ 前后端对象属性双向同步,对应文件是 example1.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <style> .input { width: 400px; height: 25px;} .button { width: 70px; height: 30px;} </style>
    <title>QQWebChannel 示例一: Js / C++ 前后端对象属性双向同步</title>
    <!-- 关键点⑫: 引入 qwebchannel.js -->
    <script src="C:\Lib\Qt\Examples\Qt-6.10.2\webchannel\shared\qwebchannel.js"></script>
</head>
<body>
<h2>QWebChannel 示例一: Js / C++ 前后端对象属性双向同步</h2>
<p>前端 myObject.message 的值:</p>
<p>
    <input type="text" id="input-read-message" class="input" placeholder="点击"读取"刷新..." readonly/>
    <button id="button-read" class="button">读取</button>
</p>
<p>
    <input type="text" id="input-written-message" class="input" placeholder="输入新消息后点击"写入"..."/>
    <button id="button-write" class="button">写入</button>
</p>
<script>
    // 初始化 QWebChannel
    new QWebChannel(qt.webChannelTransport, function (channel) {

        // 关键点⑬: 前端获取注册对象的 Js 对等对象
        const myObject = channel.objects.myObject;

        document.getElementById('button-read').onclick = function () {
            // 关键点⑭: 读取 Js 对象的属性实际上是读取的 C++ 对象上的 message 的值
            // 但通常这并不会发起一次远程交互,而是后端对象的属性发生变更时,会自动更新到前端,
            // 正是因为有这样一种自动更新机制,才保证了前端属性值能与后端属性值时刻保持一致。
            document.getElementById('input-read-message').value = myObject.message;
        }

        document.getElementById('button-write').onclick = function () {
            const msg = document.getElementById('input-written-message').value;
            if (msg) {
                // 关键点⑮: 从前端向 Js 对象的属性写入新值,这会发起一次远程交互,把新值同步写到后端
                // C++ 对象的属性上。这是Js / C++ 前后端对象属性双向同步机制的一部分:前端写入,自动同步到后端
                myObject.message = msg; // 执行时会触发后端执行 setMessage() 方法, 但这不属于直接调用后端函数,调用后端函数是示例三。
                document.getElementById('input-written-message').value = '';
            }
        };
    });
</script>
</body>
</html>

首先,要介绍一下四个示例都会有的一些"例行性"操作:

  • 关键点⑫ 引入 qwebchannel.js 文件,这是配合 QWebChannel 工作的前端 Js 库,必须要引入。这个文件在本地 Qt 安装目录下可以找到,位置是:Qt 安装目录\Examples\Qt-6.10.2\webchannel\shared\qwebchannel.js,你可以将其拷贝到工程中,也可以直接引用原始文件
  • 关键点⑬: 前端获取注册对象的 Js 对等对象。如前面介绍原理时所说的那样,QWebChannel 会根据注册对象生成一个对等的 Js 对象,这里获得的这个 myObject 就是了。建议就用后端 C++ 对象的名字给它取名,因为它们有相同的属性和方法,使用的方式也很像,Js 端的这个对象就像是 C++ 对象的一个"远程代理"

然后,我们再来看示例一独有的、要展示的特性:Js / C++ 前后端对象属性双向同步,解释如下:

  • 关键点⑭:点击"读取"按钮会重新读取 myObject 的 message 属性。当后台属性更新时,我们要通过这个按钮查看前端 Js 对象的属性有没有同步刷新。
  • 关键点⑮:在前台给 myObject 的 message 设置新值,这个值会自动同步到后台

下面,我们就看一下演示:

在演示中:

  1. 窗口打开后,点击"读取"按钮得到我是后端 C++ 对象中的初始消息!字样,这是后端初始化 MyObject 时 message 属性的初始值;

  2. 在输入框中输入 1,再点"写入"按钮,后端控制台会先输出第一条消息:C++ 后端"消息属性"设置了新值:[ 1 ],并发送了消息变更信号!表明 setMessage 方法已被自动调用;接着输出第二条消息:C++ 后端"消息属性"被读取:[ 1 ],这条消息非常有趣,它就是[《4. 属性同步与 NOTIFY》](#《4. 属性同步与 NOTIFY》)介绍的"后端属性更新前端自动同步"的"证明"。因为 QWebChannel 检测到了属性的变化,所以它需要把新值更新到前端 Js 对象里去,所以才发生了这里的读取操作(就是调用了 READ 方法进而调用了 message() 方法。此时再次点击"读取"按钮,我们就得到了刷新后的值 1.

  3. 在页面下方的消息输入框中输入 2,点击"在后端设置"消息属性"并发送 messageChanged 信号"按钮,会在后台看到两条新消息:C++ 后端"消息属性"设置了新值:[ 2 ],并发送了消息变更信号!
    C++ 后端"消息属性"被读取:[ 2 ],这与第2步的情形是一样的,不同之处在于:这次是在从后台直接修改了属性值,这一步就是要演示:当属性在后台被更新时,也能通知到前台自动同步。此时点击"读取"按钮,得到了刷新后的值 2,就印证了我们的结论。

  4. 在页面下方的消息输入框中继续输入 3, 点击"在后端设置"消息属性"但不发送 messageChanged 信号"按钮,这时你会看到后台只输出了一条消息:C++ 后端"消息属性"设置了新值:[ 3 ],但未发送消息变更信号!,并没有读取 3 的消息,这说明 QWebChannel 没有自动更新前端 Js 中的属性值,原因就是没有发出 messageChanged 信号,QWebChannel 不知道属性值改了。此时点击"读取"按钮,得到的还是上一次的值 2,就印证了我们的结论。

最后,作为对 NOTIFY 作用的最后一次解释,也是对[《4. 属性同步与 NOTIFY》](#《4. 属性同步与 NOTIFY》)一节最末尾处的回应,我们再做一个实验:在 myobject.h 中将 Q_PROPERTY 声明中的 NOTIFY messageChanged 临时删除,重新编译运行:

程序启动后,先读取消息,显示的是后台台初始值,然后,在页面下方的消息输入框中输入 1, 点击"在后端设置"消息属性"并发送 messageChanged 信号"按钮,这次我们只能在后台看到一条消息:C++ 后端"消息属性"设置了新值:[ 1 ],并发送了消息变更信号!并没有读取 1 的消息,这也说明 QWebChannel 没有自动更新前端 Js 中的属性值,但这次去不是因为没有发出 messageChanged 信号,因为我们没有改动 setMessage() 方法,这次的原因就是:因为 Q_PROPERTY 中没有 NOTIFY messageChanged 声明,QWebChannel 不知道应该在哪一个信号发出时判定属性发生了变更,所以,前端 Js 对象中的属性将永远得不到自动更新,这就是 NOTIFY 作用最直观的体现

5.4 前端 Js 调用后端 C++ 函数

接下来看示例二: 前端 Js 调用后端 C++ 函数,对应文件是 example2.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>QWebChannel 示例二: 前端 Js 调用后端 C++ 函数</title>
    <style> .input { width: 400px; height: 25px;} .button { width: 70px; height: 30px;} </style>
    <!-- 引入 qwebchannel.js -->
    <script src="C:\Lib\Qt\Examples\Qt-6.10.2\webchannel\shared\qwebchannel.js"></script>
</head>
<body>
<h2>QWebChannel 示例二: 前端 Js 调用后端 C++ 函数</h2>
<p>
    <input type="text" id="input-message-to-send" class="input" placeholder="输入新消息后点击"发送"..."/>
    <button id="button-send" class="button">发送</button>
</p>
<script>
    // 初始化 QWebChannel
    new QWebChannel(qt.webChannelTransport, function (channel) {

        const myObject = channel.objects.myObject;

        document.getElementById('button-send').onclick = function () {
            const msg = document.getElementById('input-message-to-send').value;
            if (msg) {
                // 关键点⑯: 调用 Js 对象的方法实际上是通过一次远程交互调用的 C++ 对象上的对应方法
                // 注意:是因为 C++ 对象中的 processMessage 方法被声明为 slot, 所以在 Js 对象才会生成对等的方法,
                // 否则 Js 对象中不会有这个方法,运行时会报错: js: Uncaught TypeError: myObject.processMessage is not a function
                myObject.processMessage(msg);
                document.getElementById('input-message-to-send').value = '';
            }
        };
    });
</script>
</body>
</html>

示例二需要重点关注的是:

  • 关键点⑯: myObject.processMessage(msg) 这个看似是在调用前端对象方法的地方实际会发起一次远程调用,驱动后端的 myObject 执行它的 processMessage() 方法,这也是为什么我们说前端的 Js 对象像是一个后端 C++ 对象的"远程代理"的原因。下面看一下演示视频:

程序启动后,在输入框中输入 1,点击"发送"按钮,后台输出:C++ 后端处理了新消息:[ 1 ],并发送了消息已处理信号!这表明后端的 processMessage 方法被调用了。需要提醒的是:这里演示的后端方法调用机制和示例一中的 setMessage 方法被调用的机制是不一样的,后者是属性赋值时通过 WRITE 方法执行的,而这里是直白的"方法直接调用"。

5.5 前端 Js 监听后端 C++ 信号

示例三: 前端 Js 监听后端 C++ 信号,对应文件是 example3.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>QWebChannel 示例三: 前端 Js 监听后端 C++ 信号</title>
    <style> .input { width: 400px; height: 25px;} .button { width: 70px; height: 30px;} </style>
    <!-- 引入 qwebchannel.js -->
    <script src="C:\Lib\Qt\Examples\Qt-6.10.2\webchannel\shared\qwebchannel.js"></script>
</head>
<body>
<h2>QWebChannel 示例三: 前端 Js 监听后端 C++ 信号</h2>
<p>
    <input type="text" id="input-message" class="input" placeholder="等待后端 C++ 发送信号..." readonly/>
</p>
<script>
    // 初始化 QWebChannel
    new QWebChannel(qt.webChannelTransport, function (channel) {

        const myObject = channel.objects.myObject;

        // 关键点⑰: 在前端 Js 对象的 signal 上使用 connect 连接到一个 Js 本地的 function 上,
        // 这形成了:前端 Js 监听后端 C++ 信号的功能。注意:是因为 C++ 对象中的 processMessage 
        // 方法被声明为 slot, 所以在 Js 对象才会生成对等的方法,否则 Js 对象中不会有这个方法,
        // 运行时会报错: js: Uncaught TypeError: myObject.processMessage is not a function
        myObject.messageProcessed.connect(function (message) {
            document.getElementById('input-message').value = message;
        });
    });
</script>
</body>
</html>

示例二需要重点关注的是:

  • 关键点⑰: 它将一个后端发出的事件绑定到了一个前端的 Js 函数上,给后端"间接"操作前端提供了途径。这里的实现非常优雅,有一种 JS 和 C++ 破壁融合的既视感,因为它的语法是:在 js 里,将一个 C++ 对象的singal connect 到了一个 Js 的 function 上,初看到时会觉得非常神奇。以下是演示视频:

程序启动后,在页面下方的消息输入框中输入 1,点击"在后端处理消息并发送 messageProcessed 信号"按钮,会在后台看到一条新消息:C++ 后端处理了新消息:[ 1 ],并发送了消息已处理信号!,此时,由于前端关键点⑰的操作,该信号发出后,会触发前端的 Js 函数执行,把收到的信息设置到文本框中。这是"后端操作前端"的通路,只不过,它是用事件响应的方式间接实现的。

5.6 Js / C++ 前后端双向联动的完整示例

最后,我们把前三个示例合在一起,实现一个前后端双向联动的完整示例,对应文件是 example4.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>QWebChannel 示例四:Js / C++ 前后端双向联动的完整示例</title>
    <style> .input { width: 400px; height: 25px;} .button { width: 70px; height: 30px;} </style>
    <!-- 引入 qwebchannel.js -->
    <script src="C:\Lib\Qt\Examples\Qt-6.10.2\webchannel\shared\qwebchannel.js"></script>
</head>
<body>
<h2>QWebChannel 示例四:Js / C++ 前后端双向联动的完整示例</h2>
<p>
    当前消息:<input type="text" id="input-message" class="input" placeholder="等待刷新消息..." readonly/>
</p>
<p>
    <input type="text" id="input-message-to-send" class="input" placeholder="输入新消息后点击"发送"..."/>
    <button id="button-send" class="button">发送</button>
</p>
<script>
    // 初始化 QWebChannel
    new QWebChannel(qt.webChannelTransport, function (channel) {

        const myObject = channel.objects.myObject;

        document.getElementById('input-message').value = myObject.message;

        document.getElementById('button-send').onclick = function () {
            const msg = document.getElementById('input-message-to-send').value;
            if (msg) {
                myObject.processMessage(msg);
                document.getElementById('input-message-to-send').value = '';
            }
        };

        myObject.messageProcessed.connect(function (message) {
            document.getElementById('input-message').value = message;
        });
    });
</script>
</body>
</html>

示例四没有需要特别注意的关注点了,因为它的所有代码在此前三个示例中都已出现,我们直接看一下演示视频吧:

程序启动后,第一个文本框会显示前端 Js 对象的 message 属性值,这个值是从后端读取到的属性初始值:
我是后端 C++ 对象中的初始消息!,然后,在第二个文本框中输入 1,点解"发送"按钮,后台输出:C++ 后端处理了新消息:[ 1 ],并发送了消息已处理信号!这表明后端的 processMessage 方法被调用并发送了 messageProcessed 信号。与此同时,由于前端 connect 了这个 messageProcessed 信号,所以又回到前端继续响应,把新的消息值写到第一个文本框,完成了:前端(Js 环境)主动调用 ➪ 后端(C++环境)负责处理 + 发射信号 ➪ 前端(Js 环境)响应处理 的完整流程。

6. 附录 / 补充源代码文件

6.1 main.cpp

cpp 复制代码
#include "mainwindow.h"
#include <QApplication>
using std::string;

int main(int argc, char *argv[])
{
    string example = argv[1];
    if (example == "example1" || example == "example2" 
        || example == "example3" || example == "example4") {
        QApplication a(argc, argv);
        MainWindow w = MainWindow(nullptr, argv[1]);
        w.show();
        return a.exec();
    }
}

6.2 mainwindown.h

cpp 复制代码
#ifndef CPP_PRACTICES_MAINWINDOW_H
#define CPP_PRACTICES_MAINWINDOW_H

#include <QLineEdit>
#include <QMainWindow>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWebEngineView>
#include <QWebChannel>
#include "myobject.h"

using std::string, std::unordered_map;

class MainWindow : public QMainWindow {
    Q_OBJECT

public:
    MainWindow(QWidget *parent, char* exampleName);
    // ~MainWindow();
    void setMessageWithoutSignal();
    void processMessageWithoutSignal();

public slots:
    void setMessage();
    void processMessage();

private:
    void initUi(const string example);
    void initConnections();
    static const QHash<QString, QString> examples;
    QWebEngineView *webView;
    QWebChannel *webChannel;
    MyObject *myObject;
    QLineEdit *lineEditMessage;
    QPushButton *btnSetMsg;
    QPushButton *btnSetMsgWithoutSignal;
    QPushButton *btnProcessMsg;
    QPushButton *btnProcessMsgWithoutSignal;
};

#endif //CPP_PRACTICES_MAINWINDOW_H

6.3 mainwindown.cpp

cpp 复制代码
#include "mainwindow.h"
#include <qcoreapplication.h>
#include <QLabel>
#include <QVBoxLayout>
#include <QRadioButton>
#include <QWebEnginePage>

using std::string;

const  QHash<QString, QString> MainWindow::examples = {
    {"example1","C:/@Workspace/WS_Qt/cpp-practices/qwebchannel-demo/example1.html"},
    {"example2","C:/@Workspace/WS_Qt/cpp-practices/qwebchannel-demo/example2.html"},
    {"example3","C:/@Workspace/WS_Qt/cpp-practices/qwebchannel-demo/example3.html"},
    {"example4","C:/@Workspace/WS_Qt/cpp-practices/qwebchannel-demo/example4.html"}
};

MainWindow::MainWindow(QWidget *parent, char* exampleName) : QMainWindow(parent) {
    initUi(exampleName);
    initConnections();
    // 关键点⑪: 向 WebChannel 注册 myObject. 注册后,前端会生成一个对等 JS 对象
    // 这个 Js 对象将具有 myObject 的属性、信号和槽方法。
    webChannel->registerObject(QStringLiteral("myObject"), myObject);
    webView->page()->setWebChannel(webChannel);
    webView->setUrl(QUrl::fromLocalFile(examples.value(exampleName)));
}

void MainWindow::initUi(const string exampleName) {
    // 三个核心关注组件
    webView = new QWebEngineView(this);
    webChannel = new QWebChannel(this);
    myObject = new MyObject(this);

    // 其他 UI 组件
    lineEditMessage = new QLineEdit(this);
    lineEditMessage->setObjectName("lineEditMessage");
    btnSetMsg = new QPushButton("在后端设置"消息属性"并发送 messageChanged 信号", this);
    btnSetMsgWithoutSignal = new QPushButton("在后端设置"消息属性"但不发送 messageChanged 信号", this);
    btnProcessMsg = new QPushButton("在后端处理消息并发送 messageProcessed 信号", this);
    btnProcessMsgWithoutSignal = new QPushButton("在后端处理消息但不发送 messageProcessed 信号", this);

    QVBoxLayout *mainLayout = new QVBoxLayout;
    mainLayout->addWidget(webView);
    mainLayout->addWidget(lineEditMessage);
    mainLayout->addWidget(btnSetMsg);
    mainLayout->addWidget(btnSetMsgWithoutSignal);
    mainLayout->addWidget(btnProcessMsg);
    mainLayout->addWidget(btnProcessMsgWithoutSignal);

    QWidget *centralWidget = new QWidget(this);
    centralWidget->setLayout(mainLayout);
    setCentralWidget(centralWidget);

    resize(600, 500);

    if (exampleName == "example1") {
        btnProcessMsg->setVisible(false);
        btnProcessMsgWithoutSignal->setVisible(false);
    } else if (exampleName == "example2") {
        btnSetMsg->setVisible(false);
        btnSetMsgWithoutSignal->setVisible(false);
        btnProcessMsg->setVisible(false);
        btnProcessMsgWithoutSignal->setVisible(false);
        lineEditMessage->setVisible(false);
    } else if (exampleName == "example3") {
        btnSetMsg->setVisible(false);
        btnSetMsgWithoutSignal->setVisible(false);
    } else if (exampleName == "example4") {
        btnSetMsg->setVisible(false);
        btnSetMsgWithoutSignal->setVisible(false);
        btnProcessMsg->setVisible(false);
        btnProcessMsgWithoutSignal->setVisible(false);
        lineEditMessage->setVisible(false);
    }
}

void MainWindow::initConnections() {
    connect(btnSetMsg, &QPushButton::clicked, this, &MainWindow::setMessage);
    connect(btnSetMsgWithoutSignal, &QPushButton::clicked, this, &MainWindow::setMessageWithoutSignal);
    connect(btnProcessMsg, &QPushButton::clicked, this, &MainWindow::processMessage);
    connect(btnProcessMsgWithoutSignal, &QPushButton::clicked, this, &MainWindow::processMessageWithoutSignal);
}

void MainWindow::setMessage() {
    QString inputText = lineEditMessage->text();
    if (!inputText.isEmpty()) {
        // myObject->setProperty("message",inputText);
        myObject->setMessage(inputText);
        lineEditMessage->clear();
    } else {
        qDebug() << "提示:输入文本为空!";
    }
}

void MainWindow::setMessageWithoutSignal() {
    QString inputText = lineEditMessage->text();
    if (!inputText.isEmpty()) {
        myObject->setMessageWithoutSignal(inputText);
        lineEditMessage->clear();
    } else {
        qDebug() << "提示:输入文本为空!";
    }
}

void MainWindow::processMessage() {
    QString inputText = lineEditMessage->text();
    if (!inputText.isEmpty()) {
        myObject->processMessage(inputText);
        lineEditMessage->clear();
    } else {
        qDebug() << "提示:输入文本为空!";
    }
}

void MainWindow::processMessageWithoutSignal() {
    QString inputText = lineEditMessage->text();
    if (!inputText.isEmpty()) {
        myObject->processMessageWithoutSignal(inputText);
        lineEditMessage->clear();
    } else {
        qDebug() << "提示:输入文本为空!";
    }
}

  1. 这个透明的自动更新机制是:属性在声明时必须设置 NOTIFY,且当后端 C++ 对象中变更时要发射 NOTIFY 指定的属性值变更信号,当 QWebChanel 接收到这个信号时,发现与属性 NOTIFY 指定的信号一致,则说明:这个值发生了变更, QWebChanel 需要立即更新前端 Js 对象的属性值。这个过程是透明的,但如果没有 NOTIFY 声明或没有发出对应的信号,自动更新就不会发生。 ↩︎

  2. 是有条件的,属性必须设置了 NOTIFY 且在后端 C++ 对象中变更时要发射 NOTIFY 指定的属性值变更信号(就是 [1] 中描述的属性更新机制) ↩︎

  3. 关于后端对前端"可操作"性的实现,Qt 为什么没有仿照机制<2>让后端"有能力"直接调用前端的方法呢?关于这个问题我也思考过,不过限于我目前掌握的 Qt 知识不足以得出确定的结论。不过,有这样一些论点或许有一定道理:首先,从设计上这可能就不是一个好想法,因为前后端都可以直接互操作对方时,会不会发生"死循环"问题?其次,我也不知道在 C++ 环境会操纵一个远程的 Js 对象这在技术上是否可行。 ↩︎

相关推荐
Pu_Nine_91 小时前
JavaScript 字符串与数组核心方法详解
前端·javascript·ecmascript
王老师青少年编程1 小时前
2026年3月GESP真题及题解(C++五级):有限不循环小数
c++·题解·真题·gesp·csp·五级·有限不循环小数
Amnesia0_01 小时前
C++中的IO流
开发语言·c++
2401_891482172 小时前
C++模块化编程指南
开发语言·c++·算法
这是个栗子2 小时前
前端开发中的常用工具函数(六)
javascript·every
码云数智-园园2 小时前
从输入 URL 到页面展示:一场精密的互联网交响乐
前端
暮冬-  Gentle°2 小时前
自定义类型转换机制
开发语言·c++·算法
2301_816651222 小时前
嵌入式C++低功耗设计
开发语言·c++·算法
架构师沉默2 小时前
Java 终于有自己的 AI Agent 框架了?
java·后端·架构