PyQt QWebChannel详解-C++与Web页面的无缝双向通信

PyQt QWebChannel详解-C++与Web页面的无缝双向通信

一、QWebChannel详解

1、核心概念

QWebChannel 是 PyQt6 提供的 跨进程通信机制,用于在 C++/Python 后端与前端 JavaScript 之间建立双向通信通道。其核心架构基于发布-订阅模式:

  1. 通信层

    • 使用 WebSocket 协议建立持久连接
    • 默认端口范围:49152-65535(动态分配)
    • 数据序列化格式:JSON-RPC 2.0
  2. 对象注册机制

    通过 registerObject() 将 Python 对象暴露给 JS:

    python 复制代码
    channel.registerObject("pyObj", my_python_object)
  3. 信号槽系统

    • Python → JS:通过 pyqtSignal 触发 JS 回调
    • JS → Python:直接调用注册对象的槽函数

2、完整工作流程

2.1、Python 端初始化

python 复制代码
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebSockets import QWebSocketServer

# 创建 WebSocket 服务器
server = QWebSocketServer("QWebChannel", QWebSocketServer.SslMode.NonSecureMode)
server.listen(port=0)  # 自动分配端口

# 创建通信通道
channel = QWebChannel()
channel.registerObject("backend", BackendObject())  # 注册业务对象

# 连接新客户端
def new_connection():
    socket = server.nextPendingConnection()
    channel.connectTo(socket)  # 绑定通道

server.newConnection.connect(new_connection)

2.2、JavaScript 端连接

html 复制代码
<script src="qwebchannel.js"></script>  <!-- 必须引入官方库 -->
<script>
new QWebChannel(qt.webChannelTransport, function(channel) {
    // 获取 Python 对象
    const backend = channel.objects.backend;
    
    // 调用 Python 方法
    backend.processData("input", result => {
        console.log("Python返回:", result);
    });
    
    // 订阅 Python 信号
    backend.dataUpdated.connect(data => {
        console.log("实时更新:", data);
    });
});
</script>

3、核心功能详解

3.1、 数据类型映射表

Python 类型 JavaScript 类型 特殊处理
int/float Number 自动转换
str String UTF-8 编码
list/dict Array/Object 递归转换
QObject 子类 Proxy Object 保留信号槽机制
datetime Date 时区自动转换

3.2、信号槽连接原理

Python JS Python JS 调用 slot_method(arg) 返回 Promise 对象 触发 signal_name(data)

3.3、异步通信模型

  • 调用超时 :默认 5000ms(可通过 QWebChannel.setTimeout() 修改)

  • 错误处理

    javascript 复制代码
    backend.method().catch(error => {
        console.error("调用失败:", error);
    });

4、高级应用场景

4.1、二进制数据传输

python 复制代码
# Python 端发送二进制
class DataSender(QObject):
    binaryReady = pyqtSignal(bytes)
    
    def send_image(self):
        with open("image.png", "rb") as f:
            self.binaryReady.emit(f.read())
javascript 复制代码
// JS 端接收
backend.binaryReady.connect(data => {
    const blob = new Blob([new Uint8Array(data)]);
    img.src = URL.createObjectURL(blob);
});

4.2、多通道负载均衡

python 复制代码
# 创建多个独立通道
channel1 = QWebChannel()
channel2 = QWebChannel()

# 分别绑定不同业务对象
channel1.registerObject("serviceA", objA)
channel2.registerObject("serviceB", objB)

5、调试与优化

  1. 日志监控

    启用调试模式:

    python 复制代码
    QLoggingCategory.setFilterRules("qt.webchannel* = true")
  2. 性能优化技巧

    • 使用 @pyqtSlot 装饰器明确槽函数

    • 大数据分块传输:

      python 复制代码
      @pyqtSlot(int)
      def request_chunk(seq):
          return large_data[seq*1024: (seq+1)*1024]
  3. 安全机制

    • 启用 SSL 加密:

      python 复制代码
      server = QWebSocketServer("", QWebSocketServer.SslMode.SecureMode)
      server.setSslConfiguration(ssl_config)
    • 访问白名单控制:

      python 复制代码
      def new_connection():
          if server.remoteAddress() not in allowed_ips:
              socket.close()

