PyQt:如何在不冻结 GUI 的情况下更新进度?

Posted

技术标签:

【中文标题】PyQt:如何在不冻结 GUI 的情况下更新进度?【英文标题】:PyQt: How to update progress without freezing the GUI? 【发布时间】:2010-10-08 20:27:03 【问题描述】:

问题:

    什么是最佳实践 跟踪线程的 在不锁定 GUI 的情况下进行 (“没有回应”)? 一般来说,最佳实践是什么? 线程,因为它适用于 GUI 开发?

问题背景:

我有一个适用于 Windows 的 PyQt GUI。 用于处理 html 集 文件。 需要三秒钟 到三个小时来处理一套 文件。 我希望能够处理 多套同时使用。 我不希望 GUI 锁定。 我在看线程模块 实现这一目标。 我对线程比较陌生。 GUI 有一个进度条。 我希望它显示进度 选定的线程。 显示所选结果 如果它完成了线程。 我使用的是 Python 2.5。

我的想法:让线程在进度更新时发出 QtSignal,触发一些更新进度条的函数。完成处理时也会发出信号,以便显示结果。

#NOTE: this is example code for my idea, you do not have
#      to read this to answer the question(s).

import threading
from PyQt4 import QtCore, QtGui
import re
import copy

class ProcessingThread(threading.Thread, QtCore.QObject):

    __pyqtSignals__ = ( "progressUpdated(str)",
                        "resultsReady(str)")

    def __init__(self, docs):
        self.docs = docs
        self.progress = 0   #int between 0 and 100
        self.results = []
        threading.Thread.__init__(self)

    def getResults(self):
        return copy.deepcopy(self.results)

    def run(self):
        num_docs = len(self.docs) - 1
        for i, doc in enumerate(self.docs):
            processed_doc = self.processDoc(doc)
            self.results.append(processed_doc)
            new_progress = int((float(i)/num_docs)*100)
            
            #emit signal only if progress has changed
            if self.progress != new_progress:
                self.emit(QtCore.SIGNAL("progressUpdated(str)"), self.getName())
            self.progress = new_progress
            if self.progress == 100:
                self.emit(QtCore.SIGNAL("resultsReady(str)"), self.getName())
    
    def processDoc(self, doc):
        ''' this is tivial for shortness sake '''
        return re.findall('<a [^>]*>.*?</a>', doc)


class GuiApp(QtGui.QMainWindow):
    
    def __init__(self):
        self.processing_threads =   #'thread_name': Thread(processing_thread)
        self.progress_object =      #'thread_name': int(thread_progress)
        self.results_object =       #'thread_name': []
        self.selected_thread = ''     #'thread_name'
        
    def processDocs(self, docs):
        #create new thread
        p_thread = ProcessingThread(docs)
        thread_name = "example_thread_name"
        p_thread.setName(thread_name)
        p_thread.start()
        
        #add thread to dict of threads
        self.processing_threads[thread_name] = p_thread
        
        #init progress_object for this thread
        self.progress_object[thread_name] = p_thread.progress  
        
        #connect thread signals to GuiApp functions
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('progressUpdated(str)'), self.updateProgressObject(thread_name))
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('resultsReady(str)'), self.updateResultsObject(thread_name))
        
    def updateProgressObject(self, thread_name):
        #update progress_object for all threads
        self.progress_object[thread_name] = self.processing_threads[thread_name].progress
        
        #update progress bar for selected thread
        if self.selected_thread == thread_name:
            self.setProgressBar(self.progress_object[self.selected_thread])
        
    def updateResultsObject(self, thread_name):
        #update results_object for thread with results
        self.results_object[thread_name] = self.processing_threads[thread_name].getResults()
        
        #update results widget for selected thread
        try:
            self.setResultsWidget(self.results_object[thread_name])
        except KeyError:
            self.setResultsWidget(None)

对这种方法的任何评论(例如缺点、陷阱、赞美等)将不胜感激。

分辨率:

我最终使用 QThread 类和相关的信号和插槽在线程之间进行通信。这主要是因为我的程序已经将 Qt/PyQt4 用于 GUI 对象/小部件。该解决方案还需要对我现有的代码进行较少的更改来实现。

这里是适用的 Qt 文章的链接,该文章解释了 Qt 如何处理线程和信号,http://www.linuxjournal.com/article/9602。摘录如下:

幸运的是,Qt 允许 要连接的信号和插槽 跨线程——只要线程 正在运行自己的事件循环。 这是一种更清洁的方法 与发送相比,通信和 接收事件,因为它避免了 所有的簿记和中间 QEvent 派生类成为 在任何重要的事情中都是必要的 应用。之间的交流 线程现在变成了 将信号从一个线程连接到 另一个中的插槽,以及互斥 和交换的线程安全问题 线程之间的数据由 Qt。

为什么需要举办活动 在您要访问的每个线程中循环 想要连接信号?原因 与线程间有关 Qt 使用的通信机制 当从一个连接信号时 线程到另一个线程的插槽。 当建立这样的连接时,它是 称为排队连接。 当信号通过一个 排队连接,插槽被调用 下次目标对象的 事件循环被执行。如果插槽 而是由 a 直接调用 来自另一个线程的信号,那个槽 将在相同的上下文中执行 调用线程。通常,这是 不是你想要的(尤其不是 如果您使用的是您想要的 数据库连接,作为数据库 连接只能由 创建它的线程)。排队的 连接正确调度 信号给线程对象和 通过在自己的上下文中调用其插槽 捎带事件系统。 这正是我们想要的 其中线程间通信 一些线程正在处理 数据库连接。 Qt 信号/槽机制是根 线程间的实现 上面概述的事件传递方案, 但是用更清洁和 更易于使用的界面。

