Introduction

Since QGis 1.8 (and even before), you can use QGis to build forms to deal with the attributes when you create a new geographic object. Over the successive version of QGis, form building get plent of new features like a 1,1 value relation (v2.2) and now 1,n relations (v2.2). You can also use the autobuild feature with which QGis builds the form depending on what you put in the fields properties. There is alos a way to build you 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 an 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 control the controls content. Here I publish this solution.

Data presentation

I've got the following tables:

  • On geographic table named OBJECTGEO.
  • Flat tables
    • One for AREAs
    • One for Couties
    • 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

Schema for OBJECTGEO

Schema for AREA

Schema for COUNTY

Schema for TOWN

Build the form

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

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

For the moment, your form looks like this:

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])

You can find explanations directly 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.

Add Python init function

Now, you can have actives cascading controls:

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 intern (and easier to use without Python code) method to use cascading form controls !