10

I have some layers with very complex data-defined symbols. If I use a legend in the print composer these symbols are not drawn correctly.

Is there a way to change the legend-symbols with pyqgis, so I can use custom PNG or SVG images instead of the default legend symbol for these layers?

I know how to add a Push Button to the Print composer and how to connect it with a function. I want to add a button to the Legend-Settings to allow me to replace the automatically generated legend icon with a custom image. So what I still need is the information of how I can access the legend-symbols with pyqgis/pyqt, and how to replace them with a QImage on a QLabel or something like that?

Very basic mockup of the button used to change the symbol:

enter image description here

Automatically generated Legend:

enter image description here

Legend with custom legend symbol:

enter image description here

I already found out how to access the items of the legend in the print composer but not yet how to access the symbol itself:

import qgis
from PyQt4.QtCore import *
from PyQt4.QtGui import *

activeComposer = iface.activeComposers()

for item in activeComposer:
    if item.composerWindow().windowTitle()=='test':
        for i in item.items():
            if isinstance(i,QgsComposerLegend):
                #print i
                #print i.model()
                legend = i
                for i in xrange(legend.modelV2().rowCount()):
                    posteleg=legend.modelV2().index(i, 0)
                    print posteleg.data()

Edit 1:

I have also found out how to Access QIcon-Objects of the legend-tree but cannot swap them yet:

def run(self):

        activeComposer = self.iface.activeComposers()
        #print(self.resolve('icon.png'))
        for item in activeComposer:
            if item.composerWindow().windowTitle()=='test':
                for i in item.items():
                    if isinstance(i,QgsComposerLegend):
                        legend = i

                        layerIcon = QIcon(os.path.join(os.path.dirname(__file__), "icon.png"))

                        for i in xrange(legend.modelV2().rowCount()):
                            posteleg=legend.modelV2().index(i, 0)
                            posteleg.model().iconGroup().swap(layerIcon)
                            print posteleg.data()

Here is one real life example where you can see a symbology combined of many symbol layers: enter image description here This will end up in the legend like this: enter image description here

As I need the proper symbol in the legend I would like to make a screenshot of my symbol, crop it and use it as an image in my legend.

I know I could just overlay a seperate image on top of my legend which covers the automatically generated symbol but I would like to have a "cleaner" solution that allows me to replace the symbols in the legend with custom images.

Edit 2:

In the meantime I have found another way to gain access to the legend entries:

from qgis.core import QgsLegendRenderer, QgsComposerLegendStyle

compDict = {}
for comp in iface.activeComposers():
    # workaround to get name: read it from window title
    compDict[comp.composerWindow().windowTitle()] = comp.composition()
if "mycomposername" in compDict:
    itemLlegend = compDict["mycomposername"].getComposerItemById("mylegend_id")
    if itemLlegend:
        print itemLlegend

tree_layer_layer =  itemLlegend.modelV2().rootGroup().children()
for item in tree_layer_layer:
        if item.layerName()=="MyLayername":
            print "match"
            QgsLegendRenderer.setNodeLegendStyle(item, QgsComposerLegendStyle.Group)

This allows me to access the QgsLayerTreeLayer objects and I can switch the legend Style (Group,Subgroup,Hidden). But I still have no idea how to access the legend-symbol. Any ideas?

1
  • 2
    When I run into this with QGIS, I will typically create additional layers that have the symbology I want to show in the legend (usually simple duplicates of existing layers - not new data sources). Then, in the composer I set up the map window and lock the layers. After the layers are locked, I turn on the "fake" layers and can add them to a legend. It's purely a workaround, and not in PyQGIS, but perhaps there is some way to emulate the "fake" layers for what you need? Commented Feb 26, 2017 at 3:57

3 Answers 3

10
+50

Since this topic covers a lot of arguments, I will only focus on the SVG symbol layers, hoping that I have understood well what are you looking for (I didn't realize the length of the answer while writing, so I'm sorry for that but I hope it will add more clearness, though).


