How do I prevent Qgis from being detected as "not responding" when running a heavy plugin?

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.


Your only true way of doing this is by multithreading.

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

Some extra reading http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Note Some people don't like inheriting from QThread, and apparently this isn't the "correct" way to do it, but it does work so....


As this question is relatively old, it deserves an update. With QGIS 3 there is approach with QgsTask.fromFunction(), QgsProcessingAlgRunnerTask() and QgsApplication.taskManager().addTask().

More about it for example at Using Threads in PyQGIS3 BY MARCO BERNASOCCHI

Tags:

Python

Qgis