Come posso mantenere una GUI resposive usando QThread con PyQGIS


11

Ho sviluppato alcuni strumenti di elaborazione batch come plug-in Python per QGIS 1.8.

Ho scoperto che mentre i miei strumenti sono in esecuzione la GUI diventa non reattiva.

La saggezza generale è che il lavoro dovrebbe essere svolto su un thread di lavoro, con le informazioni sullo stato / completamento restituite alla GUI come segnali.

Ho letto i documenti della riva del fiume e ho studiato la fonte di doGeometry.py (un'implementazione funzionante da ftools ).

Usando queste fonti ho provato a costruire una semplice implementazione per esplorare questa funzionalità prima di apportare modifiche a una base di codice stabilita.

La struttura generale è una voce nel menu dei plug-in, che apre una finestra di dialogo con i pulsanti di avvio e arresto. I pulsanti controllano un thread che conta fino a 100, inviando un segnale alla GUI per ogni numero. La GUI riceve ogni segnale e invia una stringa contenente il numero sia del registro dei messaggi sia del titolo della finestra.

Il codice di questa implementazione è qui:

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *

class ThreadTest:

    def __init__(self, iface):
        self.iface = iface

    def initGui(self):
        self.action = QAction( u"ThreadTest", self.iface.mainWindow())
        self.action.triggered.connect(self.run)
        self.iface.addPluginToMenu(u"&ThreadTest", self.action)

    def unload(self):
        self.iface.removePluginMenu(u"&ThreadTest",self.action)

    def run(self):
        BusyDialog(self.iface.mainWindow())

class BusyDialog(QDialog):
    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setLayout(QVBoxLayout())
        self.startButton = QPushButton("Start", self)
        self.startButton.clicked.connect(self.startButtonHandler)
        self.layout().addWidget(self.startButton)
        self.stopButton=QPushButton("Stop", self)
        self.stopButton.clicked.connect(self.stopButtonHandler)
        self.layout().addWidget(self.stopButton)
        self.show()

    def startButtonHandler(self, toggle):
        self.workerThread = WorkerThread(self.parent)
        QObject.connect( self.workerThread, SIGNAL( "killThread(PyQt_PyObject)" ), \
                                                self.killThread )
        QObject.connect( self.workerThread, SIGNAL( "echoText(PyQt_PyObject)" ), \
                                                self.setText)
        self.workerThread.start(QThread.LowestPriority)
        QgsMessageLog.logMessage("end: startButtonHandler")

    def stopButtonHandler(self, toggle):
        self.killThread()

    def setText(self, text):
        QgsMessageLog.logMessage(str(text))
        self.setWindowTitle(text)

    def killThread(self):
        if self.workerThread.isRunning():
            self.workerThread.exit(0)


class WorkerThread(QThread):
    def __init__(self, parent):
        QThread.__init__(self,parent)

    def run(self):
        self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: starting work" )
        self.doLotsOfWork()
        self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: finshed work" )
        self.emit( SIGNAL( "killThread(PyQt_PyObject)"), "OK")

    def doLotsOfWork(self):
        count=0
        while count < 100:
            self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: " + str(count) )
            count += 1
#           if self.msleep(10):
#               return
#          QThread.yieldCurrentThread()

Sfortunatamente non è tranquillo lavorare come speravo:

  • Il titolo della finestra si aggiorna "live" con il contatore, ma se faccio clic sulla finestra di dialogo, non risponde.
  • Il registro dei messaggi è inattivo fino alla fine del contatore, quindi presenta tutti i messaggi contemporaneamente. Questi messaggi sono contrassegnati con un timestamp da QgsMessageLog e questi timestamp indicano che sono stati ricevuti "in tempo reale" con il contatore, cioè non vengono accodati dal thread di lavoro o dalla finestra di dialogo.
  • L'ordine dei messaggi nel registro (seguito dall'esempio) indica che startButtonHandler completa l'esecuzione prima che il thread di lavoro inizi a funzionare, ovvero il thread si comporta come un thread.

    end: startButtonHandler
    Emit: starting work
    Emit: 0
    ...
    Emit: 99
    Emit: finshed work
  • Sembra che il thread di lavoro non stia condividendo risorse con il thread della GUI. Ci sono un paio di righe commentate alla fine della fonte sopra dove ho provato a chiamare msleep () e yieldCurrentThread (), ma nessuno dei due sembrava aiutare.

