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


相关推荐
DianSan_ERP7 小时前
电商架构演进:如何在高并发场景下实现多平台API的标准化履约?
运维·前端·网络·安全·架构·自动化
碎_浪10 小时前
给键盘党的英语记忆工具:我做了一款「打字背单词」桌面应用
前端·程序员·ai编程
阿成学长_Cain11 小时前
Linux dirs命令详解|Bash目录堆栈管理快速切换目录实战教程
linux·运维·前端·数据库
yqcoder11 小时前
httpOnly 是什么,又有什么用?
开发语言·前端·javascript
IT_陈寒11 小时前
Java的Stream.parallel()把我CPU跑爆了,这种优化要谨慎
前端·人工智能·后端
小皮虾12 小时前
小程序首页性能优化实战:从 4 秒到 1.8 秒
前端·微信小程序
山河木马12 小时前
GPU自动处理专题1-裁剪到底在裁什么(裁剪)
前端·webgl·计算机图形学
奔跑的蜗牛ing12 小时前
CentOS Nginx 安装与静态文件服务器配置指南
前端·面试·架构
子兮曰12 小时前
Bun 重写为 Rust:11天、64个Claude、16.5万美元,一次改变行业认知的激进实验
前端·后端·bun