[英]How to see a file download progress without the GUI freezing (python 3.4, pyQt5) using QThread
[英]PyQt: How to update progress without freezing the GUI?
我的想法:讓線程在更新進度時發出 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將被更新。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.