Come posso evitare che Qgis venga rilevato come "non risponde" quando eseguo un plugin pesante?


10

Uso la linea folowing per informare l'utente sullo stato:

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

Il plug-in impiega circa 2 minuti per essere eseguito sul mio set di dati, ma Windows lo rileva come "non risponde" e smette di mostrare gli aggiornamenti di stato. Per un nuovo utente questo non è così buono poiché sembra che il programma si sia bloccato.

C'è qualche soluzione in giro in modo che l'utente non venga lasciato al buio per quanto riguarda lo stato del plugin?

Risposte:


13

Come sottolinea Nathan W , il modo per farlo è con il multithreading, ma la sottoclasse di QThread non è la migliore pratica. Vedi qui: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

Vedi sotto un esempio di come creare un QObject, quindi spostarlo in un QThread(cioè il modo "corretto" per farlo). Questo esempio calcola l'area totale di tutte le funzionalità in un livello vettoriale (usando la nuova API QGIS 2.0!).

Innanzitutto, creiamo l'oggetto "lavoratore" che farà il lavoro pesante per noi:

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)

Per usare il lavoratore dobbiamo inizializzarlo con un livello vettoriale, spostarlo sul thread, collegare alcuni segnali, quindi avviarlo. Probabilmente è meglio guardare il blog linkato sopra per capire cosa sta succedendo qui.

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

Questo esempio illustra alcuni punti chiave:

  • Tutto all'interno del run()metodo del lavoratore è all'interno di un'istruzione try-tranne. È difficile ripristinare quando il codice si arresta in modo anomalo all'interno di un thread. Emette il traceback tramite il segnale di errore, che di solito mi connetto al QgsMessageLog.
  • Il segnale finito indica al metodo collegato se il processo è stato completato correttamente, nonché il risultato.
  • Il segnale di avanzamento viene chiamato solo quando cambia la percentuale completa, anziché una volta per ogni funzione. Questo impedisce troppe chiamate per aggiornare la barra di avanzamento rallentando il processo di lavoro, il che vanificherebbe l'intero punto di esecuzione del lavoratore in un altro thread: separare il calcolo dall'interfaccia utente.
  • Il lavoratore implementa un kill()metodo che consente alla funzione di terminare con grazia. Non provare a utilizzare il terminate()metodo QThread: potrebbero accadere cose brutte!

Assicurati di tenere traccia del tuo threade degli workeroggetti da qualche parte nella struttura del plugin. Qt si arrabbia se non lo fai. Il modo più semplice per farlo è salvarli nella finestra di dialogo quando li crei, ad esempio:

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

Oppure puoi lasciare che Qt diventi proprietario di QThread:

thread = QtCore.QThread(self)

Mi ci è voluto molto tempo per scavare tutti i tutorial per mettere insieme questo modello, ma da allora lo sto riutilizzando dappertutto.


Grazie, questo era esattamente quello che stavo cercando ed è stato molto utile! Sono abituato a thread i C # ma non ci ho pensato in Python.
Johan Holtby,

Sì, questo è il modo corretto.
Nathan W,

1
Dovrebbe esserci un "io". davanti al livello in "features = layer.getFeatures ()"? -> "features = self.layer.getFeatures ()"
Håvard Tveite,

@ HåvardTveite Hai ragione. Ho corretto il codice nella risposta.
Snorfalorpagus,

Sto cercando di seguire questo schema per uno script di elaborazione che sto scrivendo e ho problemi a farlo funzionare. Ho provato a copiare questo esempio in un file di script, ho aggiunto le necessarie istruzioni di importazione e sono cambiato worker.progress.connect(self.ui.progressBar)in qualcos'altro, ma ogni volta che lo eseguo qgis-bin si blocca. Non ho esperienza di debug del codice Python o qgis. Tutto quello che sto ricevendo è Access violation reading location 0x0000000000000008quindi sembra che qualcosa sia nullo. Manca un codice di installazione per poterlo utilizzare in uno script di elaborazione?
TJ Rockefeller,

4

Il tuo unico vero modo per farlo è tramite il 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()

Qualche lettura extra http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Nota Ad alcune persone non piace ereditare da QThread, e apparentemente questo non è il modo "corretto" per farlo, ma funziona così ....


:) Sembra un bel modo sporco di farlo. Alcune volte lo stile non è necessario. Per questa volta (il primo in pyqt) penso che andrò nel modo giusto poiché sono abituato a quello in C #.
Johan Holtby,

2
Non è un modo sporco, era il vecchio modo di farlo.
Nathan W,

2

Dato che questa domanda è relativamente vecchia, merita un aggiornamento. Con QGIS 3 esiste un approccio con QgsTask.fromFunction (), QgsProcessingAlgRunnerTask () e QgsApplication.taskManager (). AddTask ().

Maggiori informazioni ad esempio su Uso dei thread in PyQGIS3 DI MARCO BERNASOCCHI

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.