Qualcuno con qualche esperienza con questo in grado di individuare il mio errore? Spero che sia un errore semplice ma fondamentale che sia facile da correggere una volta identificato.


È normale che non sia possibile fare clic sul pulsante di arresto? L'obiettivo principale della GUI reattiva è annullare il processo se è troppo lungo. Provo a modificare il tuo script ma non riesco a far funzionare correttamente il pulsante. Come si interrompe il thread?
etrimaille,

Risposte:


6

Quindi ho dato un'altra occhiata a questo problema. Ho iniziato da zero e ho avuto successo, poi sono tornato a guardare il codice sopra e ancora non riesco a risolverlo.

Nell'interesse di fornire un esempio funzionante a chiunque effettui ricerche in questo argomento, fornirò qui un codice funzionale:

from PyQt4.QtCore import *
from PyQt4.QtGui import *

class ThreadManagerDialog(QDialog):
    def __init__( self, iface, title="Worker Thread"):
        QDialog.__init__( self, iface.mainWindow() )
        self.iface = iface
        self.setWindowTitle(title)
        self.setLayout(QVBoxLayout())
        self.primaryLabel = QLabel(self)
        self.layout().addWidget(self.primaryLabel)
        self.primaryBar = QProgressBar(self)
        self.layout().addWidget(self.primaryBar)
        self.secondaryLabel = QLabel(self)
        self.layout().addWidget(self.secondaryLabel)
        self.secondaryBar = QProgressBar(self)
        self.layout().addWidget(self.secondaryBar)
        self.closeButton = QPushButton("Close")
        self.closeButton.setEnabled(False)
        self.layout().addWidget(self.closeButton)
        self.closeButton.clicked.connect(self.reject)
    def run(self):
        self.runThread()
        self.exec_()
    def runThread( self):
        QObject.connect( self.workerThread, SIGNAL( "jobFinished( PyQt_PyObject )" ), self.jobFinishedFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryValue( PyQt_PyObject )" ), self.primaryValueFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryRange( PyQt_PyObject )" ), self.primaryRangeFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryText( PyQt_PyObject )" ), self.primaryTextFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryValue( PyQt_PyObject )" ), self.secondaryValueFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryRange( PyQt_PyObject )" ), self.secondaryRangeFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryText( PyQt_PyObject )" ), self.secondaryTextFromThread )
        self.workerThread.start()
    def cancelThread( self ):
        self.workerThread.stop()
    def jobFinishedFromThread( self, success ):
        self.workerThread.stop()
        self.primaryBar.setValue(self.primaryBar.maximum())
        self.secondaryBar.setValue(self.secondaryBar.maximum())
        self.emit( SIGNAL( "jobFinished( PyQt_PyObject )" ), success )
        self.closeButton.setEnabled( True )
    def primaryValueFromThread( self, value ):
        self.primaryBar.setValue(value)
    def primaryRangeFromThread( self, range_vals ):
        self.primaryBar.setRange( range_vals[ 0 ], range_vals[ 1 ] )
    def primaryTextFromThread( self, value ):
        self.primaryLabel.setText(value)
    def secondaryValueFromThread( self, value ):
        self.secondaryBar.setValue(value)
    def secondaryRangeFromThread( self, range_vals ):
        self.secondaryBar.setRange( range_vals[ 0 ], range_vals[ 1 ] )
    def secondaryTextFromThread( self, value ):
        self.secondaryLabel.setText(value)