注意: eliben 也有一个很好的答案,如果我不使用处理线程安全和互斥的 PyQt4,他的解决方案将是我的选择.

【问题讨论】:

【参考方案1】:

如果您想使用信号来指示主线程的进度,那么您应该使用 PyQt 的 QThread 类而不是 Python 线程模块中的 Thread 类。

可以在 PyQt Wiki 上找到一个使用 QThread、信号和槽的简单示例:

https://wiki.python.org/moin/PyQt/Threading,_Signals_and_Slots

【讨论】:

【参考方案2】:

本机 python 队列将无法工作,因为您必须阻塞队列 get(),这会破坏您的 UI。

Qt 本质上在内部实现了一个队列系统,用于跨线程通信。尝试从任何线程调用此调用以发布对插槽的调用。

QtCore.QMetaObject.invokeMethod()

它很笨重,文档记录也很差,但即使来自非 Qt 线程,它也应该可以满足您的需求。

您也可以为此使用事件机制。有关名为“post”之类的方法,请参见 QApplication(或 QCoreApplication)。

编辑:这是一个更完整的示例...

我基于 QWidget 创建了自己的类。它有一个接受字符串的槽;我是这样定义的:

@QtCore.pyqtSlot(str)
def add_text(self, text):
   ...

稍后,我在主 GUI 线程中创建了这个小部件的一个实例。从主 GUI 线程或任何其他线程(敲木头)我可以调用:

QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))

笨重,但它可以让你到达那里。

丹。

【讨论】:

【参考方案3】:

我建议您使用队列而不是信号。就我个人而言,我发现它是一种更加健壮且易于理解的编程方式,因为它更加同步。

线程应该从队列中获取“作业”,并将结果放回另一个队列。然而,线程可以使用第三个队列来获取通知和消息,例如错误和“进度报告”。以这种方式构建代码后,管理起来就会简单得多。

这样,一组工作线程也可以使用单个“作业队列”和“结果队列”,它将线程中的所有信息路由到主 GUI 线程。

【讨论】:

你能提供一个例子吗?我对线程如何与 Queue 对象进行通信有点困惑。【参考方案4】:

如果您的方法“processDoc”不更改任何其他数据(只是查找一些数据并返回它并且不更改父类的变量或属性),您可以使用 Py_BEGIN_ALLOW_THREADS 和 Py_END_ALLOW_THREADS 宏它。因此文档将在不会锁定解释器的线程中处理,并且 UI 将被更新。

【讨论】:

【参考方案5】:

下面是一个基本的 PyQt5/PySide2 示例,展示了如何在更新进度条的同时运行后台任务。该任务被移至工作线程,并使用自定义信号与主 GUI 线程进行通信。任务可以停止和重新启动,并在窗口关闭时自动终止。

# from PySide2 import QtCore, QtWidgets
#
# class Worker(QtCore.QObject):
#     progressChanged = QtCore.Signal(int)
#     finished = QtCore.Signal()

from PyQt5 import QtCore, QtWidgets

class Worker(QtCore.QObject):
    progressChanged = QtCore.pyqtSignal(int)
    finished = QtCore.pyqtSignal()

    def __init__(self):
        super().__init__()
        self._stopped = True

    def run(self):
        count = 0
        self._stopped = False
        while count < 100 and not self._stopped:
            count += 5
            QtCore.QThread.msleep(250)
            self.progressChanged.emit(count)
        self._stopped = True
        self.finished.emit()

    def stop(self):
        self._stopped = True

class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.button = QtWidgets.QPushButton('Start')
        self.button.clicked.connect(self.handleButton)
        self.progress = QtWidgets.QProgressBar()
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.progress)
        layout.addWidget(self.button)
        self.thread = QtCore.QThread(self)
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        self.worker.finished.connect(self.handleFinished)
        self.worker.progressChanged.connect(self.progress.setValue)
        self.thread.started.connect(self.worker.run)

    def handleButton(self):
        if self.thread.isRunning():
            self.worker.stop()
        else:
            self.button.setText('Stop')
            self.thread.start()

    def handleFinished(self):
        self.button.setText('Start')
        self.thread.quit()

    def closeEvent(self, event):
        self.worker.stop()
        self.thread.quit()
        self.thread.wait()

if __name__ == '__main__':

    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setWindowTitle('Threaded Progress')
    window.setGeometry(600, 100, 250, 50)
    window.show()
    sys.exit(app.exec_())

【讨论】:

【参考方案6】:

在 Python 中你总是会遇到这个问题。谷歌 GIL “全局解释器锁”了解更多背景信息。有两种通常推荐的方法来解决您遇到的问题:使用Twisted,或使用类似于 2.5 中引入的multiprocessing 模块的模块。

Twisted 要求您学习异步编程技术,这在开始时可能会让人感到困惑,但如果您需要编写高吞吐量网络应用程序会很有帮助,并且从长远来看对您更有利。

多处理模块将派生一个新进程并使用 IPC 使其表现得就像您拥有真正的线程一样。唯一的缺点是您需要安装相当新的 python 2.5,它默认包含在大多数 Linux 发行版或 OSX 中。

【讨论】:

我相信“多处理”模块是在 2.6 中引入的。另外,我正在编写一个独立的 Windows 应用程序,所以 Twisted 并不真正适用。不过感谢您的想法。

以上是关于PyQt:如何在不冻结 GUI 的情况下更新进度?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不冻结 GUI 的情况下使用视频帧不断更新 contentcontrol?

如何使 pyqt gui 刷新

如何在不冻结 GUI 的情况下让 AudioQueue 播放?

如何在不冻结 gui 的情况下运行 QProcess 的同步链?

如何在不冻结 GUI 的情况下在单个插槽中实现阻塞进程?

如何在不关闭 GUI 窗口的情况下停止运行 PyQt5 程序?