You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Python的QtPy GUI中用多线程实现PLC串口通信

解决Qt GUI中RS232读写PLC的线程问题与QTimer替代循环方案

问题背景

开发基于Qt的RS232读写PLC控制器GUI应用时,最初将所有串口读写操作放在主GUI线程中,导致应用频繁出现延迟;尝试迁移到多线程后,又出现GUI崩溃、控件显示异常的问题,同时需要用QTimer替代While(True)轮询循环。

核心问题拆解

  • 主GUI线程执行串口读写会阻塞事件循环,导致界面卡顿
  • 子线程直接操作GUI控件违反Qt线程规则,引发崩溃/异常
  • While(True)轮询会持续占用线程资源,不符合Qt事件驱动模型

完整解决方案代码

1. 导入依赖模块

import sys
import serial
import serial.tools.list_ports
from PyQt5 import QtWidgets, QtCore

2. 串口工作线程类

将所有串口读写操作封装在独立线程中,通过信号与主线程通信:

class SerialWorker(QtCore.QThread):
    # 定义信号:发送读取到的PLC数据给主线程
    data_received = QtCore.pyqtSignal(str, str, str)  # 对应三个读取函数的返回数据
    error_occurred = QtCore.pyqtSignal(str)  # 发送错误信息

    def __init__(self):
        super().__init__()
        self.serial_port = None
        self.is_running = False
        self.read_timer = QtCore.QTimer()
        self.read_timer.setInterval(1000)  # 每秒读取一次,可根据需求调整
        self.read_timer.timeout.connect(self.read_all_data)

    def open_serial_port(self):
        """打开串口并初始化"""
        try:
            self.serial_port = serial.Serial(
                port='COM10',
                baudrate=9600,
                parity=serial.PARITY_NONE,
                bytesize=8,
                stopbits=1,
                timeout=1
            )
            self.is_running = True
            self.read_timer.start()
        except Exception as e:
            self.error_occurred.emit(f"串口打开失败: {str(e)}")

    def close_serial_port(self):
        """关闭串口并停止线程"""
        self.is_running = False
        self.read_timer.stop()
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
        self.quit()
        self.wait()

    def read_all_data(self):
        """定时读取PLC的三组数据"""
        if not self.serial_port or not self.serial_port.is_open:
            return
        try:
            data1 = self.Reading_Controller()
            data2 = self.Reading_Controller_2()
            data3 = self.Reading_Controller_3()
            self.data_received.emit(data1, data2, data3)
        except Exception as e:
            self.error_occurred.emit(f"读取失败: {str(e)}")

    # 以下是实际的PLC读取逻辑,根据需求实现
    def Reading_Controller(self):
        # 示例读取逻辑:替换为实际的RS232读取代码
        if self.serial_port.in_waiting > 0:
            return self.serial_port.readline().decode('utf-8').strip()
        return "无数据"

    def Reading_Controller_2(self):
        # 示例读取逻辑:替换为实际的RS232读取代码
        return "数据2"

    def Reading_Controller_3(self):
        # 示例读取逻辑:替换为实际的RS232读取代码
        return "数据3"

3. 主窗口类(整合UI与业务逻辑)

将Qt Designer生成的UI类与线程控制逻辑分离:

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        # 加载UI
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        # 初始化串口线程
        self.serial_worker = SerialWorker()
        # 连接信号与槽
        self.serial_worker.data_received.connect(self.update_gui_labels)
        self.serial_worker.error_occurred.connect(self.show_error_message)
        self.ui.pushButton.clicked.connect(self.toggle_serial_reading)
        # 关闭窗口时清理线程
        self.destroyed.connect(self.serial_worker.close_serial_port)

    def toggle_serial_reading(self):
        """启动/停止串口读取"""
        if not self.serial_worker.is_running:
            self.serial_worker.open_serial_port()
            self.ui.pushButton.setText("停止读取")
        else:
            self.serial_worker.close_serial_port()
            self.ui.pushButton.setText("开始读取")

    def update_gui_labels(self, data1, data2, data3):
        """主线程更新GUI控件"""
        self.ui.label.setText(data1)
        self.ui.label_2.setText(data2)
        self.ui.label_3.setText(data3)
        # 可根据需求更新其他标签
        # self.ui.label_4.setText(...)

    def show_error_message(self, error_msg):
        """显示错误提示"""
        QtWidgets.QMessageBox.critical(self, "错误", error_msg)

# Qt Designer生成的UI类(保持原结构,仅调整部分文本)
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(563, 397)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButton = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(190, 200, 101, 31))
        self.pushButton.setObjectName("pushButton")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(70, 50, 51, 41))
        self.label.setObjectName("label")
        self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(160, 50, 51, 41))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(250, 50, 51, 41))
        self.label_3.setObjectName("label_3")
        self.label_4 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_4.setGeometry(QtCore.QRect(340, 50, 51, 41))
        self.label_4.setObjectName("label_4")
        self.label_5 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_5.setGeometry(QtCore.QRect(430, 50, 51, 41))
        self.label_5.setObjectName("label_5")
        self.label_6 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_6.setGeometry(QtCore.QRect(20, 150, 101, 21))
        self.label_6.setObjectName("label_6")
        self.label_7 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_7.setGeometry(QtCore.QRect(20, 190, 101, 21))
        self.label_7.setObjectName("label_7")
        self.label_8 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_8.setGeometry(QtCore.QRect(20, 230, 101, 21))
        self.label_8.setObjectName("label_8")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 563, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "PLC控制器监控"))
        self.pushButton.setText(_translate("MainWindow", "开始读取"))
        self.label.setText(_translate("MainWindow", "等待数据"))
        self.label_2.setText(_translate("MainWindow", "等待数据"))
        self.label_3.setText(_translate("MainWindow", "等待数据"))
        self.label_4.setText(_translate("MainWindow", "TextLabel"))
        self.label_5.setText(_translate("MainWindow", "TextLabel"))
        self.label_6.setText(_translate("MainWindow", "TextLabel"))
        self.label_7.setText(_translate("MainWindow", "TextLabel"))
        self.label_8.setText(_translate("MainWindow", "TextLabel"))

4. 主函数

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

关键注意事项

  • 串口对象线程归属:串口必须在子线程中创建,不能跨线程复用,否则会引发线程安全问题
  • GUI操作规则:所有更新GUI控件的代码必须通过信号槽在主线程执行,子线程绝对不能直接操作Qt控件
  • QTimer使用:子线程中的QTimer需要在线程启动后再调用start(),确保其运行在子线程的事件循环中
  • 资源清理:关闭窗口时必须停止线程并关闭串口,避免资源泄漏

内容的提问来源于stack exchange,提问作者עומר דגן

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 03:37:32