4

I've got a script which adds a legend to a print layout. It works perfectly as a script but when I try to implement this in the processAlgorithm of a QGIS plugin, it doesn't work.

If it is helpful, the full code for the plugin is here: https://github.com/epurpur/eco-valuator/blob/CreateAndExportPrintLayout/create_print_layout_and_export_map.py

I'm pretty sure the problem is with the following line:

legend.model().setRootGroup(root)

Here is my script (outside the plugin):

"""This creates a new print layout"""
project = QgsProject.instance()
manager = project.layoutManager()
layout = QgsPrintLayout(project)
layoutName = "PrintLayout"

layout = QgsPrintLayout(project)
layout.initializeDefaults()
layout.setName(layoutName)
manager.addLayout(layout)


"""Gathers active layers to add to legend"""
checked_layers = [layer.name() for layer in QgsProject().instance().layerTreeRoot().children() if layer.isVisible()]
layersToAdd = [layer for layer in QgsProject().instance().mapLayers().values() if layer.name() in checked_layers]
root = QgsLayerTree()
for layer in layersToAdd:
    print(f"Adding {layer.name()} to legend")
    root.addLayer(layer)

"""This adds a legend item to the Print Layout"""
legend = QgsLayoutItemLegend(layout)
legend.model().setRootGroup(root)
layout.addLayoutItem(legend)
legend.attemptMove(QgsLayoutPoint(246, 5, QgsUnitTypes.LayoutMillimeters))

This adds a nice legend, with just the active layers: enter image description here

When implemented in the processAlgorithm of my plugin, I either get a map with no legend or QGIS crashes. So something is broken. To help troubleshoot, I added a print statement in my for loop when adding layers to my "root" object, and this works correctly. The next time I interact with "root" is in this line (as stated above):

legend.model().setRootGroup(root)

To spare you another long code block, my processAlgorithm is EXACTLY the same code except it is all nested within this:

 def processAlgorithm(self, parameters, context, feedback):
    """This actually does the processing for creating the print layout and exporting as .pdf"""


    log = feedback.setProgressText

    rest of code exactly as it was above

If I comment out all lines related to gathering just the active layers:

between """Gather active layers...""" and root.addLayer(layer), and the line: legend.model().setRootGroup(root)

and then run my plugin, I get a legend (except it adds all layers and looks bad): enter image description here

I asked a similar question on StackOverflow recently as I was having trouble with adding a map item to my print layout and Nyall Dawson was nice enough to provide me this function to include in my code:

def flags(self):
    return super().flags() | QgsProcessingAlgorithm.FlagNoThreading

and it worked! I was able to add a map object. But to be honest I don't know what this does, or if this issue is related.

  • 1
    Did you also add the no threading flag to this algorithm? – ndawson Feb 22 at 8:56
  • @ndawson This example with the legend and my previous thread with the map are both parts in the same processAlgorithm. I split them hoping to simplify things on StackOverflow. I think I added the no threading flag correctly. I copy and pasted your code in with the other class methods like initAlgorithm, processAlgorithm, displayName, etc. If it is helpful, the full code is here: github.com/epurpur/eco-valuator/blob/CreateAndExportPrintLayout/… – Erich Purpur Feb 22 at 16:18
3
+50

I was able to reproduce your problem and it seems to be related to setting a new root group to the existing legend model (I won't pretend to know why it works fine in the python console but not in a processing script)!

I have modified a few parts of your code and it is successfully working for me in a processing script.

The main changes are:

  1. It seems that the correct way to access the project and layer tree objects from within a processing script is by calling context.project().layerTreeRoot()

  2. Instead of constructing a new layer tree and adding the checked layers to it, first add the legend object to the layout, then get references to the existing model and root group and remove the layers you don't want i.e. the unchecked layers (more or less the reverse logic of the original approach).

So your processAlgorithm function would now look like this (minimal example):

def processAlgorithm(self, parameters, context, feedback):

    layout_name = self.parameterAsString(parameters, self.INPUT_TITLE, context)
    project = context.project()
    manager = project.layoutManager()
    layout = QgsPrintLayout(project)
    layout.initializeDefaults()
    layout.setName(layout_name)
    manager.addLayout(layout)

    #Gather visible layers in project layer tree and create a list of the map layer objects
    #which are not checked which we will subsequently remove from the legend model
    tree_layers = project.layerTreeRoot().children()
    checked_layers = [layer.name() for layer in tree_layers if layer.isVisible()]
    layers_to_remove = [layer for layer in project.mapLayers().values() if layer.name() not in checked_layers]

    #This adds a legend item to the Print Layout
    legend = QgsLayoutItemLegend(layout)
    layout.addLayoutItem(legend)
    legend.attemptMove(QgsLayoutPoint(246, 5, QgsUnitTypes.LayoutMillimeters))        
    #Get reference to existing legend model and root group then remove the unchecked layers
    legend.setAutoUpdateModel(False) #this line is important!! without it the unchecked layers
    #will be removed not only from the layout legend, but also from the table of contents panel and your project!!
    m = legend.model()
    g = m.rootGroup()
    for l in layers_to_remove:
        g.removeLayer(l)
    legend.adjustBoxSize()

    return {}

Important note: Without the line: legend.setAutoUpdateModel(False) not only will the unchecked layers be removed from the layout legend, they will also disappear from the table of contents panel and your project!

This was the resulting layout (all unchecked layers have been removed from the legend):

enter image description here

And no crashes!!

By the way, what you have in your flags() function is fine (and is definitely required for this algorithm).

  • 1
    Thank you so much for your help again, Ben. I really appreciate it. You might recognize parts of my code as your own from a couple weeks ago. One last question. In the lines: m = legend.model(), g = m.rootGroup(), I don't understand what the difference is between the legend model and legend root group. Is there a reference to this in the documentation? – Erich Purpur Feb 25 at 13:49
  • 2
    No problem Erich, glad it's now working. To try and answer your question: If you have an instance of QgsLayoutItemLegend, calling .model() returns the QgsLegendModel object belonging to that legend. The documentation states that QgsLegendModel is based on the LayerTreeModel class which has a method rootGroup(). This returns the QgsLayerTree object that contains the layer objects shown in the legend. Doc links: qgis.org/api/… | qgis.org/api/classQgsLegendModel.html – Ben W Feb 25 at 23:40
  • 2
    Doc links cont: qgis.org/api/… | qgis.org/api/classQgsLayerTree.html – Ben W Feb 25 at 23:41
  • Another method might be just using the g.clear() function, and then add layers we need. – natsuapo Jun 12 at 17:02

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.