class WorkerThread( QThread ):
    def __init__( self, parentThread):
        QThread.__init__( self, parentThread )
    def run( self ):
        self.running = True
        success = self.doWork()
        self.emit( SIGNAL( "jobFinished( PyQt_PyObject )" ), success )
    def stop( self ):
        self.running = False
        pass
    def doWork( self ):
        return True
    def cleanUp( self):
        pass

class CounterThread(WorkerThread):
    def __init__(self, parentThread):
        WorkerThread.__init__(self, parentThread)
    def doWork(self):
        target = 100000000
        stepP= target/100
        stepS=target/10000
        self.emit( SIGNAL( "primaryText( PyQt_PyObject )" ), "Primary" )
        self.emit( SIGNAL( "secondaryText( PyQt_PyObject )" ), "Secondary" )
        self.emit( SIGNAL( "primaryRange( PyQt_PyObject )" ), ( 0, 100 ) )
        self.emit( SIGNAL( "secondaryRange( PyQt_PyObject )" ), ( 0, 100 ) )
        count = 0
        while count < target:
            if count % stepP == 0:
                self.emit( SIGNAL( "primaryValue( PyQt_PyObject )" ), int(count / stepP) )
            if count % stepS == 0:  
                self.emit( SIGNAL( "secondaryValue( PyQt_PyObject )" ), count % stepP / stepS )
            if not self.running:
                return False
            count += 1
        return True

d = ThreadManagerDialog(qgis.utils.iface, "CounterThread Demo")
d.workerThread = CounterThread(qgis.utils.iface.mainWindow())
d.run()

La struttura di questo esempio è una classe ThreadManagerDialog che può essere assegnata a WorkerThread (o sottoclasse). Quando viene chiamato il metodo di esecuzione della finestra di dialogo, a sua volta chiamerà il metodo doWork sul lavoratore. Il risultato è che qualsiasi codice in doWork verrà eseguito in un thread separato, lasciando la GUI libera di rispondere all'input dell'utente.

In questo esempio viene assegnata un'istanza di CounterThread come lavoratore e un paio di barre di avanzamento verranno tenute occupate per circa un minuto.

Nota: questo è formattato in modo che sia pronto per essere incollato nella console di Python. Le ultime tre righe dovranno essere rimosse prima di salvare in un file .py.


Questo è un ottimo esempio plug and play! Sono curioso di sapere quale sia la migliore posizione in questo codice per implementare la nostra algorythmn funzionante. Tale necessità dovrebbe essere collocata nella classe WorkerThread, o piuttosto nella classe CounterThread, def doWork? [Chiesto nell'interesse di collegare queste barre di avanzamento agli algoritmi di lavoro inseriti]
Katalpa,

Sì, CounterThreadè solo un esempio di classe di ossa nude WorkerThread. Se crei la tua classe figlio con un'implementazione più significativa di doWorkallora dovresti andare bene.
Kelly Thomas,

Le caratteristiche di CounterThread sono applicabili al mio obiettivo (notifiche dettagliate all'utente dello stato di avanzamento) - ma come sarebbe integrato con una nuova routine "doWork" di c.class? (anche - posizionamento saggio, 'doWork' all'interno del CounterThread giusto?)
Katalpa

L'implementazione CounterThread sopra a) inizializza il lavoro, b) inizializza la finestra di dialogo, c) esegue un ciclo principale, d) restituisce vero al completamento con esito positivo. Qualsiasi attività che può essere implementata con un ciclo dovrebbe semplicemente cadere sul posto. Un avvertimento che offrirò è che emettere i segnali per comunicare con il gestore comporta un certo sovraccarico, ovvero se chiamato con ogni iterazione del ciclo rapido potrebbe causare più latenza rispetto al lavoro effettivo.
Kelly Thomas,

Grazie per tutti i consigli. Potrebbe essere un problema per questo funzionare nella mia situazione. Allo stato attuale, doWork provoca un arresto in minidump in qgis. Un risultato di un carico troppo pesante o delle mie capacità di programmazione (per principianti)?
Katalpa,
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.