4

I am creating a layout with two maps, several boxes, picture, ... The problem turn up when I add a legend where I modified the layers. For that I create a list with layers in my map, after that, I remove the OpenStreetMap layer and I add the rest to a new list. Then, I create a new layer tree and add the new layers. Everything is ok, but when I use legend.model().setRootGroup(root) and show the layout (Project >> Layout >> my_layout), an unexpected problem appears, and QGIS blow up.

Does anyone know what is my error?

Here is the main part of my code. The rest part works correctly, but the legend don't.

project = QgsProject.instance()
manager = project.layoutManager()

#Funciones varias
##Función para añadir boxes al layout
def addItemBox(layoutName, name, x, y, width, height):
    layout = manager.layoutByName(layoutName) #selecciona el layout a trabajar
    box = QgsLayoutItemShape(layout)
    box.setShapeType(QgsLayoutItemShape.Rectangle)
    box.setId(name)
    layout.addLayoutItem(box)
    box.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
    box.attemptResize(QgsLayoutSize(width, height, QgsUnitTypes.LayoutMillimeters))
    print(f'-> {name} was added')

##Función para añadir imágenes al layout
def addItemPicture(layoutName, name, path, x, y, width, height):
    layout = manager.layoutByName(layoutName) #selecciona el layout a trabajar
    picture = QgsLayoutItemPicture(layout)
    #### ...(r'...path a la imagen...') ####
    picture.setPicturePath(path)
    picture.setId(name)
    layout.addLayoutItem(picture)
    picture.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
    picture.attemptResize(QgsLayoutSize(width, height, QgsUnitTypes.LayoutMillimeters))
    print(f'-> {name} was added')

##Función para añadir etiquetas al layout
def addItemLabel(layoutName, name, text, x, y, width, height):
    layout = manager.layoutByName(layoutName) #selecciona el layout a trabajar
    label = QgsLayoutItemLabel(layout)
    label.setId(name)
    label.setText(text)
    layout.addLayoutItem(label)
    label.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
    label.attemptResize(QgsLayoutSize(width, height, QgsUnitTypes.LayoutMillimeters))
    print(f'-> {name} was added')

##Función de exportación del layout configurado como atlas previamente
def exportAtlas(layoutName):
    layout = manager.layoutByName(layoutName) #selecciona el layout a trabajar
    print(layout.name())
    exporter = QgsLayoutExporter(layout)
    exporter.exportToPdf(layout.atlas(),r'C:\Users\alvaro.garcia.daroca\Documents\guxhagen.pdf',QgsLayoutExporter.PdfExportSettings())
    print('\nExport End')

##Función que añade la escala al mapa
def addScaleBar(layoutName, map):
    layout = manager.layoutByName(layoutName) #selecciona el layout a trabajar
    scalebar = QgsLayoutItemScaleBar(layout)
    scalebar.setLinkedMap(map)
    scalebar.setId('escala mapa')
    scalebar.setStyle('Single Box')
    scalebar.setUnitLabel('m')
    scalebar.setSegmentSizeMode(1)
    scalebar.setMaximumBarWidth(100)
    scalebar.applyDefaultSize()
    layout.addItem(scalebar)
    scalebar.attemptMove(QgsLayoutPoint(15, 230, QgsUnitTypes.LayoutMillimeters))
    scalebar.attemptResize(QgsLayoutSize(30, 15, QgsUnitTypes.LayoutMillimeters))

##Función mapa princiapal
def addMap(layoutName, id, atlas, x, y, w, h):
    layout = manager.layoutByName(layoutName) #selecciona el layout a trabajar
    map = QgsLayoutItemMap(layout)
    map.setRect(20, 20, 20, 20)
    canvas = iface.mapCanvas()
    map.setExtent(canvas.extent())
    map.setId(id) #set name id
    layout.addLayoutItem(map)
    map.setAtlasDriven(atlas) #set control by atlas (True or False)
    map.setFrameEnabled(True)
    map.setFrameStrokeWidth(QgsLayoutMeasurement(0.3))
    map.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
    map.attemptResize(QgsLayoutSize(w, h, QgsUnitTypes.LayoutMillimeters))
    #only add scale to the main map
    if id == 'mapa principal':
        addScaleBar(layoutName, map)
    print(f'-> {id} was added')

