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、运行结果


相关推荐
剪刀石头布啊11 小时前
vue-react为何要虚拟dom
前端
剪刀石头布啊11 小时前
ahooks常用的一些库,我们使用一下
前端
剪刀石头布啊11 小时前
从git更新本地提交记录到更新已经推送到远端记录
前端
Flynt14 小时前
axios 被投毒半年后,我把 npm 的"安全防线"实测了一遍:最信任的 SHA512 是安慰剂
前端·npm·node.js
excel14 小时前
Base62 和 Base64 有什么区别?
前端
风骏时光牛马15 小时前
大模型底层架构与AI源码深度剖析
前端
掘金挖土16 小时前
前端手摸手跑路之 AI 应用开发(九)
前端·后端
中文呀19 小时前
三维 GIS 离线缓存实战:逆向 CesiumOfflineCache,并亲手为它补上"自动更新"能力
前端
前进前进前进19 小时前
TRAE 全链路实战:做了一个警示牌一键生成器:纯 Canvas、单文件、零依赖
前端