22 Stimmen

PyQt: Wie kann man den Fortschritt aktualisieren, ohne die GUI einzufrieren?

Fragen:

  1. Was ist die beste Methode, um den Fortschritt eines Threads zu verfolgen, ohne die GUI zu sperren ("Reagiert nicht")?
  2. Im Allgemeinen, was sind die besten Praktiken für Threadbearbeitung im Hinblick auf die GUI-Entwicklung?

Frage Hintergrund:

  • Ich habe eine PyQt GUI für Windows.
  • Sie wird verwendet, um Sets von HTML-Dokumenten zu verarbeiten.
  • Es dauert zwischen drei Sekunden und drei Stunden, um ein Set von Dokumenten zu verarbeiten.
  • Ich möchte in der Lage sein, mehrere Sets gleichzeitig zu verarbeiten.
  • Ich möchte nicht, dass die GUI blockiert.
  • Ich betrachte das Threading-Modul, um dies zu erreichen.
  • Ich bin relativ neu im Bereich Threading.
  • Die GUI hat eine Fortschrittsanzeige.
  • Ich möchte, dass sie den Fortschritt des ausgewählten Threads anzeigt.
  • Ergebnisse des ausgewählten Threads anzeigen, wenn er fertig ist.
  • Ich benutze Python 2.5.

Mein Gedanke: Die Threads sollen ein QtSignal aussenden, wenn der Fortschritt aktualisiert wird, was eine Funktion auslöst, die die Fortschrittsanzeige aktualisiert. Außerdem soll ein Signal gesendet werden, wenn die Verarbeitung abgeschlossen ist, sodass die Ergebnisse angezeigt werden können.

#HINWEIS: Dies ist Beispielcode für meine Idee, du musst dies nicht lesen, um die Frage(n) zu beantworten.

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 zwischen 0 und 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)

            #Signal nur aussenden, wenn der Fortschritt sich geändert hat
            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):
        ''' Dies ist trivial verkürzt '''
        return re.findall(']*>.*?', 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):
        #neuen Thread erstellen
        p_thread = ProcessingThread(docs)
        thread_name = "example_thread_name"
        p_thread.setName(thread_name)
        p_thread.start()

        #Thread zur Thread-Liste hinzufügen
        self.processing_threads[thread_name] = p_thread

        #Fortschrittsobjekt initialisieren für diesen Thread
        self.progress_object[thread_name] = p_thread.progress  

        #Thread-Signale mit GuiApp-Funktionen verbinden
        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):
        #Fortschrittsobjekt für alle Threads aktualisieren
        self.progress_object[thread_name] = self.processing_threads[thread_name].progress

        #Fortschrittsanzeige für ausgewählten Thread aktualisieren
        if self.selected_thread == thread_name:
            self.setProgressBar(self.progress_object[self.selected_thread])

    def updateResultsObject(self, thread_name):
        #Ergebnisobjekt für Thread mit Ergebnissen aktualisieren
        self.results_object[thread_name] = self.processing_threads[thread_name].getResults()

        #Ergebnis-Widget für ausgewählten Thread aktualisieren
        try:
            self.setResultsWidget(self.results_object[thread_name])
        except KeyError:
            self.setResultsWidget(None)

Jegliche Kommentare zu diesem Ansatz (z.B. Nachteile, Fehler, Lob usw.) werden geschätzt.

Auflösung:

Ich habe letztendlich die QThread-Klasse und zugehörige Signale und Slots verwendet, um zwischen den Threads zu kommunizieren. Dies liegt hauptsächlich daran, dass mein Programm bereits Qt/PyQt4 für die GUI-Objekte/Widgets verwendet. Diese Lösung erforderte auch weniger Änderungen an meinem vorhandenen Code, um sie umzusetzen.

Hier ist ein Link zu einem passenden Qt-Artikel, der erklärt, wie Qt Threads und Signale behandelt, http://www.linuxjournal.com/article/9602. Auszug unten:

Glücklicherweise erlaubt Qt, dass Signale und Slots über Threads hinweg verbunden werden können – solange die Threads ihre eigene Ereignisschleife ausführen. Dies ist eine deutlich sauberere Methode der Kommunikation im Vergleich zum Senden und Empfangen von Ereignissen, da es all das Verwaltung und die Zwischenschritte und Zwischenklassen QEvent vermeidet, die in jeder nicht trivialen Anwendung notwendig werden. Die Kommunikation zwischen Threads wird nun zu einer Sache des Verbindens von Signalen von einem Thread zu den Slots in einem anderen und die Mutexierung und Thread-Sicherheitsprobleme beim Austausch von Daten zwischen Threads werden von Qt behandelt.

