[GIS] QGIS plugin multithreading and loading memory layers

multithreadingpyqgispython-2.7qgis-plugins

I've been working on a small plugin recently and I wanted to add a progress bar to it, since there are tasks in this plugin that take some time to complete. I looked at this and this, but so far I've encountered a problem which I can't seem to solve.

Whenever I run the plugin without moving it to another thread, it runs perfectly fine, everything works (but the GUI is unresponsive for a short while). But when I move the worker (the python class that does the heavy lifting) to another thread, suddenly, I can't seem to add the QgsVectorLayer (which is a memory layer) to the iface.LegendInterface anymore. Everything else still works fine, but the layer just does not show up in the legend in QGIS. So far, this is my code, I've left out the specific methods and imported code, since that all works if I don't move it to a thread (so I figure that can't be the problem):

# import some modules used in the example
from PyQt4 import QtCore
from qgis.core import *
from qgis.gui import *
import utility_functions as uf

import traceback

class Worker(QtCore.QObject):

def __init__(self, iface, data, layer, plugindir, outfile):
    QtCore.QObject.__init__(self)
    self.iface = iface
    self.data = data
    self.layer = layer
    self.plugindir = plugindir
    self.outfile = outfile
    self.killed = False
    self.PathLayer = None

finished = QtCore.pyqtSignal(object)
error = QtCore.pyqtSignal(Exception, basestring)
progress = QtCore.pyqtSignal(float)
layerCreated = QtCore.pyqtSignal(object) # tried for debugging, does not work

def run(self):
    ret = None
    try:
        # Create edgelist
        edgelist = self.selectEdges()

        # Select features on layer and copy to new layer
        features = self.selectFeatures(edgelist, self.layer)

Everything works perfectly fine up until here. In the method 'createNewLayer', the memory layer should be (1) created using QgsVectorLayer, and (2) Loaded using QgsMapLayerRegistry.instance().addMapLayer(layer)

        self.createNewLayer(features)

        # Apply layer style
        if not self.PathLayer:
            lyr = uf.getLegendLayerByName(self.iface, 'Path')
        else:
            lyr = self.PathLayer
        self.applyStyle(lyr)

        if self.killed is False:
            self.progress.emit(len(self.data))
            ret = 1
    except Exception, e:
        # forward the exception upstream
        self.error.emit(e, traceback.format_exc())
    self.finished.emit(ret)

But somehow, the moving to another thread messes with the layer and it can't be added. What's even weirder: I can see the layer when I type into the QGIS python console: QgsMapLayerRegistry.instance().mapLayers(). So I'm guessing that the way QThreads work, messes up the QgsMapLayerRegistry OR the iface.LegendInterface.

I'm working on macOS Sierra, QGIS 2.14, but I've also tried the plugin on a W10 machine, QGIS 2.18, same problems.

Best Answer

The worker thread should not interfere with the GUI. Make the worker thread as isolated / independent as possible - it should only interact with the GUI thread using signals.

You can use a signal to send the layer back to the GUI thread and then add the layer to the legend from the GUI thread. You can declare the signal like this:

finished = QtCore.pyqtSignal(bool, object)

And then emit it when the layer is ready:

self.finished.emit(True, mymemoryLayer)

I have used this approach in one of my plugins, and it works fine.

Related Question