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

如何在多线程环境中正常运行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所在线程)执行。你的代码违反了这一规则:

  1. Progress作为QRunnable被放到线程池,在子线程中创建ProgressDialog并调用show(),子线程没有Qt事件循环,无法处理UI渲染和交互事件,导致窗口冻结或延迟显示。
  2. 单独运行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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 23:47:04