Python中QT开发UI时,在子线程创建界面、主线程处理逻辑,并通过消息队列链接的方案是否可行?
刚接触 UI 开发没多久,很多东西还不知道 用的 pyqt5,一般创建界面都是在主线程里创建,遇到耗时的任务时开个 QThread 子线程来处理不是吗。
我想要达到分离界面和逻辑的目的,可能我水平次,所以能想到的解决办法就是: 在子线程里创建主界面,主线程里处理逻辑。 然后两者通过一个 Queue 消息队列链接。比如:界面输入完数据后,往 queue 里放一个自定义消息 MSG_BTN_CLICK,然后主线程里通过:
while True:
event_id , parameter = event_queue.get()
if event_id == MSG_BTN_CLICK:
# 代码逻辑
如果界面要更新界面数据的话,就发送个 MSG_UPDATE_LIST 之类的消息附带上窗口对象,然后主线程丽获取数据后发送个 qt 信号过去更新数据。
不知道这个想法可不可取?谢谢指教!
Python中QT开发UI时,在子线程创建界面、主线程处理逻辑,并通过消息队列链接的方案是否可行?
emmmm,信号和槽
这个方案技术上可行,但强烈不推荐,因为Qt的GUI对象(QWidget及其子类)必须在主线程(也称为GUI线程)中创建和操作。这是Qt框架的核心规则,违反它会导致程序崩溃或出现不可预测的界面问题。
正确的做法是反过来的:主线程创建界面,子线程处理耗时逻辑,然后通过信号槽(或线程安全的方式)将结果传回主线程更新UI。
下面是一个演示正确模式的完整示例。它模拟了一个耗时的计算任务在子线程中运行,并通过信号槽安全地更新主线程的UI。
import sys
import time
from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton,
QVBoxLayout, QWidget, QLabel, QTextEdit)
from PySide6.QtCore import QThread, Signal, Slot
# 1. 工作线程类,负责处理耗时逻辑
class WorkerThread(QThread):
# 定义信号,用于将数据(这里是日志文本)发送到主线程
log_signal = Signal(str)
result_signal = Signal(int)
def run(self):
"""线程的主运行函数,在这里执行耗时操作。"""
self.log_signal.emit("工作线程启动...")
total = 0
for i in range(5):
time.sleep(1) # 模拟耗时操作,比如网络请求或复杂计算
current_result = i * 10
total += current_result
# 通过信号发送中间日志和结果
self.log_signal.emit(f"完成第 {i+1} 步,当前值: {current_result}")
self.log_signal.emit("计算完成!")
# 发送最终结果
self.result_signal.emit(total)
# 2. 主窗口类,UI都在这里创建
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Qt多线程示例")
self.setGeometry(100, 100, 400, 300)
# 创建中央部件和布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# 创建UI控件
self.label = QLabel("点击按钮开始耗时计算")
self.text_edit = QTextEdit()
self.text_edit.setReadOnly(True)
self.button = QPushButton("开始计算")
self.button.clicked.connect(self.start_calculation)
layout.addWidget(self.label)
layout.addWidget(self.text_edit)
layout.addWidget(self.button)
# 创建工作线程实例
self.worker_thread = WorkerThread()
# 将工作线程的信号连接到主窗口的槽函数
self.worker_thread.log_signal.connect(self.update_log)
self.worker_thread.result_signal.connect(self.update_result)
self.worker_thread.finished.connect(self.on_thread_finished)
@Slot()
def start_calculation(self):
"""按钮点击槽函数,启动工作线程。"""
self.button.setEnabled(False)
self.label.setText("计算中...")
self.text_clear()
self.worker_thread.start() # 启动线程,会自动调用其run()方法
@Slot(str)
def update_log(self, message):
"""更新日志文本框的槽函数。由工作线程的log_signal触发。"""
self.text_edit.append(f"[日志] {message}")
@Slot(int)
def update_result(self, result):
"""更新最终结果的槽函数。由工作线程的result_signal触发。"""
self.label.setText(f"最终结果: {result}")
@Slot()
def on_thread_finished(self):
"""线程结束时的槽函数。"""
self.button.setEnabled(True)
self.text_edit.append("[系统] 工作线程已结束。")
def text_clear(self):
"""清空文本框。"""
self.text_edit.clear()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
代码解释:
WorkerThread类继承自QThread,在它的run()方法中执行模拟的耗时任务。它通过自定义信号 (log_signal,result_signal) 与主线程通信。MainWindow类在主线程中创建所有UI组件(按钮、标签、文本框)。- 当用户点击按钮时,
start_calculation槽函数被调用,它禁用按钮(防止重复点击)并启动工作线程。 - 工作线程在运行过程中,通过发射信号将日志文本和结果发送出去。
- 主线程中的槽函数 (
update_log,update_result) 接收到这些信号后,安全地更新UI(因为它们在主线程上下文中执行)。 - 线程结束后,
finished信号被触发,重新启用按钮。
总结:必须遵循“主线程管UI,子线程干重活”的原则。
在 UI 线程(往往是主线程,但不能相等)给个成员变量并监听变动,new 子线程并给予其对本线程(上文的 UI 线程)的引用,你的逻辑完成后修改 UI 线程的变量,主线程处理后续逻辑
Qt 对界面相关的的操作只能在主线程中完成。你可以试一下这个做法: https://stackoverflow.com/questions/11033971/qt-thread-with-movetothread
感谢回复。看来官方的确还是建议只在主线程里跑 UI 啊。
不过有点搞不懂,文档里明确说了 widgets 这些不能在子线程里跑起来<br>All widgets and several related classes, for example QPixmap, don't work in secondary threads<br>
但我在子线程里创建窗口显示按钮都很正常没问题…
看到上面说只有 UI 线程才能操作界面,那和 Android 差不多.
通常都是在新线程处理数据,然后处理完了回到 UI 线程处理界面.
子线程不能跑 UI。既然是处理逻辑,子线程就可以啊,搞不清楚你为什么要反着来。
据我所知,一般都不允许子线程处理 UI,
逻辑很简单,多个子线程处理 UI 的时候由于次序问题很容易导致界面错乱,或者使用了被其它线程释放掉的资源导致崩溃
主要是想要彻底把界面和逻辑分开。因为感觉界面做主线程,处理逻辑开个子线程的话,界面代码和逻辑代码实际上依旧是混合在一起的。
而反着来的话,界面类里就只有界面代码的,界面有操作的话,就按钮点击后就发送个类似于 MSG_BTN_CLICKED 消息到队列里。这样主线程就能捕获到这个消息,处理了逻辑了,这样界面和处理逻辑就彻底分开了。
当然,因为我从没做过界面开发,所以可能想歪了。所以想来征求下大家的意见
然后现在我有个更疑惑的问题,看了大家都在说子线程不能操作 UI,,就是 pyqt 似乎真的能子线程创建 UI 没问题?
随便写了个最简单的代码例子,似乎跑起来没问题?
https://gist.github.com/ShinonomeHana/be8ec0bf77da9503fd2076837d2b8522
Android 子线程是可以操作 ui 的,但是要在创建 Activity 的时候用非常快的速度去修改。
原理就是做 ui 操作时会检查当前线程是否和 Activity 的创建线程(也就是主线程)是否一致,不一致就抛异常。
不知道其他框架是不是也是这么干的。
Qt 在 windows 下允许用非主线程创建 QApplication 对象并执行事件循环。在 mac 下这样做会出错。
估计子线程后面没有对 UI 的操作了所以没报错,你在线程 start 之后立马去创个按妞试试看,估计就报错了吧。
> 一般创建界面都是在主线程里创建,遇到耗时的任务时开个 QThread 子线程来处理不是吗。
嗯,用线程或者进程
> 随便写了个最简单的代码例子,似乎跑起来没问题?
有问题的,比如 macOS 下,这样的程序会直接崩溃(我在 gist 下写了更多详情)
> 我想要达到分离界面和逻辑的目的
LZ 有没有想过这么几个问题
1. 界面和逻辑分离有哪些好处呢?
2. 哪些算界面部分,哪些算逻辑部分?
3. 界面操作(改变按钮颜色、调整组件宽度、组件动画)这些算逻辑还是算界面?
--------------------------------
关于界面和逻辑分离的观点,一个 GUI 程序,大部分逻辑就是两种情况:
1. 获取数据 -> 刷新界面
2. 用户操作界面 -> 修改数据
大部分情况,界面和逻辑是密不可分的,分离界面和逻辑是个错误的决定,分离只会让你的代码变得复杂。
还有一部分场景:Qt 提供了一种 Model/View/Delegator 的编程模式,它解决的问题是复杂业务场景下的界面逻辑分离。
原来如此,当时我按着这个思路下试了下就跑起来了还以为这个思路是没问题的,哈哈。
不会报错,只要所有对 UI 的创建都是在那个子线程里进行的话就不会报错。
多谢指点,没考虑到跨平台的问题,哈哈。 的确在 macOS 下会报错。
经你一说的确想通了,对界面的变更操作的确不好区分,很多界面的操作逻辑和 UI 是密不可分的,如果界面要做一些变更就给主线程发送个 MSG 的话,的确反倒会造成主线程那边代码变得相当复杂
#13 分离界面的逻辑 前提是 界面操作要提供一堆声明式的接口 来满足逻辑的需要,逻辑需要做的是描述我想要的界面是什么样的,而不是关心界面是如何被操作的。
做 UI 的大致都只能主线程刷新界面,子线程处理完了丢个消息刷新界面,原因是许多 OS 的图形子系统底层都有锁,防止出现图形绘制不正确之类的
PS.我之前做 MFC,子线程是不能直接调用 GDI API 的,强行 ShowDialog 会造成 GDI contex 错误
基本 90%的 ui 框架都不允许非 ui 线程操作界面。
我觉得可以做一个 model,映射界面数据,然后 ui 和后台线程对该 model 进行更新,ui 在每次事件循环中根据 model 来更新界面。。好像就是 mvc 的思路,好久没用 qt 了。。都忘的差不多了。
既然都 Qt 了为什么不用信号和槽呢?这俩本来就可以跨线程的呀。把自己的类 moveToThread,就运行在子线程了,数据用信号来传
Windows 也许可以,但不建议这么做。macOS 不行。
印象中 macOS 非主线程或者 fork 出来的子进程不能使用 UI 相关的功能。
是 V 和 M 双向绑定的意思吗?
我都是在主线程创建 UI,然后子线程处理逻辑,两者通过信号槽来传递消息…话说 QT 的信号槽真的好用
主线程跑 UI,子线程跑任务,通过信号槽联系,