6、典型错误处理

错误类型 解决方案
ObjectNotFound 检查 registerObject() 的 ID 一致性
TypeError 验证参数类型匹配
ConnectionRefused 检查防火墙/端口占用
SignalNotConnected 确保 JS 端已 connect()

最佳实践 :生产环境中建议结合 QWebEngineView 使用嵌入式方案:

python 复制代码
view = QWebEngineView()
page = view.page()
page.setWebChannel(channel)  # 自动注入 qwebchannel.js
view.setHtml(html_content)

二、代码示例

1、库安装

2、源码分享

python 复制代码
import sys

from PIL import Image
import numpy as np
from PyQt6.Qsci import QsciScintilla, QsciAPIs, QsciLexerPython, QsciLexerJSON
from PyQt6.QtCore import QDir
from PyQt6.QtGui import QImage, QColor, QKeySequence, QAction, QFont
from PyQt6.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox, QStatusBar, QMenu, QMenuBar, QWidget, \
    QHBoxLayout, QVBoxLayout, QLineEdit, QPushButton, QLabel

from mainWindow import Ui_MainWindow


import sys
import time
from PyQt6.QtCore import (
    QObject, pyqtSlot, pyqtSignal, Qt, QThread, QTimer
)
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout,
    QPushButton, QTextEdit, QLineEdit
)

# ------------------------------------------------------------------------------
# 1. 工作线程:模拟耗时任务(避免卡UI)
# ------------------------------------------------------------------------------
class WorkerThread(QThread):
    progress = pyqtSignal(int)
    finished = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)

    def run(self):
        for i in range(1, 101):
            time.sleep(0.02)
            self.progress.emit(i)
        self.finished.emit("后台任务执行完成!")

# ------------------------------------------------------------------------------
# 2. 通信桥接对象:给JS调用的所有接口
# ------------------------------------------------------------------------------
class Bridge(QObject):
    # Python → JS 信号
    sendLog = pyqtSignal(str)
    sendProgress = pyqtSignal(int)
    sendResult = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.worker = WorkerThread()
        self.worker.progress.connect(self.sendProgress)
        self.worker.finished.connect(self.sendResult)

    # JS → Python:无参数无返回
    @pyqtSlot()
    def jsCallVoid(self):
        msg = "JS 调用了 Python 无参方法"
        print(msg)
        self.sendLog.emit(msg)

    # JS → Python:带字符串参数
    @pyqtSlot(str)
    def jsCallWithString(self, text):
        msg = f"JS 传入文本:{text}"
        print(msg)
        self.sendLog.emit(msg)

    # JS → Python:带数字参数,返回值
    @pyqtSlot(int, result=int)
    def jsCallWithReturn(self, num):
        res = num * 10
        msg = f"JS 传入 {num},Python 返回 {res}"
        print(msg)
        self.sendLog.emit(msg)
        return res

    # JS → Python:启动后台任务
    @pyqtSlot()
    def startBackgroundTask(self):
        self.sendLog.emit("Python 开始后台任务...")
        self.worker.start()
        print("Python 开始后台任务...")

    # Python 主动发消息给 JS
    def pythonSendToJs(self, msg):
        self.sendLog.emit(f"Python 主动发送:{msg}")
        print(f"Python 主动发送:{msg}")

