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.