如何在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,提问作者עומר דגן
相关产品推荐
相关产品推荐

