Wie pflege ich eine resposive GUI mit QThread mit PyQGIS?

11

Ich habe einige Stapelverarbeitungswerkzeuge als Python-Plugins für QGIS 1.8 entwickelt.

Ich habe festgestellt, dass die GUI während der Ausführung meiner Tools nicht mehr reagiert.

Die allgemeine Weisheit ist, dass die Arbeit an einem Arbeitsthread ausgeführt werden sollte, wobei die Status- / Abschlussinformationen als Signale an die GUI zurückgegeben werden sollten.

Ich habe die Dokumente zum Flussufer gelesen und die Quelle von doGeometry.py (eine funktionierende Implementierung von ftools ) untersucht.

Mit diesen Quellen habe ich versucht, eine einfache Implementierung zu erstellen, um diese Funktionalität zu untersuchen, bevor Änderungen an einer etablierten Codebasis vorgenommen werden.

Die Gesamtstruktur ist ein Eintrag im Plugins-Menü, der einen Dialog mit Start- und Stopp-Schaltflächen führt. Die Schaltflächen steuern einen Thread, der bis 100 zählt, und senden für jede Nummer ein Signal an die GUI zurück. Die GUI empfängt jedes Signal und sendet eine Zeichenfolge, die die Nummer sowohl des Nachrichtenprotokolls als auch den Fenstertitel enthält.

Der Code dieser Implementierung ist hier:

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *

class ThreadTest:

    def __init__(self, iface):
        self.iface = iface

    def initGui(self):
        self.action = QAction( u"ThreadTest", self.iface.mainWindow())
        self.action.triggered.connect(self.run)
        self.iface.addPluginToMenu(u"&ThreadTest", self.action)

    def unload(self):
        self.iface.removePluginMenu(u"&ThreadTest",self.action)

    def run(self):
        BusyDialog(self.iface.mainWindow())

class BusyDialog(QDialog):
    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setLayout(QVBoxLayout())
        self.startButton = QPushButton("Start", self)
        self.startButton.clicked.connect(self.startButtonHandler)
        self.layout().addWidget(self.startButton)
        self.stopButton=QPushButton("Stop", self)
        self.stopButton.clicked.connect(self.stopButtonHandler)
        self.layout().addWidget(self.stopButton)
        self.show()

    def startButtonHandler(self, toggle):
        self.workerThread = WorkerThread(self.parent)
        QObject.connect( self.workerThread, SIGNAL( "killThread(PyQt_PyObject)" ), \
                                                self.killThread )
        QObject.connect( self.workerThread, SIGNAL( "echoText(PyQt_PyObject)" ), \
                                                self.setText)
        self.workerThread.start(QThread.LowestPriority)
        QgsMessageLog.logMessage("end: startButtonHandler")

    def stopButtonHandler(self, toggle):
        self.killThread()

    def setText(self, text):
        QgsMessageLog.logMessage(str(text))
        self.setWindowTitle(text)

    def killThread(self):
        if self.workerThread.isRunning():
            self.workerThread.exit(0)


class WorkerThread(QThread):
    def __init__(self, parent):
        QThread.__init__(self,parent)

    def run(self):
        self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: starting work" )
        self.doLotsOfWork()
        self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: finshed work" )
        self.emit( SIGNAL( "killThread(PyQt_PyObject)"), "OK")

    def doLotsOfWork(self):
        count=0
        while count < 100:
            self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: " + str(count) )
            count += 1
#           if self.msleep(10):
#               return
#          QThread.yieldCurrentThread()

Leider funktioniert es nicht so ruhig, wie ich es mir erhofft hatte:

  • Der Fenstertitel aktualisiert "live" mit dem Zähler, aber wenn ich auf den Dialog klicke, reagiert er nicht.
  • Das Nachrichtenprotokoll ist inaktiv, bis der Zähler endet, und zeigt dann alle Nachrichten gleichzeitig an. Diese Nachrichten werden von QgsMessageLog mit einem Zeitstempel versehen. Diese Zeitstempel zeigen an, dass sie "live" mit dem Zähler empfangen wurden, dh sie werden weder vom Arbeitsthread noch vom Dialog in die Warteschlange gestellt.
  • Die Reihenfolge der Nachrichten im Protokoll (Auszug folgt) gibt an, dass startButtonHandler die Ausführung abschließt, bevor die Kommentare des Arbeitsthreads funktionieren, dh der Thread verhält sich wie ein Thread.

    end: startButtonHandler
    Emit: starting work
    Emit: 0
    ...
    Emit: 99
    Emit: finshed work
    
  • Es scheint, dass der Worker-Thread keine Ressourcen mit dem GUI-Thread teilt. Es gibt ein paar auskommentierte Zeilen am Ende der obigen Quelle, in denen ich versucht habe, msleep () undieldCurrentThread () aufzurufen, aber keine schien zu helfen.