Context

1) Symbol layer classes

The following symbol layer classes are available for SVG format:

  • QgsSvgMarkerSymbolLayerV2, which displays a point geometry using a specified SVG image (it works for point layers);
  • QgsSVGFillSymbolLayer, which draws the interior of a polygon geometry using a repeated SVG image (it works for polygon layers).

A common approach for creating a symbol layer is initialising it with a dictionary of properties.

You may initialize a new symbol layer and see its default properties in this way:

symbol_layer = QgsSvgMarkerSymbolLayerV2()
for k,v in symbol_layer.properties().iteritems():
    print k, ':', v

You will obtain all the properties that are stored in it:

outline_width : 0.2
outline_color : 0,0,0,255
angle : 0
name : crosses/Star1.svg
scale_method : diameter
color : 0,0,0,255
size_unit : MM
horizontal_anchor_point : 1
size_map_unit_scale : 0,0,0,0,0,0
outline_width_unit : MM
offset : 0,0
offset_map_unit_scale : 0,0,0,0,0,0
outline_width_map_unit_scale : 0,0,0,0,0,0
size : 4
vertical_anchor_point : 1
offset_unit : MM

If you want to edit the properties, you may use methods, which are callable from the help of the class (e.g. run help(QgsSvgMarkerSymbolLayerV2) in the Python Console). You'll see later an example of how using methods.

For the sake of completeness, you may also initialize a symbol layer with a dictionary of properties (for example, see here), but I sincerely prefer the first approach and I will use it.

2) Creating a renderer

To use the symbol layer once you have created it (and eventually edited), you need to create an appropriate renderer and then assign that renderer to your map layer.

To access the existing renderer of a layer:

renderer = layer.rendererV2()

To get a list of the available renderer types, use:

renderer_types = QgsRendererV2Registry().renderersList()

For your case, we should deal with a Categorized symbol renderer. As I said before, you need to create a renderer and then assign it to the layer:

# define the lookup: value -> (color, label)
landuses = {'Agriculture': ('#d3a151', 'Agriculture'), 'Natural': ('#175dcd', 'Natural'),}

# create a category for each item in landuses
categories = []
for landuse_name, (color, label) in landuses.items():
    symbol = QgsSymbolV2.defaultSymbol(layer.geometryType())
    symbol.setColor(QColor(color))
    category = QgsRendererCategoryV2(landuse_name, symbol, label)
    categories.append(category)

# create the renderer and assign it to the layer
expression = 'landuse' # field name
renderer = QgsCategorizedSymbolRendererV2(expression, categories) # categorized symbol renderer
layer.setRendererV2(renderer) # assign the renderer to the layer

3) Changing symbol layer

The several symbols of the Categorized symbol renderer are callable through symbols() (it returns a list):

for symb in renderer.symbols():
    print symb

<qgis._core.QgsMarkerSymbolV2 object at 0x0E1FF760>
<qgis._core.QgsMarkerSymbolV2 object at 0x0E1FF7B0>

If you want to replace a given symbol layer within the symbol defined before, you only need to know its index and then tell it to the renderer in this way:

renderer.symbols()[0].changeSymbolLayer(0, new_symbol)

where [0] indicates the first item of the categorized group.

Solution

Finally, let's apply what we just have learned!

Assuming to work on this polygon layer, which stores the land uses we have defined before:

enter image description here

If you want to change the default pattern for the agricultural land uses (they have the position No. 1 in the 'landuse' group) with a specific SVG image, you may run this code (read here for knowing how to add a custom SVG path):

import qgis
from PyQt4.QtCore import *
from PyQt4.QtGui import *

# define the lookup: value : (color, label)
landuses = {'Agriculture': ('#d3a151', 'Agriculture'), 'Natural': ('#175dcd', 'Natural'),}

