Jak zapobiec wykrywaniu Qgis jako „nie reagującej” podczas uruchamiania ciężkiej wtyczki?

10

Korzystam z poniższego wiersza, aby poinformować użytkownika o statusie:

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

Wtyczka działa na moim zestawie danych około 2 minut, ale system Windows wykrywa go jako „nie odpowiada” i przestaje wyświetlać aktualizacje stanu. Dla nowego użytkownika nie jest to tak dobre, ponieważ wygląda na to, że program się zawiesił.

Czy jest coś do obejścia, aby użytkownik nie pozostawał w ciemności, jeśli chodzi o status wtyczki?

Johan Holtby
źródło

Odpowiedzi:

13

Jak zauważa Nathan W , sposobem na to jest wielowątkowość, ale podklasa QThread nie jest najlepszą praktyką. Zobacz tutaj: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

Zobacz poniżej przykład tworzenia a QObject, a następnie przenieś go do QThread(tj. „Prawidłowego” sposobu na zrobienie tego). Ten przykład oblicza całkowity obszar wszystkich funkcji w warstwie wektorowej (przy użyciu nowego API QGIS 2.0!).

Najpierw tworzymy obiekt „roboczy”, który wykona za nas ciężkie podnoszenie:

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)

Aby użyć pracownika, musimy zainicjować go warstwą wektorową, przenieść go do wątku, podłączyć niektóre sygnały, a następnie uruchomić. Prawdopodobnie najlepiej jest zajrzeć na powyższego bloga, aby zrozumieć, co się tutaj dzieje.

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

Ten przykład ilustruje kilka kluczowych punktów:

  • Wszystko w run()metodzie pracownika znajduje się w instrukcji try-wyjątkiem. Trudno go odzyskać, gdy kod ulega awarii w wątku. Emituje ślad poprzez sygnał błędu, który zwykle podłączam do QgsMessageLog.
  • Gotowy sygnał informuje podłączoną metodę, czy proces zakończył się powodzeniem, a także wynik.
  • Sygnał postępu jest wywoływany tylko wtedy, gdy zmienia się procent ukończenia, a nie raz dla każdej funkcji. Zapobiega to zbyt dużej liczbie wywołań, aby zaktualizować pasek postępu, spowalniając proces roboczy, co zniweczyłoby cały punkt uruchamiania pracownika w innym wątku: oddzielenie obliczeń od interfejsu użytkownika.
  • Pracownik implementuje kill()metodę, która umożliwia łagodne zakończenie funkcji. Nie próbuj używać tej terminate()metody w QThread- mogą się zdarzyć złe rzeczy!

Pamiętaj, aby śledzić swoje obiekty threadi workerobiekty gdzieś w strukturze wtyczek. Qt się denerwuje, jeśli nie. Najprostszym sposobem na to jest przechowywanie ich w oknie dialogowym podczas ich tworzenia, np .:

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

Lub możesz pozwolić Qt przejąć na własność QThread:

thread = QtCore.QThread(self)

Dużo czasu zajęło mi wykopanie wszystkich samouczków, aby złożyć ten szablon razem, ale od tego czasu używam go wszędzie.

Snorfalorpagus
źródło
Dziękuję, tego właśnie szukałem i było to bardzo pomocne! Jestem przyzwyczajony do wątków w C #, ale nie myślałem o tym w Pythonie.
Johan Holtby
Tak, to poprawny sposób.
Nathan W
1
Czy powinno być „ja”. przed warstwą w „features = layer.getFeatures ()”? -> „features = self.layer.getFeatures ()”
Håvard Tveite
@ HåvardTveite Masz rację. Naprawiłem kod w odpowiedzi.
Snorfalorpagus
Staram się podążać za tym wzorem w pisanym przeze mnie skrypcie przetwarzania i mam problem z uruchomieniem go. Próbowałem skopiować ten przykład do pliku skryptu, dodałem niezbędne instrukcje importu i zmieniłem worker.progress.connect(self.ui.progressBar)na coś innego, ale za każdym razem, gdy go uruchamiam, qgis-bin ulega awarii. Nie mam doświadczenia w debugowaniu kodu Python ani qgis. Dostaję tylko to, Access violation reading location 0x0000000000000008więc wydaje się, że coś jest nieważne. Czy brakuje jakiegoś kodu konfiguracji, aby móc go użyć w skrypcie przetwarzania?
TJ Rockefeller
4

Twoim jedynym prawdziwym sposobem na to jest wielowątkowość.

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

Trochę dodatkowych lektur http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Uwaga Niektóre osoby nie lubią dziedziczyć po QThread i najwyraźniej nie jest to „poprawny” sposób, ale działa, więc…

Nathan W.
źródło
:) Wygląda na niezły brudny sposób na zrobienie tego. Czasami styl nie jest konieczny. Tym razem (pierwszy w pyqt) myślę, że pójdę właściwą drogą, ponieważ jestem przyzwyczajony do tego w C #.
Johan Holtby
2
To nie jest brudny sposób, to był stary sposób na zrobienie tego.
Nathan W
2

Ponieważ to pytanie jest stosunkowo stare, zasługuje na aktualizację. W QGIS 3 dostępne jest podejście z QgsTask.fromFunction (), QgsProcessingAlgRunnerTask () i QgsApplication.taskManager (). AddTask ().

Więcej na ten temat na przykład przy użyciu wątków w PyQGIS3 BY MARCO BERNASOCCHI

Miro
źródło