¿Cómo evito que se detecte Qgis como "no responde" cuando ejecuto un complemento pesado?

10

Utilizo la siguiente línea para informar al usuario sobre el estado:

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

El complemento tarda unos 2 minutos en ejecutarse en mi conjunto de datos, pero Windows lo detecta como "no responde" y deja de mostrar las actualizaciones de estado. Para un nuevo usuario, esto no es tan bueno ya que parece que el programa se ha bloqueado.

¿Hay alguna solución para que el usuario no se quede en la oscuridad con respecto al estado del complemento?

Johan Holtby
fuente

Respuestas:

13

Como señala Nathan W , la forma de hacerlo es con subprocesos múltiples, pero subclasificar QThread no es la mejor práctica. Ver aquí: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

Vea a continuación un ejemplo de cómo crear un QObject, luego muévalo a un QThread(es decir, la forma "correcta" de hacerlo). Este ejemplo calcula el área total de todas las características en una capa vectorial (¡usando la nueva API QGIS 2.0!).

Primero, creamos el objeto "trabajador" que hará el trabajo pesado por nosotros:

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)

Para usar el trabajador necesitamos inicializarlo con una capa vectorial, moverlo al hilo, conectar algunas señales y luego iniciarlo. Probablemente sea mejor mirar el blog vinculado anteriormente para comprender lo que está sucediendo aquí.

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

Este ejemplo ilustra algunos puntos clave:

  • Todo dentro del run()método del trabajador está dentro de una declaración try-except. Es difícil recuperarse cuando su código se bloquea dentro de un hilo. Emite el rastreo a través de la señal de error, que generalmente conecto con el QgsMessageLog.
  • La señal finalizada le dice al método conectado si el proceso se completó con éxito, así como el resultado.
  • La señal de progreso solo se llama cuando cambia el porcentaje completado, en lugar de una vez para cada función. Esto evita demasiadas llamadas para actualizar la barra de progreso, lo que ralentiza el proceso del trabajador, lo que anularía el punto de ejecutar el trabajador en otro hilo: separar el cálculo de la interfaz de usuario.
  • El trabajador implementa un kill()método que permite que la función finalice correctamente. No intentes usar el terminate()método QThread, ¡podrían pasar cosas malas!

Asegúrese de realizar un seguimiento de sus objetos thready workeren algún lugar de su estructura de complementos. Qt se enoja si no lo haces. La forma más fácil de hacer esto es almacenarlos en su diálogo cuando los cree, por ejemplo:

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

O puede dejar que Qt tome posesión del QThread:

thread = QtCore.QThread(self)

Me llevó mucho tiempo desenterrar todos los tutoriales para armar esta plantilla, pero desde entonces la he reutilizado por todas partes.

Snorfalorpagus
fuente
¡Gracias, esto era exactamente lo que estaba buscando y fue muy útil! Estoy acostumbrado a subprocesos en C # pero no pensé en Python.
Johan Holtby
Sí, esta es la forma correcta.
Nathan W
1
¿Debería haber un "yo"? delante de la capa en "features = layer.getFeatures ()"? -> "features = self.layer.getFeatures ()"
Håvard Tveite
@ HåvardTveite Tienes razón. He arreglado el código en la respuesta.
Snorfalorpagus
Estoy tratando de seguir este patrón para un script de procesamiento que estoy escribiendo, y tengo problemas para que funcione. Intenté copiar este ejemplo en un archivo de script, agregué las declaraciones de importación necesarias y cambié worker.progress.connect(self.ui.progressBar)a algo más, pero cada vez que lo ejecuto qgis-bin se bloquea. No tengo experiencia depurando código python o qgis. Todo lo que obtengo es Access violation reading location 0x0000000000000008que parece que algo es nulo. ¿Falta algún código de configuración para poder usar esto en un script de procesamiento?
TJ Rockefeller
4

Su única forma verdadera de hacerlo es mediante subprocesos múltiples.

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

Algunas lecturas adicionales http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Nota A algunas personas no les gusta heredar de QThread, y aparentemente esta no es la forma "correcta" de hacerlo, pero funciona así ...

Nathan W
fuente
:) Parece una buena forma sucia de hacerlo. Algunas veces el estilo no es necesario. Para este momento (el primero en pyqt) creo que seguiré el camino correcto ya que estoy acostumbrado a eso en C #.
Johan Holtby
2
No es una forma sucia, era la vieja forma de hacerlo.
Nathan W
2

Como esta pregunta es relativamente antigua, merece una actualización. Con QGIS 3 hay un acercamiento con QgsTask.fromFunction (), QgsProcessingAlgRunnerTask () y QgsApplication.taskManager (). AddTask ().

Más sobre esto, por ejemplo, en Usar hilos en PyQGIS3 POR MARCO BERNASOCCHI

Miró
fuente