# create a category for each item in landuses
categories = []
for landuse_name, (color, label) in landuses.items():
    symbol = QgsSymbolV2.defaultSymbol(layer.geometryType())
    symbol.setColor(QColor(color))
    category = QgsRendererCategoryV2(landuse_name, symbol, label)
    categories.append(category)

# create the renderer and assign it to the layer
expression = 'landuse' # field name
renderer = QgsCategorizedSymbolRendererV2(expression, categories)

activeComposer = iface.activeComposers()
for item in activeComposer:
    if item.composerWindow().windowTitle()=='test':
        for i in item.items():
            if isinstance(i,QgsComposerLegend):
                legend = i
                for k in xrange(legend.modelV2().rowCount()):
                    posteleg=legend.modelV2().index(k, 0)
                    layer = QgsMapLayerRegistry.instance().mapLayersByName( posteleg.data() )[0]
                    if k == 0: # k is the position of the layer of interest in Legend (NOT in the Layers Panel)
                        svg_location = 'C:/path_to_svg/smile.svg'
                        new_symbol = QgsSVGFillSymbolLayer()
                        new_symbol.setSvgFilePath(svg_location)
                        new_symbol.setPatternWidth(7.0)
                        #... If you want to set additional parameters, type help(QgsSVGFillSymbolLayer) in the Python Console
                        renderer.symbols()[1].changeSymbolLayer(0, new_symbol)
                    layer.setRendererV2(renderer)
                    layer.triggerRepaint()

This will be the result:

enter image description here

The code above is very rough but, since I didn't know if you wanted a specific solution for your example or a more general explanation, I preferred to focus the attention on the available tools instead of refining the code itself (I'm sure you are able to slightly edit it according to your specific needs!).

3
  • 1
    Thanks for your extensive answer but I am looking for a solution to change the symbol (only) in the composer legend not for the layer itself. I'll update my question above in a second with one more example
    – Thomas B
    Commented Feb 27, 2017 at 7:38
  • @markgraeflerland I'm sure nobody thought about the issue explained in your (practically new) edited question. You never specified that the image in the legend won't reproduce what you see in the layer (it seemed you wanted to replace an item from the legend with an image, as I have done). Sincerely, in my opinion, your original question was very misleading and my post actually tried to give an answer to it. I'm sorry for the wasted time, but I won't delete the answer since it could be of interest for someone else when googling. Good luck with your research!
    – mgri
    Commented Feb 27, 2017 at 8:09
  • 3
    In the first sentence of my question I wrote "If i use a legend in the print composer these symbols are not drawn correctly" so I don't think that I didn't specified that the image in the legend won't reproduce what I see in the layer.
    – Thomas B
    Commented Feb 27, 2017 at 15:57
5

Alternatively, and without Python coding, I solved this by creating a new layer group dedicated to the legend creation, where I can put anything I like, with the sizes and colors I want. Like this in the print composer I just deleted from the legend items the actual data layers and kept only the legend layer group.

It is specifically practical when in need to make a legend which can represent cases not happening in the actual printed map.

EDIT : and about your second edit, with the composed symbols not displaying properly, do you have some variables defining your symbols, like the "C" ou "G" is in fact relative to a field, or the orientation ? If it is the case, QGIS cannot guess what you want to be displayed, so it goes by displaying everything without any value for these parameters. A workaround can be to symply save the symbol with some fixed values in place of the variables. This way allowed me to replace this default displaying legend item : enter image description here by this one fitting my need enter image description here

0

This workaround works when you don't have to compose a lot of maps because each time you have to manually configure your legend. For me this was eventually too time-consuming and error prone.

Another (quite complicated but more automated) way to replace the icons in the legend is to go to "layer properties / legend". There you check "text on symbols". You can then apply an SVG as a background image which overlays the symbol in the legend (when it has got the right size). Make sure you fill in a space character under "text on symbols" otherwise the SVG will not be shown at all. It is complicated but works for me until a proper solution comes up in QGIS.

What also is a big downside is that when the style of the layer is copied and applied to another layer then the "legend properties" are not passed on for some reason.

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.