如何在不冻结PyQt GUI的情况下跟踪Python中的线程进度?

tgr*_*ray 19 python user-interface multithreading pyqt

问题:

  1. 在不锁定GUI的情况下跟踪踏板进度的最佳实践是什么("无响应")?
  2. 一般来说,适用于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)
Run Code Online (Sandbox Code Playgroud)

任何关于这种方法的评论(例如缺点,陷阱,赞美等)都将受到赞赏.

解析度:

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

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

幸运的是,只要线程正在运行自己的事件循环,Qt就允许跨线程连接信号和插槽.与发送和接收事件相比,这是一种更清晰的通信方法,因为它避免了在任何重要应用程序中必需的所有簿记和中间QEvent派生类.现在,线程之间的通信变成了将信号从一个线程连接到另一个线程中的槽的问题,并且线程之间交换数据的静音和线程安全问题由Qt处理.

为什么有必要在每个要连接信号的线程中运行事件循环?原因与Qt在将信号从一个线程连接到另一个线程的槽时使用的线程间通信机制有关.当建立这样的连接时,它被称为排队连接.当通过排队连接发出信号时,下次执行目标对象的事件循环时将调用该槽.如果插槽已被另一个线程的信号直接调用,则该插槽将在与调用线程相同的上下文中执行.通常情况下,这不是您想要的(尤其不是您在使用数据库连接时所需的内容,因为数据库连接只能由创建它的线程使用).排队连接正确地将信号调度到线程对象,并通过在事件系统上捎带来在其自己的上下文中调用其插槽.这正是我们想要的线程间通信,其中一些线程正在处理数据库连接.Qt信号/插槽机制在根本上是上面概述的线程间事件传递方案的实现,但具有更清洁和更易于使用的接口.

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

小智 9

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

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

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


小智 5

原生python队列不起作用,因为你必须阻塞队列get(),这会阻塞你的UI.

Qt基本上在内部实现了一个排队系统,用于跨线程通信.从任何线程尝试此调用以将呼叫发布到插槽.

QtCore.QMetaObject.invokeMethod()

它很笨拙,文档记录很差,但它应该从非Qt线程做你想做的事情.

您也可以使用事件机制.请参阅QApplication(或QCoreApplication)以获取名为"post"的方法.

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

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

@QtCore.pyqtSlot(str)
def add_text(self, text):
   ...
Run Code Online (Sandbox Code Playgroud)

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

QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))
Run Code Online (Sandbox Code Playgroud)

笨重,但它让你在那里.

担.