Skip to content

Custom Expression Functions

API Note: Any QGIS APIs referenced will link out to the Python API Docs.

Custom expression functions are a powerful feature in QGIS that allow you to extend the expression engine to do exactly as you need without being limited to what as been added to the core expression functions in each release.

As we can use Python for these custom expressions we are able to pretty much do anything we need when the need comes.

Workshop examples

  • Custom functions we can use in styles
  • Custom functions we can use in layouts

Understanding Custom Expressions

Custom expression functions can be added inside the expression builder using the expression Function Editor tab. You can access this panel though any expression dialog:

Function Editor

Before we start we will break down the parts of a expression function just to understand how it works:

@qgsfunction(args='auto', group='Workshop')
def render_real_size(sizelabel, feature, parent):
    """
    Render the real size of the stormwater feature given the
    label with the size.
    """
    pass

Breaking down the function

@qgsfunction(args='auto', group='Workshop')

The qgsfunction marks the method as a custom QGIS expression function. args='auto' means the function will try and work out the number of required args based on the function signature. group='Workshop' the group in the function tree to include this new function in.

def render_real_size(sizelabel, feature, parent):

The normal Python function definition. feature and parent are always the last argument to be to passed in and are required.

  • feature is a QgsFeature object which contains the geometry object as well as the attribute data.
  • parent is a QgsExpression containing the current expression being parsed.
  • context (optional) a optional forth argument is a QgsExpressionContext. This can be used to get access to expression variables and other information that has been added for the expression.

Each argument before the three above is required to be included when the function is called when included in the expression. The case above sizelabel is the only argument so when we call the function inside the expression it will be called as: render_real_size("label").

    """
    Render the real size of the stormwater feature given the
    label with the size.
    """

The method doc string. Anything here will show up as help for the function in the expression builder. HTML supported.

    pass

The function body will update with logic we need.

Style Example

In this example we are going to use the Pits layer that contains a Label field with the following style data:

Function Editor

The real size of the pit is in a label as "wdithxheight". We can extract this out and render the real size of the pit on the map using a custom function and the geometry generator symbol type.

The final result should look like this:

Function Editor

We will be using the Pits layer for the following examples so feel free to disable the other layers.

Exercise: Change the style of the Pits layer

First we need to open the style dock for the Pits layer and change the symbol to Geometry generator. This symbol type allows us to render a different type of geometry from the layers data. For example we can render a polygon from a point layer.

Function Editor

Leave the geometry type as Polygon/MuliPolygon as that is what we want to render.

Click the insert expression button to insert a new expression

Function Editor

Exercise: Adding a new function

Switch to the Function editor and click the Add button on the bottom right to create a new file called workshop.

Function Editor

The new function has now been created.

Function Editor

Exercise: Updating function code.

Delete the new file contents and paste the following:

from qgis.core import *
from qgis.gui import *
from PyQt5.QtCore import QRectF

@qgsfunction(args='auto', group='Workshop')
def render_real_size(sizelabel, feature, parent):
    """
    Render the real size of the stormwater feature given the
    label with the size.
    """
    geom = feature.geometry()
    centroid = geom.centroid().asPoint()
    splitdata = sizelabel.split("x")
    if len(splitdata) == 2:
        width, height = int(splitdata[0]) / 1000, int(splitdata[1]) / 1000
        x, y = centroid.x()-width/2,centroid.y()-height/2
        rect = QRectF(x, y, width, height)
        return QgsGeometry.fromWkt(QgsRectangle(rect).asWktPolygon())

The above function takes a string in the format of widthxheight and will return a real geometry object of that size around the center of the object.

Exercise: Using the function

Press the Save and Load Functions to load it into the expression engine.

Note: You will need to do this each time you want to reload your changes.

Switch back to the expression tab and enter the following:

render_real_size("label")

In the output preview you will see <geometry:Polygon> which tells us the output is a polygon geometry type. Click OK on the expression editor and the map will refresh with the rendered pit size.

After you change the colours and add a label based on label property you will see the following:

Function Editor

Layout Example

Custom expression functions can be used anywhere that expressions can be used, which includes in layouts.
In this example we will create a layout which will show a summary of the layer values in the current map view.

The final result should look something this:

Function Editor

Enable the Trees layer in the project and open the layout called in Layout Example

This layout has a single map view and a label that will contain out feature count.

Exercise: Creating the new function.

Select the label and use Insert an Expression...

Switch to the function editor and create a new function file called layout and paste the following:

from qgis.core import *
from qgis.gui import *
from collections import Counter

@qgsfunction(args='auto', group='Workshop')
def feature_summary(mapextent, layername, field, feature, parent, context):
    maplayer = QgsProject.instance().mapLayersByName(layername)[0]
    data = [f[field] for f in maplayer.getFeatures(QgsFeatureRequest(mapextent.boundingBox()))]
    template = "<ul>{0}</ul>"
    items = []
    for name, count in Counter(data).items():
        items.append("<li>{}: {}</li>".format(count, name))
    return template.format("".join(items))

Click Save and Load Functions

Exercise: Using the new function

The above function will take the map extent, layername, and field as arguments. So we need to call it with those values.

We get the map extent of our map using the map_get function.

map_get(item_variables('MainMap'),'map_extent'))

However we still need to call our custom function.

So lets call it and include it as the first arg:

feature_summary( ( map_get(item_variables('MainMap'),'map_extent')), 'TREES', 'Type')

Exercise: Updating the rest of the label.

As layout labels allow for expressions inside [% %] blocks we can add extra headers in the raw label and even another row for a different layer.

Update the label text to be the following

Note: Note the use of HTML in this label.

<H1>Tree  Type Count</h1>

[% feature_summary( ( map_get(item_variables('MainMap'),'map_extent')), 'TREES', 'Type') %]

<H1>Pit Type Count</h1>

[% feature_summary( ( map_get(item_variables('MainMap'),'map_extent')), 'PITS', 'Type') %]

You label box should look like this:

Function Editor

and the layout output should now be:

Function Editor

Don't forget to ticket Render as HTML for the label.