Kann jemand, der Erfahrung damit hat, meinen Fehler erkennen? Ich hoffe, es ist ein einfacher, aber grundlegender Fehler, der leicht zu korrigieren ist, sobald er identifiziert ist.

Kelly Thomas
quelle
Ist es normal, dass die Stopp-Taste nicht angeklickt werden kann? Das Hauptziel der reaktionsschnellen Benutzeroberfläche besteht darin, den Vorgang abzubrechen, wenn er zu lang ist. Ich versuche, Ihr Skript zu ändern, aber die Schaltfläche funktioniert nicht ordnungsgemäß. Wie brechen Sie Ihren Thread ab?
Etrimaille

Antworten:

6

Also habe ich mir dieses Problem noch einmal angesehen. Ich habe bei Null angefangen und hatte Erfolg, dann habe ich mir den obigen Code noch einmal angesehen und kann ihn immer noch nicht reparieren.

Um jedem, der sich mit diesem Thema befasst, ein funktionierendes Beispiel zu geben, werde ich hier Funktionscode bereitstellen:

from PyQt4.QtCore import *
from PyQt4.QtGui import *

class ThreadManagerDialog(QDialog):
    def __init__( self, iface, title="Worker Thread"):
        QDialog.__init__( self, iface.mainWindow() )
        self.iface = iface
        self.setWindowTitle(title)
        self.setLayout(QVBoxLayout())
        self.primaryLabel = QLabel(self)
        self.layout().addWidget(self.primaryLabel)
        self.primaryBar = QProgressBar(self)
        self.layout().addWidget(self.primaryBar)
        self.secondaryLabel = QLabel(self)
        self.layout().addWidget(self.secondaryLabel)
        self.secondaryBar = QProgressBar(self)
        self.layout().addWidget(self.secondaryBar)
        self.closeButton = QPushButton("Close")
        self.closeButton.setEnabled(False)
        self.layout().addWidget(self.closeButton)
        self.closeButton.clicked.connect(self.reject)
    def run(self):
        self.runThread()
        self.exec_()
    def runThread( self):
        QObject.connect( self.workerThread, SIGNAL( "jobFinished( PyQt_PyObject )" ), self.jobFinishedFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryValue( PyQt_PyObject )" ), self.primaryValueFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryRange( PyQt_PyObject )" ), self.primaryRangeFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryText( PyQt_PyObject )" ), self.primaryTextFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryValue( PyQt_PyObject )" ), self.secondaryValueFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryRange( PyQt_PyObject )" ), self.secondaryRangeFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryText( PyQt_PyObject )" ), self.secondaryTextFromThread )
        self.workerThread.start()
    def cancelThread( self ):
        self.workerThread.stop()
    def jobFinishedFromThread( self, success ):
        self.workerThread.stop()
        self.primaryBar.setValue(self.primaryBar.maximum())
        self.secondaryBar.setValue(self.secondaryBar.maximum())
        self.emit( SIGNAL( "jobFinished( PyQt_PyObject )" ), success )
        self.closeButton.setEnabled( True )
    def primaryValueFromThread( self, value ):
        self.primaryBar.setValue(value)
    def primaryRangeFromThread( self, range_vals ):
        self.primaryBar.setRange( range_vals[ 0 ], range_vals[ 1 ] )
    def primaryTextFromThread( self, value ):
        self.primaryLabel.setText(value)
    def secondaryValueFromThread( self, value ):
        self.secondaryBar.setValue(value)
    def secondaryRangeFromThread( self, range_vals ):
        self.secondaryBar.setRange( range_vals[ 0 ], range_vals[ 1 ] )
    def secondaryTextFromThread( self, value ):
        self.secondaryLabel.setText(value)

class WorkerThread( QThread ):
    def __init__( self, parentThread):
        QThread.__init__( self, parentThread )
    def run( self ):
        self.running = True
        success = self.doWork()
        self.emit( SIGNAL( "jobFinished( PyQt_PyObject )" ), success )
    def stop( self ):
        self.running = False
        pass
    def doWork( self ):
        return True
    def cleanUp( self):
        pass

class CounterThread(WorkerThread):
    def __init__(self, parentThread):
        WorkerThread.__init__(self, parentThread)
    def doWork(self):
        target = 100000000
        stepP= target/100
        stepS=target/10000
        self.emit( SIGNAL( "primaryText( PyQt_PyObject )" ), "Primary" )
        self.emit( SIGNAL( "secondaryText( PyQt_PyObject )" ), "Secondary" )
        self.emit( SIGNAL( "primaryRange( PyQt_PyObject )" ), ( 0, 100 ) )
        self.emit( SIGNAL( "secondaryRange( PyQt_PyObject )" ), ( 0, 100 ) )
        count = 0
        while count < target:
            if count % stepP == 0:
                self.emit( SIGNAL( "primaryValue( PyQt_PyObject )" ), int(count / stepP) )
            if count % stepS == 0:  
                self.emit( SIGNAL( "secondaryValue( PyQt_PyObject )" ), count % stepP / stepS )
            if not self.running:
                return False
            count += 1
        return True

d = ThreadManagerDialog(qgis.utils.iface, "CounterThread Demo")
d.workerThread = CounterThread(qgis.utils.iface.mainWindow())
d.run()

Die Struktur dieses Beispiels ist eine ThreadManagerDialog-Klasse, der dann ein WorkerThread (oder eine Unterklasse) zugewiesen werden kann. Wenn die Ausführungsmethode des Dialogfelds aufgerufen wird, wird wiederum die doWork-Methode für den Worker aufgerufen. Das Ergebnis ist, dass jeder Code in doWork in einem separaten Thread ausgeführt wird und die GUI frei ist, auf Benutzereingaben zu reagieren.

In diesem Beispiel wird eine Instanz von CounterThread als Worker zugewiesen, und einige Fortschrittsbalken werden etwa eine Minute lang beschäftigt sein.

Hinweis: Dies ist so formatiert, dass es zum Einfügen in die Python-Konsole bereit ist. Die letzten drei Zeilen müssen entfernt werden, bevor sie in einer .py-Datei gespeichert werden.

Kelly Thomas
quelle
Dies ist ein großartiges Plug-and-Play-Beispiel! Ich bin gespannt auf die beste Position in diesem Code für die Implementierung unseres eigenen Arbeitsalgorithmus. Müsste dies in der Klasse WorkerThread oder vielmehr in der Klasse CounterThread def doWork platziert werden? [Gefragt im Interesse der Verbindung dieser Fortschrittsbalken mit den eingefügten Worker-Algorithmen]
Katalpa
Ja, CounterThreadist nur ein Beispiel für eine Kinderklasse WorkerThread. Wenn Sie eine eigene untergeordnete Klasse mit einer aussagekräftigeren Implementierung von erstellen doWork, sollte es Ihnen gut gehen.
Kelly Thomas
Die Eigenschaften des CounterThread gelten für mein Ziel (detaillierte Benachrichtigungen des Benutzers über den Fortschritt) - aber wie würde dies in eine neue c.class 'doWork'-Routine integriert werden? (auch - Platzierung weise, 'doWork' innerhalb des CounterThread richtig?)
Katalpa
Die CounterThread-Implementierung oben a) initialisiert den Job, b) initialisiert den Dialog, c) führt eine Kernschleife durch, d) gibt nach erfolgreichem Abschluss true zurück. Jede Aufgabe, die mit einer Schleife implementiert werden kann, sollte einfach an Ort und Stelle fallen. Eine Warnung, die ich anbieten werde, ist, dass das Senden der Signale zur Kommunikation mit dem Manager mit einem gewissen Overhead verbunden ist, dh wenn es bei jeder Iteration einer schnellen Schleife aufgerufen wird, kann dies zu einer höheren Latenz führen als der eigentliche Job.
Kelly Thomas
Vielen Dank für alle Ratschläge. Könnte schwierig sein, damit dies in meiner Situation funktioniert. Derzeit verursacht doWork einen Minidump-Absturz in qgis. Ein Ergebnis einer zu hohen Belastung oder meiner (Anfänger-) Programmierkenntnisse?
Katalpa