Comment empêcher Qgis d'être détecté comme «ne répondant pas» lors de l'exécution d'un plugin lourd?

10

J'utilise la ligne suivante pour informer l'utilisateur de l'état:

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

Le plugin prend environ 2 minutes pour s'exécuter sur mon ensemble de données, mais Windows le détecte comme "ne répondant pas" et arrête d'afficher les mises à jour d'état. Pour un nouvel utilisateur, ce n'est pas si bon car il semble que le programme ait planté.

Existe-t-il des solutions pour que l'utilisateur ne soit pas laissé dans l'ignorance concernant l'état du plugin?

Johan Holtby
la source

Réponses:

13

Comme le souligne Nathan W , la façon de le faire est d'utiliser le multithreading, mais le sous-classement de QThread n'est pas la meilleure pratique. Voir ici: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

Voir ci-dessous un exemple de la façon de créer un QObject, puis de le déplacer vers un QThread(c'est-à-dire la manière "correcte" de le faire). Cet exemple calcule la surface totale de toutes les entités d'une couche vectorielle (à l'aide de la nouvelle API QGIS 2.0!).

Tout d'abord, nous créons l'objet "travailleur" qui fera le gros du travail pour nous:

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)

Pour utiliser l'ouvrier, nous devons l'initaliser avec un calque vectoriel, le déplacer vers le thread, connecter certains signaux, puis le démarrer. Il est probablement préférable de consulter le blog lié ci-dessus pour comprendre ce qui se passe ici.

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

Cet exemple illustre quelques points clés:

  • Tout à l'intérieur de la run()méthode de l'ouvrier est à l'intérieur d'une instruction try-except. Il est difficile de récupérer lorsque votre code se bloque dans un thread. Il émet la trace via le signal d'erreur, que je connecte habituellement au QgsMessageLog.
  • Le signal terminé indique à la méthode connectée si le processus s'est terminé avec succès, ainsi que le résultat.
  • Le signal de progression n'est appelé que lorsque le pourcentage d'achèvement change, plutôt qu'une fois pour chaque fonctionnalité. Cela empêche trop d'appels pour mettre à jour la barre de progression, ce qui ralentit le processus de travail, ce qui irait à l'encontre de l'intérêt de l'exécution du travailleur dans un autre thread: séparer le calcul de l'interface utilisateur.
  • L'ouvrier implémente une kill()méthode qui permet à la fonction de se terminer correctement. N'essayez pas d'utiliser la terminate()méthode dans QThread- de mauvaises choses pourraient arriver!

Assurez-vous de garder une trace de vos objets threadet workerquelque part dans la structure de votre plugin. Qt se met en colère si vous ne le faites pas. La façon la plus simple de le faire est de les stocker dans votre boîte de dialogue lorsque vous les créez, par exemple:

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

Ou vous pouvez laisser Qt s'approprier le QThread:

thread = QtCore.QThread(self)

Il m'a fallu beaucoup de temps pour déterrer tous les tutoriels afin de rassembler ce modèle, mais depuis lors, je l'ai réutilisé partout.

Snorfalorpagus
la source
Merci c'était exactement ce que je cherchais et c'était très utile! Je suis habitué aux threads en C # mais je n'y ai pas pensé en python.
Johan Holtby
Oui, c'est la bonne façon.
Nathan W
1
Devrait-il y avoir un «moi»? devant le calque dans "features = layer.getFeatures ()"? -> "features = self.layer.getFeatures ()"
Håvard Tveite
@ HåvardTveite Vous avez raison. J'ai corrigé le code dans la réponse.
Snorfalorpagus
J'essaie de suivre ce modèle pour un script de traitement que j'écris, et j'ai du mal à le faire fonctionner. J'ai essayé de copier cet exemple dans un fichier de script, ajouté les instructions d'importation nécessaires et changé worker.progress.connect(self.ui.progressBar)pour autre chose, mais chaque fois que je l'exécute, qgis-bin plante. Je n'ai aucune expérience du débogage de code python ou qgis. Tout ce que je reçois c'est Access violation reading location 0x0000000000000008qu'il semble que quelque chose soit nul. Y a-t-il du code de configuration qui manque pour pouvoir l'utiliser dans un script de traitement?
TJ Rockefeller du
4

Votre seule véritable façon de procéder est le 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()

Quelques lectures supplémentaires http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Remarque Certaines personnes n'aiment pas hériter de QThread, et apparemment ce n'est pas la manière "correcte" de le faire, mais cela fonctionne donc ....

Nathan W
la source
:) Cela ressemble à une belle façon sale de le faire. Parfois, le style n'est pas nécessaire. Pour cette fois (le premier en pyqt) je pense que j'irai dans le bon sens puisque j'y suis habitué en C #.
Johan Holtby
2
Ce n'est pas une façon sale, c'était l'ancienne façon de le faire.
Nathan W
2

Cette question étant relativement ancienne, elle mérite une mise à jour. Avec QGIS 3, il existe une approche avec QgsTask.fromFunction (), QgsProcessingAlgRunnerTask () et QgsApplication.taskManager (). AddTask ().

Plus d'informations à ce sujet, par exemple à l' aide de threads dans PyQGIS3 PAR MARCO BERNASOCCHI

Miro
la source