簡體   English   中英

PyQt:如何在不凍結GUI的情況下更新進度?

[英]PyQt: How to update progress without freezing the GUI?

問題:

  1. 在不鎖定 GUI(“無響應”)的情況下跟蹤線程進度的最佳做法是什么?
  2. 一般來說,應用於 GUI 開發的線程最佳實踐是什么?

提問背景:

  • 我有一個 Windows 的 PyQt GUI。
  • 它用於處理 HTML 個文檔集。
  • 處理一組文檔需要三秒到三個小時不等。
  • 我希望能夠同時處理多個集合。
  • 我不想鎖定 GUI。
  • 我正在查看線程模塊來實現這一點。
  • 我對線程比較陌生。
  • GUI 有一個進度條。
  • 我希望它顯示所選線程的進度。
  • 如果已完成,則顯示所選線程的結果。
  • 我正在使用 Python 2.5。

我的想法:讓線程在更新進度時發出 QtSignal,觸發一些更新進度條的 function。 當完成處理時也會發出信號,以便顯示結果。

#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 class 和關聯的信號和槽在線程之間進行通信。 這主要是因為我的程序已經將 Qt/PyQt4 用於 GUI 對象/小部件。 該解決方案還需要對我的現有代碼進行較少的更改才能實施。

這里有一篇適用的 Qt 文章的鏈接,該文章解釋了 Qt 如何處理線程和信號, http://www.linuxjournal.com/article/9602 摘錄如下:

幸運的是,Qt 允許跨線程連接信號和槽——只要線程正在運行它們自己的事件循環。 與發送和接收事件相比,這是一種更簡潔的通信方法,因為它避免了在任何重要應用程序中變得必需的所有簿記和中間 QEvent 派生類。 線程之間的通信現在變成了將信號從一個線程連接到另一個線程中的槽的問題,線程之間交換數據的互斥和線程安全問題由 Qt 處理。

為什么有必要在每個要連接信號的線程中運行一個事件循環? 原因與 Qt 在將信號從一個線程連接到另一個線程的槽時使用的線程間通信機制有關。 當建立這樣的連接時,它被稱為排隊連接。 當信號通過隊列連接發出時,槽將在下一次執行目標對象的事件循環時被調用。 如果該槽被另一個線程的信號直接調用,則該槽將在與調用線程相同的上下文中執行。 通常,這不是您想要的(如果您正在使用數據庫連接,則尤其不是您想要的,因為數據庫連接只能由創建它的線程使用)。 排隊的連接正確地將信號分派給線程 object 並通過搭載事件系統在其自己的上下文中調用其槽。 這正是我們想要的線程間通信,其中一些線程正在處理數據庫連接。 Qt 信號/槽機制本質上是上述線程間事件傳遞方案的實現,但具有更簡潔、更易於使用的接口。

注意: eliben也有一個很好的答案,如果我不使用處理線程安全和互斥的 PyQt4,他的解決方案將是我的選擇。

如果你想使用信號來指示主線程的進度,那么你應該使用PyQt的QThread類而不是Python的線程模塊中的Thread類。

在PyQt Wiki上可以找到一個使用QThread,信號和插槽的簡單示例:

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

原生python隊列不起作用,因為你必須阻塞隊列get(),這會阻塞你的UI。

Qt基本上在內部實現了一個排隊系統,用於跨線程通信。 從任何線程嘗試此調用以將呼叫發布到插槽。

QtCore.QMetaObject.invokeMethod()

它很笨拙,文檔記錄很差,但它應該從非Qt線程做你想做的事情。

您也可以使用事件機制。 請參閱QApplication(或QCoreApplication)以獲取名為“post”的方法。

編輯:這是一個更完整的例子......

我基於QWidget創建了自己的類。 它有一個接受字符串的插槽; 我這樣定義:

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

稍后,我在主GUI線程中創建此小部件的實例。 從主GUI線程或任何其他線程(敲木頭)我可以調用:

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

笨重,但它讓你在那里。

擔。

我建議你使用Queue而不是信令。 就個人而言,我發現它是一種更加健壯且易於理解的編程方式,因為它更加同步。

線程應從隊列中獲取“作業”,並將結果放回另一個隊列。 然而,線程可以使用第三個隊列來處理通知和消息,例如錯誤和“進度報告”。 一旦以這種方式構建代碼,管理就變得更加簡單。

這樣,一組工作線程也可以使用單個“作業隊列”和“結果隊列”,它將所有信息從線程路由到主GUI線程。

下面是一個基本的 PyQt5/PySide2 示例,展示了如何在更新進度條的同時運行后台任務。 任務被移動到工作線程,自定義信號用於與主 GUI 線程通信。 該任務可以停止和重新啟動,並會在 window 關閉時自動終止。

# 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_())

如果您的方法“processDoc”不更改任何其他數據(只查找某些數據並返回它並且不更改父類的變量或屬性),則可以使用Py_BEGIN_ALLOW_THREADS和Py_END_ALLOW_THREADS宏( 請參閱此處以獲取詳細信息 )。 因此,文檔將在線程中處理,不會鎖定解釋器,UI將被更新。

你總是會在Python中遇到這個問題。 谷歌GIL“全球解釋鎖定”為更多背景。 通常有兩種方法可以解決您遇到的問題:使用Twisted ,或使用類似於2.5中引入的多處理模塊的模塊。

Twisted將要求您學習異步編程技術,這些技術在開始時可能會令人困惑,但如果您需要編寫高吞吐量的網絡應用程序並且從長遠來看對您更有益,那么它將非常有用。

多處理模塊將分叉一個新進程並使用IPC使其行為就像您有真正的線程一樣。 唯一的缺點是你需要安裝python 2.5,這是相當新的,並且默認情況下包含在大多數Linux發行版或OSX中。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM