Wie kann ich verhindern, dass Qgis beim Ausführen eines schweren Plugins als "nicht reagierend" erkannt wird?

10

Ich benutze die folgende Zeile, um den Benutzer über den Status zu informieren:

iface.mainWindow().statusBar().showMessage("Status:" + str(i))

Es dauert ungefähr 2 Minuten, bis das Plugin für meinen Datensatz ausgeführt wird. Windows erkennt jedoch, dass es nicht reagiert, und zeigt die Statusaktualisierungen nicht mehr an. Für einen neuen Benutzer ist dies nicht so gut, da das Programm anscheinend abgestürzt ist.

Gibt es irgendwelche Umgehungsmöglichkeiten, damit der Benutzer hinsichtlich des Status des Plugins nicht im Dunkeln bleibt?

Johan Holtby
quelle

Antworten:

13

Wie Nathan W betont, ist der Weg, dies zu tun, Multithreading, aber die Unterklasse von QThread ist keine bewährte Methode. Siehe hier: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

Unten sehen Sie ein Beispiel, wie Sie ein erstellen QObjectund dann auf ein verschieben QThread(dh die "richtige" Vorgehensweise). In diesem Beispiel wird die Gesamtfläche aller Features in einer Vektorebene berechnet (mithilfe der neuen QGIS 2.0-API!).

Zuerst erstellen wir das "Arbeiter" -Objekt, das das schwere Heben für uns erledigt:

class Worker(QtCore.QObject):
    def __init__(self, layer, *args, **kwargs):
        QtCore.QObject.__init__(self, *args, **kwargs)
        self.layer = layer
        self.total_area = 0.0
        self.processed = 0
        self.percentage = 0
        self.abort = False

    def run(self):
        try:
            self.status.emit('Task started!')
            self.feature_count = self.layer.featureCount()
            features = self.layer.getFeatures()
            for feature in features:
                if self.abort is True:
                    self.killed.emit()
                    break
                geom = feature.geometry()
                self.total_area += geom.area()
                self.calculate_progress()
            self.status.emit('Task finished!')
        except:
            import traceback
            self.error.emit(traceback.format_exc())
            self.finished.emit(False, self.total_area)
        else:
            self.finished.emit(True, self.total_area)

    def calculate_progress(self):
        self.processed = self.processed + 1
        percentage_new = (self.processed * 100) / self.feature_count
        if percentage_new > self.percentage:
            self.percentage = percentage_new
            self.progress.emit(self.percentage)

    def kill(self):
        self.abort = True

    progress = QtCore.pyqtSignal(int)
    status = QtCore.pyqtSignal(str)
    error = QtCore.pyqtSignal(str)
    killed = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal(bool, float)

Um den Worker zu verwenden, müssen wir ihn mit einer Vektorebene initialisieren, in den Thread verschieben, einige Signale verbinden und dann starten. Am besten schauen Sie sich den oben verlinkten Blog an, um zu verstehen, was hier vor sich geht.

thread = QtCore.QThread()
worker = Worker(layer)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.progress.connect(self.ui.progressBar)
worker.status.connect(iface.mainWindow().statusBar().showMessage)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
worker.finished.connect(thread.quit)
thread.start()

Dieses Beispiel zeigt einige wichtige Punkte:

  • Alles in der run()Methode des Workers befindet sich in einer try-without-Anweisung. Es ist schwierig, sich zu erholen, wenn Ihr Code in einem Thread abstürzt. Es gibt den Traceback über das Fehlersignal aus, das ich normalerweise mit dem verbinde QgsMessageLog.
  • Das fertige Signal teilt der verbundenen Methode mit, ob der Prozess erfolgreich abgeschlossen wurde, sowie das Ergebnis.
  • Das Fortschrittssignal wird nur aufgerufen, wenn sich der Prozentsatz der Fertigstellung ändert, und nicht einmal für jede Funktion. Dies verhindert, dass zu viele Aufrufe zum Aktualisieren des Fortschrittsbalkens den Arbeitsprozess verlangsamen, was den gesamten Punkt der Ausführung des Arbeiters in einem anderen Thread zunichte machen würde: um die Berechnung von der Benutzeroberfläche zu trennen.
  • Der Worker implementiert eine kill()Methode, mit der die Funktion ordnungsgemäß beendet werden kann. Versuchen Sie nicht, die terminate()Methode anzuwenden QThread- es können schlimme Dinge passieren!

Achten Sie darauf, Ihre threadund workerObjekte irgendwo in Ihrer Plugin-Struktur zu verfolgen . Qt wird wütend, wenn Sie nicht. Der einfachste Weg, dies zu tun, besteht darin, sie in Ihrem Dialog zu speichern, wenn Sie sie erstellen, z.

thread = self.thread = QtCore.QThread()
worker = self.worker = Worker(layer)

Oder Sie können Qt das Eigentum an QThread übertragen lassen:

thread = QtCore.QThread(self)

Ich habe lange gebraucht, um alle Tutorials zu finden, um diese Vorlage zusammenzustellen, aber seitdem habe ich sie überall wiederverwendet.

Snorfalorpagus
quelle
Vielen Dank, das war genau das, wonach ich gesucht habe und es war sehr hilfreich! Ich bin es gewohnt, i C # zu fädeln, habe aber in Python nicht daran gedacht.
Johan Holtby
Ja, das ist der richtige Weg.
Nathan W
1
Sollte es ein "Selbst" geben. vor der Ebene in "features = layer.getFeatures ()"? -> "features = self.layer.getFeatures ()"
Håvard Tveite
@ HåvardTveite Du bist richtig. Ich habe den Code in der Antwort korrigiert.
Snorfalorpagus
Ich versuche, diesem Muster für ein Verarbeitungsskript zu folgen, das ich schreibe, und ich habe Probleme, es zum Laufen zu bringen. Ich habe versucht, dieses Beispiel in eine Skriptdatei zu kopieren, die erforderlichen Importanweisungen hinzugefügt und worker.progress.connect(self.ui.progressBar)in etwas anderes geändert , aber jedes Mal, wenn ich es ausführe, stürzt qgis-bin ab. Ich habe keine Erfahrung mit dem Debuggen von Python-Code oder QGIS. Alles was ich bekomme ist, Access violation reading location 0x0000000000000008dass es so aussieht, als ob etwas null ist. Fehlt ein Setup-Code, um diesen in einem Verarbeitungsskript verwenden zu können?
TJ Rockefeller
4

Ihr einzig wahrer Weg, dies zu tun, ist Multithreading.

class MyLongRunningStuff(QThread):
    progressReport = pyqtSignal(str)
    def __init__(self):
       QThread.__init__(self)

    def run(self):
       # do your long runnning thing
       self.progressReport.emit("I just did X")

 thread = MyLongRunningStuff()
 thread.progressReport.connect(self.updatetheuimethod)
 thread.start()

Einige zusätzliche Informationen http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Hinweis Einige Leute erben nicht gerne von QThread, und anscheinend ist dies nicht die "richtige" Methode, aber es funktioniert so ...

Nathan W.
quelle
:) Es sieht nach einer schönen schmutzigen Art aus, es zu tun. Manchmal ist Stil nicht notwendig. Für diese Zeit (die erste in pyqt) denke ich, dass ich den richtigen Weg einschlagen werde, da ich das in C # gewohnt bin.
Johan Holtby
2
Es ist kein schmutziger Weg, es war der alte Weg, es zu tun.
Nathan W