How to build cascading form controls in QGis 2.4🔗
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:
- One geographic table named OBJECTGEO.
- Flat tables
- One for areas
- One for counties
- Last one for towns
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
Build the form
You can build the form on OBJECTGEO table by following what is written below:
Be sure to add value relations controls for the ID_AREA, ID_COUNTY and ID_TOWN fields:
For the moment, your form looks like this:
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:
- MYFORM refers to MYFORM.py Python file.
- formOpen is the name of the method which will be launched when the form opens.
Now, you can have active cascading controls:
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 !