How to build cascading form controls in QGis 2.4🔗

Posted by Médéric Ribreux 🗓 In blog/ Qgis/

Introduction

Since QGis 1.8 (and even before), you can use QGis to build forms to deal with attributes when you create a new geographic object. Over the different versions of QGis, form building get plenty of new features like a 1,1 value relation (v2.2) and now 1,n relations (v2.4). You can also use the autobuild feature with which QGis builds the form depending on what you put in the fields properties. There is also a way to build your own form control with QtCreator ui files. Of course, you can control the form logic with Python code.

In the development of a specific form I had to build a cascading control: the value of one control is linked to the value of an upper control (controls are linked together). This is especially convenient for narrowing a choice. For example you want to choose a district or an area that restricts a list of counties that itself restricts a list of towns. I call this cascading form controls.

For the moment, QGis don't have any internal mechanism to deal with those controls. I've sent a feature request to implement it but I also found a workaround by using Python to handle form controls content. Here, I publish this solution.

Data presentation

I've got the following tables:

The flat tables are linked together: each county has got a reference to an area (with an ID) and each town has got a reference to a county.

Here the schemas of all of the tables

Schema for OBJECTGEO
Schema for OBJECTGEO

Schema for AREA
Schema for AREA

Schema for COUNTY
Schema for COUNTY

Schema for TOWN
Schema for TOWN

Build the form

You can build the form on OBJECTGEO table by following what is written below:

Forms for OBJECTGEO
Forms for OBJECTGEO

Be sure to add value relations controls for the ID_AREA, ID_COUNTY and ID_TOWN fields:

An example of value-relation form control for ID_TOWN
An example of value-relation form control for ID_TOWN

For the moment, your form looks like this:

Form without cascade control
Form without cascade control

You can find that the values of the form are not linked together (if you are French you know that Finistère is not in Pays-de-la-loire and that SAUMUR is not in Finistère). Time to change this…

Write some Python code

Now that you have some controls in your form, you need to add a little bit of logic in order to have cascading controls. Everything is made in the following Python code.

# -*- encoding: utf-8 -*-

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

myDialog = None

def formOpen(dialog,layerid,featureid):
global myDialog
myDialog = dialog

# We need to introspect the form
# In QGis v2.4 autobuild form controls do not have a name… (fixed in v2.6)
# So we need to retrieve form controls by field order !
lst_children = dialog.findChildren(QComboBox)
area = lst_children[0]
county = lst_children[1]
town = lst_children[2]

# Clear the children QComboBox widgets
county.clear()
town.clear()

# When you change the value for area widget, change the content of the county widget
area.currentIndexChanged.connect(lambda: cascadeManage('COUNTY', 'ID_COUNTY', 'COUNTY', 'ID_AREA', county, area))
# When you change the value for county widget, change the content of the town widget
county.currentIndexChanged.connect(lambda: cascadeManage('TOWN', 'ID_TOWN', 'TOWN', 'ID_COUNTY', town, county))

def cascadeManage(layername, id_idx, txt_idx, fltr, widget, parent_widget):
    """
	Generic function to manage cascading QComboBox.
    * layername: the layer (flat table) to request values for this control
    * id_idx: primary key for the table (used in for the form control key)
    * txt_idx: field shown in the form control QComboBox
    * fltr: field which is used to filter the content of this widget based on the parent widget value
    * widget: widget object (QComboBox) to control
    * parent_widget: get the value from the parent QComboBox widget
	"""
	from qgis.core import QgsMapLayerRegistry, QgsExpression

	# Get the layer (flat table)
	layers = QgsMapLayerRegistry.instance().mapLayersByName(layername)
	if (len(layers) > 0):
		layer = layers[0]
	else:
		return False

	# get attributes indexes
	ididx = layer.fieldNameIndex(id_idx)
	txtidx = layer.fieldNameIndex(txt_idx)

	# Get the value of the parent QComboBox
	filter_code = str(parent_widget.itemData(parent_widget.currentIndex()))

	# Build an expression with it
	exp = QgsExpression("{0} = {1}".format(fltr, filter_code))
	exp.prepare(layer.pendingFields())

	# clear the current widget content
	widget.clear()

	# And fill it with filtered data:
	for feature in layer.getFeatures():
		value = exp.evaluate(feature)
		if exp.hasEvalError():
			raise ValueError(exp.evalErrorString())
		if bool(value):
			widget.addItem(feature.attributes()[txtidx], feature.attributes()[ididx])

Explanations are commented in the code…

In action

Save the upper code in a file named MYFORM.py in the same directory than your QGis project.

Now, make the link between the form and the code: change the value of Python Init function text with MYFORM.formOpen:

Add Python init function
Add Python init function

Now, you can have active cascading controls:

Cascading controls form in action
Cascading controls form in action

Whenever you choose an area, it restricts the list of counties. Same thing with counties and towns…

Beware, there is a bug in QGis 2.4 which just duplicate the new object when you use a form with Python code (you can find twice on the attribute table). But it is already fixed in the master version of QGis which will become v2.6 in november 2014. For QGis 2.4, you will just delete the duplicate entry in the attribute table.

Conclusion

As a conclusion, we have a mean to implement cascading form controls even (and especially) with value-relation controls. The code could be greatly improved. For example, with the 2.6 version of QGis, you can use field name instead of trying to find the good widgets sorted by order. You could go a bit deeper and make your own ui form (with QtCreator) and create cascading form controls that only modify the value of one field (in our case, we add AREA and COUNTY fields in OBJECTGEO but the real information we want is the ID of the TOWN).

I hope that QGis will provide soon an internal (and easier to use without Python code) method to use cascading form controls !