As Nathan W points out, the way to do this is with multithreading, but subclassing QThread isn't best practice. See here: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/
See below an example of how to create a QObject
, then move it to a QThread
(i.e. the "correct" way to do it). This example calculates the total area of all the features in a vector layer (using the new QGIS 2.0 API!).
First, we create the "worker" object that will do the heavy lifting for us:
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)
To use the worker we need to initalise it with a vector layer, move it to the thread, connect some signals, then start it. It's probably best to look at the blog linked above to understand what's going on here.
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()
This example illustrates a few key points:
- Everything inside the
run()
method of the worker is inside a try-except statement. It's difficult to recover when your code crashes inside a thread. It emits the traceback via the error signal, which I usually connect to the QgsMessageLog
.
- The finished signal tells the connected method if the process completed successfully, as well as the result.
- The progress signal is only called when the percentage complete changes, rather than once for every feature. This prevents too many calls to update the progress bar slowing down the worker process, which would defeat the whole point of running the worker in another thread: to separate the calculation from the user interface.
- The worker implements a
kill()
method, which allows the function to terminate gracefully. Don't try and use the terminate()
method in QThread
- bad things could happen!
Be sure to keep track of your thread
and worker
objects somewhere in your plugin structure. Qt gets angry if you don't. The easiest way to do this is to store them in your dialog when you create them, e.g.:
thread = self.thread = QtCore.QThread()
worker = self.worker = Worker(layer)
Or you can let Qt take ownership of the QThread:
thread = QtCore.QThread(self)
It took me a long time to dig up all the tutorials in order to put this template together, but since then I've been reusing it all over the place.