Monday 30 April 2018

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


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?



Answer



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.


No comments:

Post a Comment

arcpy - Changing output name when exporting data driven pages to JPG?

Is there a way to save the output JPG, changing the output file name to the page name, instead of page number? I mean changing the script fo...