如何在多线程环境中正常运行PyQt进度条对话框?
问题描述
耗时两周尝试多种配置仍未解决问题:基于QSystemTrayIcon的应用包含多个需10-20秒完成的脚本,功能完整但希望集成带QProgressBar的QDialog。搭建测试结构后遇到以下问题:
- 运行
system_tray.py,选择"With progress bar"时进度对话框冻结 - 单独运行
progress.py,窗口要等倒计时结束才会显示 - 尝试用独立
QThread替代QThreadPool,结果类似
现有代码
worker.py
from time import sleep from PyQt5.QtCore import pyqtSignal, QRunnable, QObject class Emitter(QObject): signal_worker_count = pyqtSignal(int) signal_finished = pyqtSignal() class WorkerClass(QRunnable): def __init__(self): super().__init__() self.max_count = None self.emitter = Emitter() def countdown(self): for i in range(self.max_count, 0, -1): self.emitter.signal_worker_count.emit(i) sleep(1) self.emitter.signal_finished.emit() def run(self): self.countdown()
progress.py
import sys from time import sleep from PyQt5.QtCore import Qt, QRunnable, QObject from PyQt5.QtWidgets import QApplication, QDialog, QProgressBar, QPushButton, QLabel class Progress(QRunnable): def __init__(self): super().__init__() self.bar = None self.max_value = None def increment_progress(self, progress_value): print(self.bar.progress.value() + progress_value) self.bar.increment_progress(progress_value) def run(self): print('starting progress bar') new_bar = ProgressDialog() new_bar.setup(10) self.bar = new_bar print('continuing progress bar') self.bar.show() print('should be showing progress bar') def update_progress(self, progress_value): self.bar.update_progress(progress_value) class ProgressDialog(QDialog): def __init__(self): super().__init__() self.progress = None self.close_button = None self.message_label = None self.init_ui() def init_ui(self): self.setWindowTitle('Progress Bar') self.resize(436, 150) self.setWindowFlag(Qt.FramelessWindowHint) self.progress = QProgressBar(self) self.progress.setObjectName('ProgressBar') self.progress.setGeometry(10, 10, 416, 30) self.message_label = QLabel(self) self.message_label.setObjectName('MessageLabel') self.message_label.setGeometry(10, 50, 416, 30) self.message_label.setText('Calculating...') self.close_button = QPushButton('Close', self) self.close_button.setObjectName('CloseButton') self.close_button.setGeometry(300, 100, 75, 30) self.close_button.setEnabled(True) self.close_button.clicked.connect(self.hide) def set_message(self, message): self.message_label.setText(message) def setup(self, max_value): self.progress.maximum = max_value def increment_progress(self, progress_value): self.progress.setValue(self.progress.value() + progress_value) def update_progress(self, progress_value): self.progress.setValue(progress_value) if __name__ == "__main__": app = QApplication(sys.argv) progress = Progress() progress.run() for i in range(10): progress.increment_progress(10) sleep(0.5) app.exec_()
system_tray.py
import sys from PyQt5.QtCore import QThreadPool, QThread from PyQt5.QtWidgets import (QApplication, QSystemTrayIcon, QMenu, QAction, qApp) from PyQt5.QtGui import QIcon from worker import WorkerClass from progress import Progress class SystemTray(QSystemTrayIcon): def __init__(self): self.worker_pool = QThreadPool() self.worker_pool.setMaxThreadCount(2) self.progress_pool = QThreadPool() self.progress_pool.setMaxThreadCount(2) self.progress = None super().__init__(QIcon('icon.png')) no_progress_action = QAction("Without progress bar", self) progress_action = QAction("With progress bar", self) quit_action = QAction("Exit", self) no_progress_action.triggered.connect(lambda: self.perform("no_progress")) progress_action.triggered.connect(lambda: self.perform("progress")) quit_action.triggered.connect(lambda: self.perform("quit")) tray_menu = QMenu() tray_menu.addAction(no_progress_action) tray_menu.addSeparator() tray_menu.addAction(progress_action) tray_menu.addSeparator() tray_menu.addAction(quit_action) self.setContextMenu(tray_menu) self.show() def perform(self, req): if req == "no_progress": no_progress_worker = WorkerClass() no_progress_worker.max_count = 10 no_progress_worker.emitter.signal_worker_count.connect(lambda current_count: print(current_count)) self.worker_pool.start(no_progress_worker) elif req == "progress": progress_worker = Progress() self.progress_pool.start(progress_worker) elif req == "quit": self.hide() qApp.quit() def update_progress(self, current_count): print(current_count) if __name__ == "__main__": app = QApplication(sys.argv) main_window = SystemTray() sys.exit(app.exec())
问题原因
Qt核心规则:所有UI组件的创建、显示、更新操作必须在主线程(QApplication所在线程)执行。你的代码违反了这一规则:
Progress作为QRunnable被放到线程池,在子线程中创建ProgressDialog并调用show(),子线程没有Qt事件循环,无法处理UI渲染和交互事件,导致窗口冻结或延迟显示。- 单独运行
progress.py时,progress.run()在主线程执行,但后续的for循环+sleep阻塞了主线程事件循环,直到循环结束才进入app.exec_()处理UI事件,因此窗口延迟显示。
解决方法
调整架构,让后台任务与UI完全分离:
- 后台任务(倒计时)在子线程运行,通过信号发送进度数据
- 进度对话框在主线程创建、显示,接收后台任务的信号更新进度
- 避免在主线程执行阻塞操作,保证事件循环正常运行
修改后的代码
worker.py(无需调整,保留原逻辑)
progress.py(移除QRunnable,仅保留ProgressDialog)
from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialog, QProgressBar, QPushButton, QLabel class ProgressDialog(QDialog): def __init__(self): super().__init__() self.progress = None self.close_button = None self.message_label = None self.init_ui() def init_ui(self): self.setWindowTitle('Progress Bar') self.resize(436, 150) self.setWindowFlag(Qt.FramelessWindowHint) self.progress = QProgressBar(self) self.progress.setObjectName('ProgressBar') self.progress.setGeometry(10, 10, 416, 30) self.message_label = QLabel(self) self.message_label.setObjectName('MessageLabel') self.message_label.setGeometry(10, 50, 416, 30) self.message_label.setText('Calculating...') self.close_button = QPushButton('Close', self) self.close_button.setObjectName('CloseButton') self.close_button.setGeometry(300, 100, 75, 30) self.close_button.setEnabled(True) self.close_button.clicked.connect(self.hide) def set_message(self, message): self.message_label.setText(message) def setup(self, max_value): self.progress.setMaximum(max_value) self.progress.setValue(0) def update_progress(self, progress_value): # 将倒计时数值转换为进度条进度(如max=10,倒计时10对应进度0,倒计时1对应进度9) current_progress = self.progress.maximum() - progress_value self.progress.setValue(current_progress) self.set_message(f"剩余时间:{progress_value}秒")
system_tray.py(主线程创建对话框,连接信号)
import sys from PyQt5.QtCore import QThreadPool from PyQt5.QtWidgets import (QApplication, QSystemTrayIcon, QMenu, QAction, qApp) from PyQt5.QtGui import QIcon from worker import WorkerClass from progress import ProgressDialog class SystemTray(QSystemTrayIcon): def __init__(self): self.worker_pool = QThreadPool() self.worker_pool.setMaxThreadCount(2) self.progress_dialog = None super().__init__(QIcon('icon.png')) no_progress_action = QAction("Without progress bar", self) progress_action = QAction("With progress bar", self) quit_action = QAction("Exit", self) no_progress_action.triggered.connect(lambda: self.perform("no_progress")) progress_action.triggered.connect(lambda: self.perform("progress")) quit_action.triggered.connect(lambda: self.perform("quit")) tray_menu = QMenu() tray_menu.addAction(no_progress_action) tray_menu.addSeparator() tray_menu.addAction(progress_action) tray_menu.addSeparator() tray_menu.addAction(quit_action) self.setContextMenu(tray_menu) self.show() def perform(self, req): if req == "no_progress": no_progress_worker = WorkerClass() no_progress_worker.max_count = 10 no_progress_worker.emitter.signal_worker_count.connect(lambda current_count: print(current_count)) self.worker_pool.start(no_progress_worker) elif req == "progress": # 主线程创建进度对话框 self.progress_dialog = ProgressDialog() self.progress_dialog.setup(10) self.progress_dialog.show() # 创建后台任务并连接信号 progress_worker = WorkerClass() progress_worker.max_count = 10 progress_worker.emitter.signal_worker_count.connect(self.progress_dialog.update_progress) progress_worker.emitter.signal_finished.connect(self.on_task_finished) self.worker_pool.start(progress_worker) elif req == "quit": self.hide() qApp.quit() def on_task_finished(self): if self.progress_dialog: self.progress_dialog.set_message("任务完成!") # 可选:2秒后自动关闭对话框 # from PyQt5.QtCore import QTimer # QTimer.singleShot(2000, self.progress_dialog.hide) if __name__ == "__main__": app = QApplication(sys.argv) main_window = SystemTray() sys.exit(app.exec())
测试说明
单独测试进度对话框时,可使用以下脚本(确保主线程不被阻塞):
import sys from PyQt5.QtWidgets import QApplication from progress import ProgressDialog from worker import WorkerClass from PyQt5.QtCore import QThreadPool if __name__ == "__main__": app = QApplication(sys.argv) dialog = ProgressDialog() dialog.setup(10) dialog.show() pool = QThreadPool() worker = WorkerClass() worker.max_count =10 worker.emitter.signal_worker_count.connect(dialog.update_progress) pool.start(worker) sys.exit(app.exec_())
内容的提问来源于stack exchange,提问作者pedwards
相关产品推荐
相关产品推荐

