10

I use the folowing line to inform the user about the status:

iface.mainWindow().statusBar().showMessage("Status:" + str(i))

The plugin takes about 2 min to run on my data set but windows detects it as "not responding" and stop showing the status updates. For a new user this is not so good since it looks like the program have crashed.

Is there any work around so the user is not left in the dark regarding the status of the plugin?

13

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.

  • Thank you this was exactly what I was looking for and it was very helpful! I'm used to threads i C# but did not think of it in python. – Johan Holtby Jul 2 '13 at 13:40
  • Yeah this is correct way. – Nathan W Jul 2 '13 at 22:34
  • 1
    Should there be a "self." in front of layer in "features = layer.getFeatures()"? -> "features = self.layer.getFeatures()" – Håvard Tveite Sep 5 '14 at 15:47
  • @HåvardTveite You are correct. I've fixed the code in the answer. – Snorfalorpagus Sep 7 '14 at 10:59
  • I'm trying to follow this pattern for a processing script that I'm writing, and I'm having trouble getting it to work. I tried copying this example into a script file, added the necessary import statements, and changed worker.progress.connect(self.ui.progressBar) to something else, but every time I run it qgis-bin is crashing. I have no experience debugging python code or qgis. All I'm getting is Access violation reading location 0x0000000000000008 so it seems like something is null. Is there some set up code that is missing to be able to use this in a processing script? – TJ Rockefeller Jan 26 '17 at 22:08
4

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....

  • :) It looks like a nice dirty way to do it. Some times style is not necessary. For this time(the first in pyqt) I think I will go with the correct way since I'm used to that in C#. – Johan Holtby Jul 2 '13 at 13:44
  • 2
    It's not a dirty way, it was the old way of doing it. – Nathan W Jul 2 '13 at 22:34
2

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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