背景说明
该功能实现依赖于官方 Qt WebChannel Standalone Example 示例程序,将官方示例进行改造以熟悉了解其实现原理。
与远程客户端通信
C++应用程序设置一个QWebChannel实例并在其上发布一个
Core
对象。对于远程客户端,打开index.html
文件。两者都显示一个包含接收消息列表的对话框和一个向另一端发送消息的输入框。当用户发送消息时,
Core
会发出Core::sendText()
信号。信号会自动传播到超文本标记语言客户端。当用户在超文本标记语言端输入消息时,会调用Core::receiveText()
。超文本标记语言客户端和C++服务器之间的所有通信都是通过WebSocket完成的。C++实例化QWebSocketServer并将传入的QWebSocket连接包装在QWebChannelAbstractTransport对象中。然后这些对象连接到QWebChannel实例。
实现过程分析
创建基本UI界面
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
QT_BEGIN_NAMESPACE
namespace Ui {
class Dialog;
}
QT_END_NAMESPACE
class Dialog : public QDialog
{
Q_OBJECT
public:
explicit Dialog(QWidget *parent = nullptr);
void displayMessage(const QString &message);
signals:
void sendText(const QString &text);
private slots:
void clicked();
private:
Ui::Dialog *ui;
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
#include "ui_dialog.h"
Dialog::Dialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::Dialog)
{
ui->setupUi(this);
connect(ui->send, &QPushButton::clicked, this, &Dialog::clicked);
}
void Dialog::displayMessage(const QString &message)
{
ui->output->appendPlainText(message);
}
void Dialog::clicked()
{
const QString text = ui->input->text();
if (text.isEmpty())
return;
emit sendText(text);
displayMessage(tr("Sent message: %1").arg(text));
ui->input->clear();
}
dialog.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLineEdit" name="input">
<property name="placeholderText">
<string>Message Contents</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="send">
<property name="text">
<string>Send</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QPlainTextEdit" name="output">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string notr="true">Initializing WebChannel...</string>
</property>
<property name="backgroundVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
UI界面实现的功能很简单,一个QPlainTextEdit
用来显示已发送和已接收到的文本,一个QLineEdit
用来接收用户的输入,还有一个QPushButton
当用户输入完毕后就可以将内容发送给网页端
创建一个通信核心文件
core.h
#ifndef CORE_H
#define CORE_H
#include "dialog.h"
#include <QObject>
// 该类的一个实例通过WebChannel发布,然后HTML客户端可以访问它。
class Core : public QObject
{
Q_OBJECT
public:
Core(Dialog *dialog, QObject *parent = nullptr)
: QObject(parent), m_dialog(dialog)
{
// 构造函数初始化 传入一个UI窗口对象的指针 并将它与信号槽相连接
connect(dialog, &Dialog::sendText, this, &Core::sendText);
}
signals:
// 该信号从c++端发出,并在HTML客户端显示文本。
void sendText(const QString &text);
public slots:
// 这个槽函数从HTML客户端调用,并在服务器端显示文本。
void receiveText(const QString &text)
{
m_dialog->displayMessage(Dialog::tr("Received message: %1").arg(text));
}
private:
Dialog *m_dialog; // 私有化一个UI窗口对象的指针
};
#endif // CORE_H
这个文件的功能是对两端的消息进行中转处理
在main函数中创建通信连接及通信对象注册
main.cpp
#include "dialog.h"
#include "core.h"
#include <QApplication>
// 这两个头文件及源码是从官方案例中直接拿的,直接使用即可
#include "include/websocketclientwrapper.h"
#include "include/websockettransport.h"
#include <QDesktopServices>
#include <QDialog>
#include <QUrl>
#include <QWebChannel>
#include <QWebSocketServer>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 设置QWebSocketServer
QWebSocketServer server(QStringLiteral("QWebChannel Standalone Example Server"), QWebSocketServer::NonSecureMode);
if (!server.listen(QHostAddress::LocalHost, 12345)) {
qFatal("Failed to open web socket server.");
return 1;
}
// 在QWebChannelAbstractTransport对象中包装WebSocket客户端
WebSocketClientWrapper clientWrapper(&server);
// 设置channel
QWebChannel channel;
QObject::connect(&clientWrapper, &WebSocketClientWrapper::clientConnected,
&channel, &QWebChannel::connectTo);
// 设置程序运行窗口
Dialog dialog;
// 设置Core并将其发布到QWebChannel
Core core(&dialog);
channel.registerObject(QStringLiteral("core"), &core);
// 使用系统默认浏览器打开index.html文件
// BUILD_DIR在CMakeLists.txt中进行定义的
// 牵扯到使用到的html、channel.js文件也是在CMakeLists.txt中进行拷贝的
QUrl url = QUrl::fromLocalFile(BUILD_DIR "/index.html");
QDesktopServices::openUrl(url);
// 在程序UI窗口中输出一条消息
dialog.displayMessage(Dialog::tr("Initialization complete, opening browser at %1.").arg(url.toDisplayString()));
dialog.show();
return a.exec();
}
CMakeLists.txt文件
cmake_minimum_required(VERSION 3.5)
project(myChannel VERSION 0.1 LANGUAGES CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ####################
# 添加要用到的包 Core WebChannel WebSockets
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Core WebChannel WebSockets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Core WebChannel WebSockets)
# 这里还要将从案例中拿来的源码文件写上 include/websocketclientwrapper.cpp include/websockettransport.cpp
set(PROJECT_SOURCES
include/websocketclientwrapper.cpp
include/websockettransport.cpp
main.cpp
dialog.cpp
dialog.h
dialog.ui
)
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(myChannel
MANUAL_FINALIZATION
${PROJECT_SOURCES}
core.h
index.html
)
# Define target properties for Android with Qt 6 as:
# set_property(TARGET myChannel APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
# ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
if(ANDROID)
add_library(myChannel SHARED
${PROJECT_SOURCES}
)
# Define properties for Android with Qt 5 after find_package() calls as:
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
else()
add_executable(myChannel
${PROJECT_SOURCES}
)
endif()
endif()
# ####################
# 定义程序编译后的文件夹
target_compile_definitions(myChannel PUBLIC
"BUILD_DIR=\"${CMAKE_CURRENT_BINARY_DIR}\""
)
# ####################
# 链接要用到的包 Qt::Core Qt::WebChannel Qt::WebSockets
target_link_libraries(myChannel PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt::Core Qt::WebChannel
Qt::WebSockets)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
if(${QT_VERSION} VERSION_LESS 6.1.0)
set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.myChannel)
endif()
set_target_properties(myChannel PROPERTIES
${BUNDLE_ID_OPTION}
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
# ####################
# 对qwebchannel.js和index.html进行拷贝
file(COPY "index.html" DESTINATION "${CMAKE_BINARY_DIR}")
file(COPY "include/qwebchannel.js" DESTINATION "${CMAKE_BINARY_DIR}")
include(GNUInstallDirs)
install(TARGETS myChannel
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(myChannel)
endif()
完成以上步骤,UI端的准备就结束了
网页端js
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- 一定要在html中引入qwebchannel.js文件,不然无法建立通信 -->
<script type="text/javascript" src="./qwebchannel.js"></script>
<script type="text/javascript">
//BEGIN SETUP
function output(message) {
// 获取网页显示窗口的元素
var output = document.getElementById("output");
// 在网页显示窗口中输出一条消息
output.innerHTML = output.innerHTML + message + "\n";
}
window.onload = function() {
if (location.search != "")
// 获取URL的查询参数中没有包含webChannelBaseUrl
// 匹配格式如 http://www.baidu.com?webChannelVaseUrl=23456
var baseUrl = (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/\.]+)/.exec(location.search)[1]);
else
// 如果没有在URL中匹配到channel的端口就将端口设置为12345
var baseUrl = "ws://localhost:12345";
// 网页显示窗口中输出一条消息
output("Connecting to WebSocket server at " + baseUrl + ".");
// 创建一个socket连接
var socket = new WebSocket(baseUrl);
// 当socket被UI窗口关闭时在浏览器控制台输出一条消息
socket.onclose = function() {
console.error("web channel closed");
};
socket.onerror = function(error) {
console.error("web channel error: " + error);
};
socket.onopen = function() {
output("WebSocket connected, setting up QWebChannel.");
new QWebChannel(socket, function(channel) {
// 使通信核心对象全局可访问
window.core = channel.objects.core;
// 设置当点击网页中的send按钮时将要执行的动作
document.getElementById("send").onclick = function() {
var input = document.getElementById("input");
var text = input.value;
if (!text) {
return;
}
output("Sent message: " + text);
input.value = "";
// 在网页端要调用receiveText来向UI端发送消息
core.receiveText(text);
}
// 当收到UI端发来的消息时应执行的操作
core.sendText.connect(function(message) {
output("Received message: " + message);
});
// 向UI端发送信息说明通信建立完成
core.receiveText("Client connected, ready to send/receive messages!");
// 在网页端显示消息说明通信建立完成
output("Connected to WebChannel, ready to send/receive messages!");
});
}
}
//END SETUP
</script>
<style type="text/css">
html {
height: 100%;
width: 100%;
}
#input {
width: 400px;
margin: 0 10px 0 0;
}
#send {
width: 90px;
margin: 0;
}
#output {
width: 500px;
height: 300px;
}
</style>
</head>
<body>
<textarea id="output"></textarea><br />
<input id="input" /><input type="submit" id="send" value="Send" onclick="javascript:click();" />
</body>
</html>
对案例进行改造
了解了如何使用qt与js进行通信,现在来将官方案例改造。使网页端获取页内内容每获取一条DOM树中的子节点元素就将节点内文本内容发送给UI端,UI端使用借口对其翻译后发送给网页端,网页端收到翻译后的文本将其设置到本节点的文本中,然后继续获取下一个子节点的内容;当全部翻译完后给UI端发送翻译完成的信号,并将翻译后的整个DOM树转换为文本发送给UI端,UI端接收到文本后将其保存到一个新的HTML文件中。
准备HTML文件