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:
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:
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:
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.
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
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.
The new function has now been created.
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:
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:
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:
and the layout output should now be:
Don't forget to ticket Render as HTML for the label.