12

I have been developing some batch processing tools as python plugins for QGIS 1.8.

I have found that while my tools are running the GUI becomes non-responsive.

The general wisdom is that the work should be done on a worker thread, with the status/completion information passed back to the GUI as signals.

I have read through the riverbank docs, and studied the source of doGeometry.py (a working implementation from ftools).

Using these sources I have tried to build a simple implementation in order to explore this functionality before making changes to an established code base.

The overall structure is an entry in the plugins menu, that lanuches a dialog with start and stop buttons. The buttons control a thread that counts to 100, sending a signal back to the GUI for each number. The GUI receives each signal and sends a string containing the number both the message log, and the window title.

The code of this implementation is here:

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

Unfortunately it's not quiet working as I hoped:

  • The window title is updating "live" with the counter but if I click on the dialog, it is non-responsive.
  • The message log is inactive until the counter ends, then presents all of the messages at once. These messages are tagged with a time stamp by QgsMessageLog and these time stamps indicate they were received "live" with the counter i.e. they are not being queued by either the worker thread, or the dialog.
  • The order of messages in the log (exerpt follows) indicates that startButtonHandler completes execution before the worker thread comences work i.e. the thread behaves as a thread.

    end: startButtonHandler
    Emit: starting work
    Emit: 0
    ...
    Emit: 99
    Emit: finshed work
    
  • It seems the worker thread just isn't sharing any resources with the GUI thread. There are a couple of commented out lines at the end of the above source where I tried calling msleep() and yieldCurrentThread(), but neither seemed to help.

Is anyone with any experience with this able to spot my error? I'm hoping it is a simple but fundamental mistake that is easy to correct once it is identified.

1
  • Is it normal that that the stop button can't be clicked ? The main goal of responsive GUI is to cancel the process if it's too long. I try to modify your script but I can't get the button working properly. How do you abort your thread ?
    – etrimaille
    Commented Sep 20, 2014 at 21:09

1 Answer 1

6

So I had another look at this problem. I started from scratch and had success, then went back to look at the code above and still can't fix it.

In the interests of providing a working example for anyone researching this subject I will provide functional code here:

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

The structure of this sample is a ThreadManagerDialog class that than can be assigned a WorkerThread (or subclass). When the dialog's run method is called it will in turn call the doWork method on the worker. The result is that any code in doWork will run in a seperate thread, leaving the GUI free to respond to user input.

In this sample an instance of CounterThread is assigned as the worker and a couple of progress bars will be kept busy for a minute or so.

Note: this is formatted so that it is ready to paste into the python console. The last three lines will need to be removed before saving to a .py file.

6
  • This is a great plug and play example! I am curious as to the best position in this code for implementing our own working algorythmn. Would such need to be emplaced in the class WorkerThread, or rather in the class CounterThread, def doWork? [Asked in the interest of connecting these progress bars to the inserted worker algorithm(s)]
    – Katalpa
    Commented Sep 16, 2014 at 11:47
  • Yeah, CounterThread is just a bare bones example child class of WorkerThread. If you create your own child class with a more meaningful implementation of doWork then you should be fine. Commented Sep 16, 2014 at 11:59
  • The characteristics of the CounterThread are applicable to my goal (detailed notifications to user of progress) - but how would such be integrated with a new c.class 'doWork' routine? (also - placement wise, 'doWork' within the CounterThread right?)
    – Katalpa
    Commented Sep 16, 2014 at 12:53
  • The CounterThread implementation above a) initializes the job, b) initializes the dialog, c) performs a core loop, d) returns true on successful completion. Any task that can be implemented with a loop should just drop in place. One warning I will offer is that emitting the signals to communicate with the manager comes with some overhead i.e. if called with every iteration of rapid loop it may cause more latency than the actual job. Commented Sep 16, 2014 at 13:18
  • Thanks for all the advice. Might be troublesome for this to work in my situation. At current, the doWork causes a minidump crash in qgis. A result of too heavy a load, or my (novice) programing skills?
    – Katalpa
    Commented Sep 16, 2014 at 13:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.