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声明了属性 (非必须,但通常会有) - 使用
signals、slots声明了信号和槽方法(非必须,但通常会有)
当使用 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 对象能自动同步更新2 | 前端读写直接作用到后端,后端更新自动同步到前端 |
| 机制(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 。
简单总结一下就是:
属性的前后端自动同步 + 前端跨语言调用后端方法 + 后端“跨语言”通知前端:三种机制联合实现了前端 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++ 后端环境。运行时,通过一个命令行参数来指定执行哪一个示例程序,以下是执行四个示例的具体命令:
:: 执行示例一 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 代码与关,看以在介绍到相关示例时再回看:
#ifndefCPP_PRACTICES_MYOBJECT_H#defineCPP_PRACTICES_MYOBJECT_H#include<QObject>// 关键点①: 用于前后端交互的对象必须继承自 QObject, 这样才能使用 property、signal、slot 等 Qt 特性// 因为只有被 property、signal、slot 标记的属性和方法才会暴露给前端 Js (也就是在 Js 对象中生成对等的属性和方法)classMyObject:publicQObject{ 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:explicitMyObject(QObject *parent =nullptr); QString message()const;// 关键点④: setMessage 不是 slots,但是前端设置属性时还是会调用到该方法,// 它和 processMessage 的执行途径是一样的,它是被属性配置的 WRITE 方法调用的voidsetMessage(const QString &msg);voidsetMessageWithoutSignal(const QString &msg);voidprocessMessageWithoutSignal(const QString &msg); signals:// 关键点⑤: 所有声明为 signal 的方法,会在前端 Js 对象中生成对等方法voidmessageChanged(const QString &newMessage);voidmessageProcessed(const QString &msg);public slots:// 关键点⑥: 所有声明为 slots 的方法,会在前端 Js 对象中生成对等方法voidprocessMessage(const QString &msg);private: QString m_message;};#endif//CPP_PRACTICES_MYOBJECT_H以下是 myobject.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;}voidMyObject::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;}}voidMyObject::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;}}voidMyObject::processMessage(const QString &msg){// 关键点⑨: 处理消息后发射 messageProcessed 信号。该信号将被前端 js 函数响应,用于展示后端“跨语言”通知前端的交互 emit messageProcessed(msg); std::cout <<"C++ 后端处理了新消息:[ "<< msg.toStdString()<<" ],并发送了消息已处理信号!"<< std::endl;}voidMyObject::processMessageWithoutSignal(const QString &msg){// 关键点⑩: 处理消息但不发射 messageProcessed 信号。前端 js 的响应函数将永远得到机会执行。// emit messageProcessed(msg); std::cout <<"C++ 后端处理了新消息:[ "<< msg.toStdString()<<" ],但未发送消息已处理信号!"<< std::endl;}5.2 注册 QObject 对象
准备好自己的 QObject 后,就要把它注册到 QWebChannel 上,这是关键的一步,经过注册,前端才会得到对应的 Js 对象。这一步发生在主窗体的构造函数中。由于主窗体的包含过多 UI 组件的初始化和设置代码,我们把它的 mainwindow.h 和 mainwindow.cpp 代码贴到了附录中。这里只展示需要关注的 MainWindow 的构造函数:
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:
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><style>.input{width: 400px;height: 25px;}.button{width: 70px;height: 30px;}</style><title>QQWebChannel 示例一: Js / C++ 前后端对象属性双向同步</title><!-- 关键点⑫: 引入 qwebchannel.js --><scriptsrc="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><inputtype="text"id="input-read-message"class="input"placeholder="点击“读取”刷新..."readonly/><buttonid="button-read"class="button">读取</button></p><p><inputtype="text"id="input-written-message"class="input"placeholder="输入新消息后点击“写入”..."/><buttonid="button-write"class="button">写入</button></p><script>// 初始化 QWebChannelnewQWebChannel(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 设置新值,这个值会自动同步到后台
下面,我们就看一下演示:
在演示中:
- 窗口打开后,点击“读取”按钮得到
我是后端 C++ 对象中的初始消息!字样,这是后端初始化 MyObject 时 message 属性的初始值; - 在输入框中输入 1,再点“写入”按钮,后端控制台会先输出第一条消息:
C++ 后端“消息属性”设置了新值:[ 1 ],并发送了消息变更信号!表明 setMessage 方法已被自动调用;接着输出第二条消息:C++ 后端“消息属性”被读取:[ 1 ],这条消息非常有趣,它就是《4. 属性同步与 NOTIFY》介绍的“后端属性更新前端自动同步”的“证明”。因为 QWebChannel 检测到了属性的变化,所以它需要把新值更新到前端 Js 对象里去,所以才发生了这里的读取操作(就是调用了 READ 方法进而调用了 message() 方法。此时再次点击“读取”按钮,我们就得到了刷新后的值 1. - 在页面下方的消息输入框中输入 2,点击“在后端设置“消息属性”并发送 messageChanged 信号”按钮,会在后台看到两条新消息:
C++ 后端“消息属性”设置了新值:[ 2 ],并发送了消息变更信号!和C++ 后端“消息属性”被读取:[ 2 ],这与第2步的情形是一样的,不同之处在于:这次是在从后台直接修改了属性值,这一步就是要演示:当属性在后台被更新时,也能通知到前台自动同步。此时点击“读取”按钮,得到了刷新后的值 2,就印证了我们的结论。 - 在页面下方的消息输入框中继续输入 3, 点击“在后端设置“消息属性”但不发送 messageChanged 信号”按钮,这时你会看到后台只输出了一条消息:
C++ 后端“消息属性”设置了新值:[ 3 ],但未发送消息变更信号!,并没有读取 3 的消息,这说明 QWebChannel 没有自动更新前端 Js 中的属性值,原因就是没有发出 messageChanged 信号,QWebChannel 不知道属性值改了。此时点击“读取”按钮,得到的还是上一次的值 2,就印证了我们的结论。
最后,作为对 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:
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>QWebChannel 示例二: 前端 Js 调用后端 C++ 函数</title><style>.input{width: 400px;height: 25px;}.button{width: 70px;height: 30px;}</style><!-- 引入 qwebchannel.js --><scriptsrc="C:\Lib\Qt\Examples\Qt-6.10.2\webchannel\shared\qwebchannel.js"></script></head><body><h2>QWebChannel 示例二: 前端 Js 调用后端 C++ 函数</h2><p><inputtype="text"id="input-message-to-send"class="input"placeholder="输入新消息后点击“发送”..."/><buttonid="button-send"class="button">发送</button></p><script>// 初始化 QWebChannelnewQWebChannel(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
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>QWebChannel 示例三: 前端 Js 监听后端 C++ 信号</title><style>.input{width: 400px;height: 25px;}.button{width: 70px;height: 30px;}</style><!-- 引入 qwebchannel.js --><scriptsrc="C:\Lib\Qt\Examples\Qt-6.10.2\webchannel\shared\qwebchannel.js"></script></head><body><h2>QWebChannel 示例三: 前端 Js 监听后端 C++ 信号</h2><p><inputtype="text"id="input-message"class="input"placeholder="等待后端 C++ 发送信号..."readonly/></p><script>// 初始化 QWebChannelnewQWebChannel(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:
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>QWebChannel 示例四:Js / C++ 前后端双向联动的完整示例</title><style>.input{width: 400px;height: 25px;}.button{width: 70px;height: 30px;}</style><!-- 引入 qwebchannel.js --><scriptsrc="C:\Lib\Qt\Examples\Qt-6.10.2\webchannel\shared\qwebchannel.js"></script></head><body><h2>QWebChannel 示例四:Js / C++ 前后端双向联动的完整示例</h2><p> 当前消息:<inputtype="text"id="input-message"class="input"placeholder="等待刷新消息..."readonly/></p><p><inputtype="text"id="input-message-to-send"class="input"placeholder="输入新消息后点击“发送”..."/><buttonid="button-send"class="button">发送</button></p><script>// 初始化 QWebChannelnewQWebChannel(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
#include"mainwindow.h"#include<QApplication>using std::string;intmain(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
#ifndefCPP_PRACTICES_MAINWINDOW_H#defineCPP_PRACTICES_MAINWINDOW_H#include<QLineEdit>#include<QMainWindow>#include<QPushButton>#include<QVBoxLayout>#include<QWebEngineView>#include<QWebChannel>#include"myobject.h"using std::string, std::unordered_map;classMainWindow:publicQMainWindow{ Q_OBJECT public:MainWindow(QWidget *parent,char* exampleName);// ~MainWindow();voidsetMessageWithoutSignal();voidprocessMessageWithoutSignal();public slots:voidsetMessage();voidprocessMessage();private:voidinitUi(const string example);voidinitConnections();staticconst QHash<QString, QString> examples; QWebEngineView *webView; QWebChannel *webChannel; MyObject *myObject; QLineEdit *lineEditMessage; QPushButton *btnSetMsg; QPushButton *btnSetMsgWithoutSignal; QPushButton *btnProcessMsg; QPushButton *btnProcessMsgWithoutSignal;};#endif//CPP_PRACTICES_MAINWINDOW_H6.3 mainwindown.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)));}voidMainWindow::initUi(const string exampleName){// 三个核心关注组件 webView =newQWebEngineView(this); webChannel =newQWebChannel(this); myObject =newMyObject(this);// 其他 UI 组件 lineEditMessage =newQLineEdit(this); lineEditMessage->setObjectName("lineEditMessage"); btnSetMsg =newQPushButton("在后端设置“消息属性”并发送 messageChanged 信号",this); btnSetMsgWithoutSignal =newQPushButton("在后端设置“消息属性”但不发送 messageChanged 信号",this); btnProcessMsg =newQPushButton("在后端处理消息并发送 messageProcessed 信号",this); btnProcessMsgWithoutSignal =newQPushButton("在后端处理消息但不发送 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 =newQWidget(this); centralWidget->setLayout(mainLayout);setCentralWidget(centralWidget);resize(600,500);if(exampleName =="example1"){ btnProcessMsg->setVisible(false); btnProcessMsgWithoutSignal->setVisible(false);}elseif(exampleName =="example2"){ btnSetMsg->setVisible(false); btnSetMsgWithoutSignal->setVisible(false); btnProcessMsg->setVisible(false); btnProcessMsgWithoutSignal->setVisible(false); lineEditMessage->setVisible(false);}elseif(exampleName =="example3"){ btnSetMsg->setVisible(false); btnSetMsgWithoutSignal->setVisible(false);}elseif(exampleName =="example4"){ btnSetMsg->setVisible(false); btnSetMsgWithoutSignal->setVisible(false); btnProcessMsg->setVisible(false); btnProcessMsgWithoutSignal->setVisible(false); lineEditMessage->setVisible(false);}}voidMainWindow::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);}voidMainWindow::setMessage(){ QString inputText = lineEditMessage->text();if(!inputText.isEmpty()){// myObject->setProperty("message",inputText); myObject->setMessage(inputText); lineEditMessage->clear();}else{qDebug()<<"提示:输入文本为空!";}}voidMainWindow::setMessageWithoutSignal(){ QString inputText = lineEditMessage->text();if(!inputText.isEmpty()){ myObject->setMessageWithoutSignal(inputText); lineEditMessage->clear();}else{qDebug()<<"提示:输入文本为空!";}}voidMainWindow::processMessage(){ QString inputText = lineEditMessage->text();if(!inputText.isEmpty()){ myObject->processMessage(inputText); lineEditMessage->clear();}else{qDebug()<<"提示:输入文本为空!";}}voidMainWindow::processMessageWithoutSignal(){ QString inputText = lineEditMessage->text();if(!inputText.isEmpty()){ myObject->processMessageWithoutSignal(inputText); lineEditMessage->clear();}else{qDebug()<<"提示:输入文本为空!";}}- 这个透明的自动更新机制是:属性在声明时必须设置 NOTIFY,且当后端 C++ 对象中变更时要发射 NOTIFY 指定的属性值变更信号,当 QWebChanel 接收到这个信号时,发现与属性 NOTIFY 指定的信号一致,则说明:这个值发生了变更, QWebChanel 需要立即更新前端 Js 对象的属性值。这个过程是透明的,但如果没有 NOTIFY 声明或没有发出对应的信号,自动更新就不会发生。 ↩︎
- 是有条件的,属性必须设置了 NOTIFY 且在后端 C++ 对象中变更时要发射 NOTIFY 指定的属性值变更信号(就是 [1] 中描述的属性更新机制) ↩︎
- 关于后端对前端“可操作”性的实现,Qt 为什么没有仿照机制<2>让后端“有能力”直接调用前端的方法呢?关于这个问题我也思考过,不过限于我目前掌握的 Qt 知识不足以得出确定的结论。不过,有这样一些论点或许有一定道理:首先,从设计上这可能就不是一个好想法,因为前后端都可以直接互操作对方时,会不会发生“死循环”问题?其次,我也不知道在 C++ 环境会操纵一个远程的 Js 对象这在技术上是否可行。 ↩︎