##Función que genera la leyenda
def addLegend(layoutName):
    layout = manager.layoutByName(layoutName) #selecciona el layout a trabajar
    #create legend
    checked_layers = [layer.name() for layer in project.layerTreeRoot().children() if layer.isVisible()]
    #remove OpenStreetMap layer from Legend
    checked_layers.remove('OpenStreetMap')
    #print('Legend:', checked_layers)
    layersToAdd = [layer for layer in project.mapLayers().values() if layer.name() in checked_layers]
    root = QgsLayerTree()
    for layer in layersToAdd:
        root.addLayer(layer)
    #add legend and format it
    legend = QgsLayoutItemLegend(layout)

    ##### The problem is here I think #####
    legend.model().setRootGroup(root)
    
    legend.setId('Legend')
    layout.addLayoutItem(legend)
    legend.setFrameEnabled(True)
    legend.setFrameStrokeWidth(QgsLayoutMeasurement(0.3))
    legend.setTitle('Legend')
    legend.attemptMove(QgsLayoutPoint(308, 115, QgsUnitTypes.LayoutMillimeters))
    legend.attemptResize(QgsLayoutSize(104, 40, QgsUnitTypes.LayoutMillimeters))
    print(f'-> legend: ({checked_layers}) was added')

##Función para crear layouts
def createLayout(layoutName):
    layout = QgsPrintLayout(project)
    #remove duplicated layouts
    layout_list = manager.printLayouts() #return a list of layouts
    for layout in layout_list:
        if layout.name() == layoutName:
            manager.removeLayout(layout)

    layout = QgsPrintLayout(project)
    layout.initializeDefaults() #initialize void layout
    #modify page format
    pc = layout.pageCollection()
    pc.pages()[0].setPageSize('A3', QgsLayoutItemPage.Orientation.Landscape)
    layout.setName(layoutName)
    manager.addLayout(layout)
    #add items to the layout
    addItemBox(layoutName, 'margen', 5, 5, 410, 287)
    addItemBox(layoutName, 'cajetín general', 8, 249, 404, 40)
    addItemBox(layoutName, 'cajetín ubicación', 48, 252, 168, 34)
    addItemBox(layoutName, 'cajetín fecha', 219, 252, 34, 34)
    addItemBox(layoutName, 'cajetín descripción', 256, 252, 110, 34)
    addMap(layoutName, 'mapa principal', True, 8, 8, 297, 238)
    addMap(layoutName, 'mapa secundario', False, 308, 8, 104, 104)
    addItemPicture(layoutName, 'north arrow',r'C:\Users\alvaro.garcia.daroca\Documents\Qgis - Daroca\Telefónica - pruebas\North_Arrow.svg', 15, 15, 20, 20)
    addItemPicture(layoutName, 'logo UGG', r'C:\Users\alvaro.garcia.daroca\Documents\Qgis - Daroca\Telefónica - pruebas\UGG_logo.png', 369, 252, 40, 40)
    addItemPicture(layoutName, 'QR UGG',r'C:\Users\alvaro.garcia.daroca\Documents\Qgis - Daroca\Telefónica - pruebas\QR_UGG.png', 11, 252, 34, 34)

    print('\n-> **Layout was created')

#call function
createLayout('layout_atlas')
´´´

1 Answer 1

9

I tested your code by running your addLegend() method in the Python console and I also experienced a crash. Interestingly, when I removed the logic from the function and simply ran the lines outside of a function definition there was no crash. As soon I moved the logic inside a function, it crashed.

Try the below for a slightly different approach by removing unchecked layers from the existing legend model layer tree rather than creating a new QgsLayerTree object and setting it to the existing model. This worked for me inside a function without crashing QGIS.

def addLegend(layoutName):
#    project = QgsProject.instance()
#    manager = project.layoutManager()
    layout = project.layoutManager().layoutByName(layoutName)
    
    checked_lyrs = [l.name() for l in QgsProject().instance().layerTreeRoot().children() if l.isVisible()]
    checked_lyrs.remove('OpenStreetMap')
    lyrsToRemove = [l for l in project.mapLayers().values() if l.name() not in checked_lyrs]
    legend = QgsLayoutItemLegend(layout)
    #setAutoUpdateModel to false otherwise main layer tree view will also be modified
    legend.setAutoUpdateModel(False)
    root = legend.model().rootGroup()
    for l in lyrsToRemove:
        root.removeLayer(l)
    legend.adjustBoxSize()
    legend.setId('Legend')
    layout.addLayoutItem(legend)
    legend.setFrameEnabled(True)
    legend.setFrameStrokeWidth(QgsLayoutMeasurement(0.3))
    legend.setTitle('Legend')
    legend.attemptMove(QgsLayoutPoint(308, 115, QgsUnitTypes.LayoutMillimeters))
    legend.attemptResize(QgsLayoutSize(104, 40, QgsUnitTypes.LayoutMillimeters))

addLegend('Test Layout')
2
  • 1
    I only needed a new point of view, remove instead of create just like that. Thank you so much, I was going crazy with this problem. Commented Oct 25, 2021 at 13:06
  • @Álvaro García Daroca, you're very welcome. It's strange indeed, tbh I don't get why your original code worked line-by-line, but crashed when run from a function! I think I may have encountered a similar problem before though as I had saved the snippet of code which worked. Anyway, glad your problem is solved :-)
    – Ben W
    Commented Oct 25, 2021 at 13:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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