Warum ist es notwendig, in jedem Thread, den Sie verbinden möchten, Ereignisschleifen auszuführen? Der Grund hat mit dem Inter-Thread-Kommunikationsmechanismus zu tun, den Qt verwendet, wenn Signale von einem Thread zum Slot eines anderen Threads verbunden werden. Wenn eine solche Verbindung hergestellt wird, wird sie als eine Warteschlangeverbindung bezeichnet. Wenn Signale durch eine Warteschlangeverbindung ausgesendet werden, wird der Slot beim nächsten Ausführen der Ereignisschleife des Zielobjekts aufgerufen. Wenn der Slot stattdessen direkt durch ein Signal aus einem anderen Thread aufgerufen worden wäre, würde dieser Slot im gleichen Kontext wie der aufrufende Thread ausgeführt werden. Normalerweise wäre das nicht das Gewünschte (und erst recht nicht, wenn man eine Datenbankverbindung verwendet, da die Datenbankverbindung nur vom Thread, der sie erstellt hat, verwendet werden kann). Die Warteschlangenverbindung leitet das Signal richtig an das Thread-Objekt weiter und ruft seinen Slot im eigenen Kontext auf, indem es sich am Ereignissystem anhängt. Dies ist genau das, was wir für die Inter-Thread-Kommunikation wollen, bei der einige der Threads Datenbankverbindungen bearbeiten. Der Qt Signal/Slot-Mechanismus ist im Grunde eine Implementierung des oben beschriebenen Inter-Thread-Ereignisweitergabesystems, jedoch mit einer viel saubereren und einfacher zu verwendenden Schnittstelle.

HINWEIS: Auch eliben hat eine gute Antwort, und wenn ich nicht PyQt4 verwenden würde, das die Thread-Sicherheit und Mutexierung behandelt, wäre seine Lösung meine Wahl gewesen.

10voto

David Boddie Punkte 1006

Wenn Sie Signale verwenden möchten, um dem Hauptthread den Fortschritt anzuzeigen, sollten Sie wirklich die QThread-Klasse von PyQt anstelle der Thread-Klasse aus dem Threading-Modul von Python verwenden.

Ein einfaches Beispiel, das QThread, Signale und Slots verwendet, finden Sie im PyQt-Wiki:

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

5voto

Native Python-Warteschlangen funktionieren nicht, weil Sie auf Warteschlangen-get() blockieren müssen, was Ihre Benutzeroberfläche blockiert.

Qt implementiert im Grunde genommen ein Warteschlangensystem im Inneren für die Kommunikation zwischen Threads. Versuchen Sie diesen Aufruf von einem beliebigen Thread aus, um einen Aufruf an einen Slot zu senden.

QtCore.QMetaObject.invokeMethod()

Es ist umständlich und schlecht dokumentiert, aber es sollte das tun, was Sie wollen, auch von einem nicht-Qt-Thread aus.

Sie können auch Event-Mechanismen dafür verwenden. Sehen Sie sich QApplication (oder QCoreApplication) für eine Methode mit einem Namen wie "post" an.

Bearbeitung: Hier ist ein vollständigeres Beispiel...

Ich habe meine eigene Klasse basierend auf QWidget erstellt. Sie hat einen Slot, der einen String akzeptiert; Ich definiere es so:

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

Später erstelle ich eine Instanz dieses Widgets im Haupt-GUI-Thread. Vom Haupt-GUI-Thread oder einem anderen Thread (klopf auf Holz) kann ich folgendes aufrufen:

QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"Hallo Welt"))

Umständlich, aber es bringt Sie ans Ziel.

Dan.

4voto

Eli Bendersky Punkte 246100

Ich empfehle Ihnen, Queue anstelle von Signalisierung zu verwenden. Persönlich finde ich es eine viel robuster und verständlichere Art des Programmierens, da es synchroner ist.

Threads sollten "Jobs" von einer Queue erhalten und die Ergebnisse auf eine andere Queue zurücklegen. Eine dritte Queue kann von den Threads für Benachrichtigungen und Nachrichten wie Fehler und "Fortschrittsberichte" verwendet werden. Sobald Sie Ihren Code auf diese Weise strukturieren, wird es viel einfacher zu verwalten.

Auf diese Weise können auch eine einzige "Job Queue" und "Ergebnis Queue" von einer Gruppe von Worker-Threads verwendet werden, die alle Informationen von den Threads in den Haupt-GUI-Thread leiten.

1 Stimmen

Könnten Sie ein Beispiel liefern? Ich bin ein wenig verwirrt darüber, wie die Threads mit den Queue-Objekten kommunizieren.

2voto

ekhumoro Punkte 106167

Im Folgenden finden Sie ein grundlegendes Beispiel für PyQt5/PySide2, das zeigt, wie ein Hintergrundtask ausgeführt wird, während eine Fortschrittsleiste aktualisiert wird. Der Task wird in einen Worker-Thread verschoben, und benutzerdefinierte Signale werden verwendet, um mit dem Haupt-GUI-Thread zu kommunizieren. Der Task kann angehalten und neu gestartet werden und wird automatisch beendet, wenn das Fenster geschlossen wird.

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

1voto

zihotki Punkte 5211

Wenn Ihre Methode "processDoc" keine anderen Daten ändert (nur nach einigen Daten sucht und sie zurückgibt und keine Variablen oder Eigenschaften der Elternklasse ändert), können Sie die Makros Py_BEGIN_ALLOW_THREADS und Py_END_ALLOW_THREADS verwenden (hier finden Sie Details ). So wird das Dokument in einem Thread verarbeitet, der den Interpreter nicht sperrt und die Benutzeroberfläche aktualisiert wird.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X