3

I am trying to implement a custom renderer with Python in QGIS. I have put together a small toy example based on the code from the Creating Custom Renderers section of the PyQGIS Cookbook.

To get this code to work, I had to override the renderFeature function from the QgsFeatureRenderer class. I believe this should not be necessary, because I am essentially just re-implementing the original implementation. However, if I remove the implementation of renderFeature from my code and try to use the renderer in a version of QGIS that was built using CMAKE_BUILD_TYPE=Debug, the following assert failure is thrown:

Fatal: ASSERT failure in QgsFeatureRenderer::renderFeature: "renderFeature called in a different thread - use a cloned renderer instead", file ../src/core/symbology/qgsrenderer.cpp, line 122

Note that in a QGIS version without the Debug build type, the code works even with the renderFeature function removed. I assume the assertion is there for a reason, though, so ignoring this seems like the wrong thing to do.

What am I missing? Which renderer do I have to clone to avoid this error?

Steps to reproduce:

  1. Start a version of QGIS built with the Debug build type
  2. Paste the code below into the Python console and hit Enter
  3. Load a points layer
  4. In the layer styling panel, select the Test renderer
  5. QGIS should crash
from qgis.core import QgsWkbTypes, QgsSymbol, QgsFeatureRenderer
from qgis.gui import QgsRendererWidget
from PyQt5.QtGui import QColor


class TestRenderer(QgsFeatureRenderer):
    def __init__(self):
        QgsFeatureRenderer.__init__(self, "TestRenderer")
        self.sym = QgsSymbol.defaultSymbol(QgsWkbTypes.geometryType(QgsWkbTypes.Point))
        self.sym.setColor(QColor('blue'))

    def symbolForFeature(self, feature):
        return self.sym

    def startRender(self, context, vlayer):
        self.sym.startRender(context)

    ####
    # The following function is basically identical to the default implementation of renderFeature
    # However, if I don't implement it here, QGIS crashes and I get the following
    # error message:
    # "renderFeature called in a different thread - use a cloned renderer instead"
    ####
    #def renderFeature(self, feature, context, layer, selected, drawVertexMarker):
    #   s = self.symbolForFeature(feature)
    #   s.renderFeature(feature, context, layer, selected, drawVertexMarker)
    #   return True

    def stopRender(self, context):
       self.sym.stopRender(context)

    def usedAttributes(self, context):
        return []

    def clone(self):
        return TestRenderer()


class TestRendererWidget(QgsRendererWidget):
    def __init__(self, layer, style, renderer):
        QgsRendererWidget.__init__(self, layer, style)
        if renderer is None or renderer.type() != "TestRenderer":
            self.r = TestRenderer()
        else:
            self.r = renderer

    def renderer(self):
        return self.r


from qgis.core import QgsRendererAbstractMetadata, QgsApplication


class TestRendererMetadata(QgsRendererAbstractMetadata):
    def __init__(self):
        QgsRendererAbstractMetadata.__init__(self, "TestRenderer", "Test renderer")

    def createRenderer(self, element):
        return TestRenderer()

    def createRendererWidget(self, layer, style, renderer):
        return TestRendererWidget(layer, style, renderer)


QgsApplication.rendererRegistry().addRenderer(TestRendererMetadata())
  • You need to call the base class (super) implementations of startRender, stopRender, usedAttributes, in your overrides – ndawson Aug 12 at 9:28
  • @ndawson: Fantastic, that did the trick! Adding super().startRender(context) and super().stopRender(context) fixes the problem. Adding _ = super().usedAttributes(context) to the usedAttributes override causes a RecursionError: maximum recursion depth exceeded while calling a Python object, which I assume happens because usedAttributes is a pure virtual function that does not have an implementation in the base class. Leaving the override as is (simply return []) works fine though. Thanks for the quick help! – Jake Aug 12 at 10:28
1

As ndawson explained in his comment, the startRender and stopRender implementations of the base class need to be called in the overrides. The following code now works, without the need to reimplement the renderFeature function.

from qgis.core import QgsWkbTypes, QgsSymbol, QgsFeatureRenderer, QgsRendererAbstractMetadata, QgsApplication
from qgis.gui import QgsRendererWidget
from PyQt5.QtGui import QColor


class TestRenderer(QgsFeatureRenderer):
    def __init__(self):
        QgsFeatureRenderer.__init__(self, "TestRenderer")
        self.sym = QgsSymbol.defaultSymbol(QgsWkbTypes.geometryType(QgsWkbTypes.Point))
        self.sym.setColor(QColor('yellow'))

    def symbolForFeature(self, feature, xx):
        return self.sym

    def startRender(self, context, fields):
        # Call QgsFeatureRenderer's startRender function
        super().startRender(context, fields)

        self.sym.startRender(context, fields)

    def stopRender(self, context):
        # Call QgsFeatureRenderer's stopRender function
        super().stopRender(context)

        self.sym.stopRender(context)

    def usedAttributes(self, context):
        return []

    def clone(self):
        return TestRenderer()


class TestRendererWidget(QgsRendererWidget):
    def __init__(self, layer, style, renderer):
        QgsRendererWidget.__init__(self, layer, style)
        if renderer is None or renderer.type() != "TestRenderer":
            self.r = TestRenderer()
        else:
            self.r = renderer

    def renderer(self):
        return self.r


class TestRendererMetadata(QgsRendererAbstractMetadata):
    def __init__(self):
        QgsRendererAbstractMetadata.__init__(self, "TestRenderer", "Test renderer")

    def createRenderer(self, element):
        return TestRenderer()

    def createRendererWidget(self, layer, style, renderer):
        return TestRendererWidget(layer, style, renderer)


QgsApplication.rendererRegistry().addRenderer(TestRendererMetadata())

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.