# ------------------------------------------------------------------------------
# 3. 主窗口
# ------------------------------------------------------------------------------
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt6 QWebChannel")
        self.setGeometry(100, 100, 900, 600)

        # UI
        central = QWidget()
        self.setCentralWidget(central)
        layout = QVBoxLayout(central)

        self.log_edit = QTextEdit()
        self.log_edit.setPlaceholderText("通信日志将在这里显示")
        self.input_line = QLineEdit()
        self.input_line.setPlaceholderText("输入内容,Python 主动发给 JS")
        self.btn_send = QPushButton("Python 主动发消息到 JS")

        self.webview = QWebEngineView()

        layout.addWidget(self.log_edit)
        layout.addWidget(self.input_line)
        layout.addWidget(self.btn_send)
        layout.addWidget(self.webview)

        # 通道
        self.bridge = Bridge()
        self.channel = QWebChannel()
        self.channel.registerObject("bridge", self.bridge)
        self.webview.page().setWebChannel(self.channel)

        # 绑定
        self.bridge.sendLog.connect(self.append_log)
        self.btn_send.clicked.connect(self.on_python_send)

        # 加载 HTML
        self.webview.setHtml(self.get_html())

    def append_log(self, msg):
        self.log_edit.append(msg)

    def on_python_send(self):
        text = self.input_line.text() or "Hello from Python!"
        self.bridge.pythonSendToJs(text)

    def get_html(self):
        return """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>JS 交互页面</title>
    <style>
        body{font-family: Microsoft YaHei; padding:20px; background:#f7f8fa}
        .box{padding:15px; background:white; border-radius:8px; margin-bottom:10px}
        button{padding:8px 16px; margin:5px; cursor:pointer}
        #log{background:#f0f0f0; padding:10px; height:200px; overflow-y:auto}
        #progress{height:8px; background:#42b983}
    </style>
</head>
<body>
    <div class="box">
        <h3>JS → Python 调用</h3>
        <button onclick="callVoid()">无参调用</button>
        <button onclick="callWithString()">带文本调用</button>
        <button onclick="callWithReturn()">带返回值调用</button>
        <button onclick="startTask()">启动后台任务</button>
    </div>

    <div class="box">
        <h3>通信日志</h3>
        <div id="log"></div>
    </div>

    <div class="box">
        <h3>进度条</h3>
        <div id="progress" style="width:0%"></div>
        <div id="result"></div>
    </div>

    <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
    <script>
        let bridge = null;
        let logDom = document.getElementById('log');
        let progressDom = document.getElementById('progress');
        let resultDom = document.getElementById('result');

        // 初始化通道
        new QWebChannel(qt.webChannelTransport, channel => {
            bridge = channel.objects.bridge;
            console.log("QWebChannel 连接成功");
            addLog("✅ JS 已连接到 Python");

            // 监听 Python 发来的日志
            bridge.sendLog.connect(msg => {
                addLog("📥 " + msg);
            });

            // 监听进度
            bridge.sendProgress.connect(p => {
                progressDom.style.width = p + "%";
                addLog("📊 进度:" + p + "%");
            });

            // 监听结果
            bridge.sendResult.connect(res => {
                resultDom.innerText = res;
                addLog("✅ " + res);
            });
        });

        function addLog(text) {
            let d = new Date();
            let t = d.toLocaleTimeString();
            logDom.innerHTML += `[${t}] ${text}<br>`;
            logDom.scrollTop = logDom.scrollHeight;
        }

        // 1. 无参
        function callVoid() {
            bridge.jsCallVoid();
        }

        // 2. 带字符串
        function callWithString() {
            bridge.jsCallWithString("你好,我是JS!");
        }

        // 3. 带返回值
        async function callWithReturn() {
            let res = await bridge.jsCallWithReturn(666);
            addLog("📤 JS 收到返回值:" + res);
        }

        // 4. 启动后台任务
        function startTask() {
            bridge.startBackgroundTask();
        }
    </script>
</body>
</html>
        """

if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = MainWindow()
    win.show()
    sys.exit(app.exec())

3、运行结果


相关推荐
M ? A2 小时前
你的 Vue v-for,VuReact 会编译成什么样的 React 代码?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
午安~婉2 小时前
Electron桌面应用(续3)
前端·javascript·electron·重构通用模型·异步可迭代对象
W.A委员会2 小时前
伪类与伪元素
前端·javascript·css
午安~婉2 小时前
Electron桌面应用(续2)
前端·javascript·electron·路由守卫·优化llm返回的内容
eEKI DAND2 小时前
一个比 Nginx 还简单的 Web 服务器
服务器·前端·nginx
Highcharts.js9 小时前
Highcharts 云端渲染的真相:交互式图表与服务器端生成的边界
前端·信息可视化·服务器渲染·highcharts·图表渲染
zhuyan10810 小时前
Linux 系统磁盘爆满导致无法启动修复指南
前端·chrome
编程牛马姐11 小时前
独立站SEO流量增长:提高Google排名的优化方法
前端·javascript·网络
NotFound48611 小时前
实战指南如何实现Java Web 拦截机制:Filter 与 Interceptor 深度分享
java·开发语言·前端