QGIS Tips – Custom feature forms with Python logic


Last week I found a nice little undocumented feature of QGIS. I plan on writing documentation, so it won’t stay that way for long but I thought I would post about it first and run though it step by step.

This post is going to be a follow up post based on what Tim Sutton did for the same subject back in 2009 at http://linfiniti.com/2009/11/creating-dynamic-forms-a-new-feature-headed-for-qgis-1-4/

For data entry one feature I really like in QGIS is the automatic feature edit forms with support for textboxs, dropdowns and all sorts of other cool Qt controls to make data entry a breeze.

However one thing that people might not be aware of is that you can have a custom forms for data entry. QGIS will take care of setting all the fields and then saving the values back to your layer.

This could be handy if you want to have say a logo, some validation and maybe some text to help the user fill in the form correctly. Or just a custom form layout because you can.

One thing Tim didn’t follow up on was a post about how to add custom Python logic to the form, which I think is the coolest feature of having these custom forms.

So lets get started.

Creating the custom form

This process is pretty much the same as what Tim outlined in his blog post however I’m going to go over it again for completeness.

In order to create the custom form you will need to install Qt Desinger. For windows I haven’t found a way to just install the desinger although if you have QGIS installed it is normally installed with the Qt framework and can be found at C:\OSGeo4w\bin\designer.exe. If you’re on Linux you can run something like

sudo apt-get install qt4-designer

Ohh how I wish windows had a package management system :(

Fire up Qt Desinger and select “Dialog with Buttons Bottom”.

Lets throw on a couple of Labels and a few Line Edits for the data. Now set the form to use a Grid Layout (Right Click on empty space on form->Layout->Layout in Grid).

Now the trick in making a custom form for QGIS is naming the object the same as the field. So I my case I have a road layer with the following fields.

– Segment_ID
– Parcel_ID
– Name
– Alias_Name
– Locality
– Parcel_type

For my custom form I only care about Segment_ID and Name, so my form looks like:

Custom form in Qt Designer

Note that I have set the read only property of the Segment ID line edit to True so that it can’t be edited. I don’t want people messing around with the ID.

As I said above the tick is in the naming so right click on each line edit and select Change objectName, naming each line edit using the same name as the field. For me the first control is called Segment_ID and the other is called Name.

Make sure the objects are the same name as the field.

Save the form into a new folder, I have put mine in C:\Temp\Roads. Jump back into QGIS, load the properties dialog for the layer. Select the General tab and set Edit UI to the new form .ui file.

Setting the edit form UI file.

Save and exit the properties window. Enable the layer for editing (or not) and select an object with the Identify Feature tool.

Woot! Custom edit form in QGIS.

Magic! As I’m in edit mode any changes I make to the Name line edit will be reflected back on the layer (but not the Segment ID as it’s read only). If you are in non-edit mode then you are given the custom form with everything disabled and a cancel button.

With Python validation and custom logic.

Now creating a custom form like above is pretty cool although having some custom Python validation behind it would be even cooler.

What I want to do is add some validation to the Name field so the user can’t enter null road names.

First save your QGIS project (as the Python code runner will look where the project is saved for the Python module). Again I have saved mine in C:\Temp\Roads as Roads.qgs. Now lets make a new python file in your favourite text editor and add the following code.

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

nameField = None
myDialog = None

def formOpen(dialog,layerid,featureid):
	global myDialog
	myDialog = dialog
	global nameField
	nameField = dialog.findChild(QLineEdit,"Name")
	buttonBox = dialog.findChild(QDialogButtonBox,"buttonBox")

	# Disconnect the signal that QGIS has wired up for the dialog to the button box.
	buttonBox.accepted.disconnect(myDialog.accept)

	# Wire up our own signals.
	buttonBox.accepted.connect(validate)
	buttonBox.rejected.connect(myDialog.reject)

def validate():
  # Make sure that the name field isn't empty.
	if not nameField.text().length() > 0:
		msgBox = QMessageBox()
		msgBox.setText("Name field can not be null.")
		msgBox.exec_()
	else:
		# Return the form as accpeted to QGIS.
		myDialog.accept()

Wow! What the hell is all that! I’ll step though the code to explain each bit.

Code break down.

First import the modules from Qt and set up a few global variables to hold the dialog and name field.

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

nameField = None
myDialog = None

Now we create a method that QGIS will call when it loads the form. This method takes an instance of our custom dialog, the Layer ID, and the Feature ID.

def formOpen(dialog,layerid,featureid):

Then using the findChild method we want to grab the reference to the Name field and the button box. We are also calling buttonBox.accepted.disconnect() to disconnect the slots that QGIS has auto wired up to our button box, we do this so we can hook up our own accepted logic.

After we have disconnected the accepted signal we can wire up our own call to the validate method using buttonBox.accepted.connect(validate).

global myDialog
myDialog = dialog
global nameField
nameField = dialog.findChild(QLineEdit,"Name")
buttonBox = dialog.findChild(QDialogButtonBox,"buttonBox")

# Disconnect the signal that QGIS has wired up for the dialog to the button box.
buttonBox.accepted.disconnect(myDialog.accept)
# Wire up our own signals.
buttonBox.accepted.connect(validate)
buttonBox.rejected.connect(myDialog.reject)

We need a method to validate the logic. This will be called when the signal buttonBox.accepted() is called. The logic in this method should be pretty streight forward. If the Name line edit has a length > 0 then we accept the dialog, if not then we give the user a message and let them fix the mistake.

def validate():
  # Make sure that the name field isn't empty.
	if not nameField.text().length() > 0:
		msgBox = QMessageBox()
		msgBox.setText("Name field can not be null.")
		msgBox.exec_()
	else:
		# Return the form as accpeted to QGIS.
		myDialog.accept()

Almost done!

Now that you have a Python file with the custom validation logic we need to tell QGIS to use this logic for the form. First save the Python file in the same directory as your project. I have called mine C:\Temp\Roads\RoadForm.py.

Back on the General tab in the layer properties we can set the Init function field. We set this to call the module and function we just made. The syntax is {module name}.{function name}. In my case my module (the Python file we made before) is called RoadForm and the function is called formOpen, so it will be RoadForm.formOpen.

Set the Init function field to moduleName.functionName

Save and use the Identify Feature tool to select a feature. You shouldn’t get any errors if everything worked ok. Now delete everything in the Name field and hit Ok.

Validation in action

Sweet! The form can now not be accepted if the name field is null.

And that’s that. Pretty simple but powerful feature once you know how to set it up.

Enjoy!

If you do end up using this custom form with python logic stuff in the real world, leave a comment and maybe a picture. It would be good to see use cases for this cool QGIS feature.

Bonus

Why not add a red highlight to the textbox if something is not valid.

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

nameField = None
myDialog = None

def formOpen(dialog,layerid,featureid):
  global myDialog
  myDialog = dialog
  global nameField
  nameField = dialog.findChild(QLineEdit,"Name")
  buttonBox = dialog.findChild(QDialogButtonBox,"buttonBox")

  nameField.textChanged.connect(Name_onTextChanged)
  # Disconnect the signal that QGIS has wired up for the dialog to the button box.
  buttonBox.accepted.disconnect(myDialog.accept)
  # Wire up our own signals.
  buttonBox.accepted.connect(validate)
  buttonBox.rejected.connect(myDialog.reject)

def validate():
  # Make sure that the name field isn't empty.
  if not nameField.text().length() > 0:
    nameField.setStyleSheet("background-color: rgba(255, 107, 107, 150);")
    msgBox = QMessageBox()
    msgBox.setText("Name field can not be null.")
    msgBox.exec_()
  else:
  # Return the form as accpeted to QGIS.
    myDialog.accept()

def Name_onTextChanged(text):
  if not nameField.text().length() > 0:
    nameField.setStyleSheet("background-color: rgba(255, 107, 107, 150);")
  else:
    nameField.setStyleSheet("")

The key part of of this is nameField.textChanged.connect(Name_onTextChanged) and the Name_onTextChanged(text) method. Give it a try, I think it looks quite nice.

Change text background to red on invalid input.

65 thoughts on “QGIS Tips – Custom feature forms with Python logic

  1. Great, I was wondering how to use the “init function” feature.

    > For windows I haven’t found a way to just install the desinger although if you have QGIS installed it is normally installed with the Qt framework and can be found at C:\OSGeo4w\bin\designer.exe

    Thanks, I didn’t realise that, either.

    > Ohh how I wish windows had a package management system :(

    Amen to that, brother!

  2. Some information that might be helpful in the documentation:

    > First save the Python file in the same directory as your project.

    If you don’t do this you will get an error message “No module named…”.
    If you want to be able to run an init function from any project, (including a project that hasn’t been saved yet), you can save the script in your Python site-packages directory. Looking at http://docs.python.org/install/index.html it seems the Scripts directory would be more appropriate (I didn’t bother testing), or somewhere similar under the user’s home directoy.

    1. Yeah it seems QGIS will look anywhere that is defined in the PYTHONPATH variable. You can see what paths are in PYTHONPATH in the QGIS python console using:

      import sys
      print sys.path

      1. I mention that in case that You have in the PYTHONPATH only the QGis plugin directory (e.g., C:/Users//.qgis2/python/plugins) and You have Your form init Py script in a plugin’s subdirectory (e.g., C:/Users//.qgis2/python/plugins//RoadForm.py), then You need to add the pluginName to the “Python init function” field (e.g., .RoadForm.formOpen)

        I did like this from the GUI and the formOpen() method was called correctly. However, if I did the same thing from the Python script by writing:

        layer.setEditorLayout(QgsVectorLayer.UiFileLayout)
        layer.setEditForm(“C:\\Users\\rauni\\.qgis2\\python\\plugins\\Test1\\metsad.ui”)
        layer.setEditFormInit(“Test1.metsad.formOpen”)

        , then I got some weird error:

        Error occured when running following code: if hasattr(Test1.metsad,’DEBUGMODE’) and Test1.metsad.DEBUGMODE: reload(Test1.metsad)

        Traceback (most recent call last): File “”, line 1, in AttributeError: ‘module’ object has no attribute ‘metsad’

        It appears that some QGis script is seeing Test1 as module and metsad as attribute, instead of seeing Test1.metsad as module.

        Can anyone help with this?

  3. Great ! Is it possible to have, instead of a qlineedit for enter a text, a list of choices (TextA/TextB/TextC/TextD) ? User selects one choice (TextA/TextB/TextC/TextD) and this is reported in a qlineedit (may be hidden) ?

    1. If you mean like a drop-down control. Yes that is possible just replace the QLineEdit with a QComboBox, no need to have a hidden text edit. QGIS understands reading the value out of the combo box.

    1. Ahh ok I see what you want to do now. Yeah you could do it, shouldn’t be too hard. I would follow the steps to set up Python logic behind the form then follow something like this for steps (not tested).

      • Add line edit to store value of field. (Name)
      • Add checkboxs (in this case radio buttons would be better).
      • At the start of the Python code hide the Name line edit using the .hide() method.
      • Connect to the stateChanged(int state) of each checkboxs.
      • When stateChanged is fired set the value of the Name line edit to the value for that checkbox.
  4. I think this is very good for customization. It would would be really really great if one could also customize the attribute view the same way instead of a simple table. Any idea if this is posible?

  5. How would you arrange multiple fields for multiple features if not in a table?
    You do know about “Layer->properies->Fields->Edit widget”, don’t you?

  6. Not quite what I was looking at but thank you anyway. I was rather hoping that one could execute for example a sql query :-)

    1. Would you mind explaining what you want in a little bit more detail?
      There has been talk of implementing “virtual columns”, which would display the results of some sort of query, calculated on the fly (You weren’t immediately implementing this as part of the expression based labelling, were you Nathan?). Is that something like what you want?

      1. I haven’t implemented it as part of the my expression labeling, no. Although it is something that I plan on looking into whenever I get some spare time, which is getting less everyday :D (although most of it is self inflicted).

  7. Alister, I think I might be on the wrong track asking for custom attributetables. What I have is a database from another GIS with views containing cadastral info. One view contains, coordinates, others containing info about owner etc. What I want to do is to display info from those views in an attractive way without making the info look like its editabel, as its not. I try to find another way :-)

  8. Hi Nathan,

    Very useful post.

    I’m a windows user and I can’t find QT Designer as suggested (ie C:\OSGeo4w\bin\designer.exe). I have QGIS (v1.7) installed via “osgeo4w-setup.exe”.

    Is there any other way to get QT Designer. I downloaded “QT Creator” from http://qt.nokia.com, but this doesn’t seem to give me access to QT Designer as you demonstrate above.

    Any suggestions would be welcome.

    Chris Medlin
    Littlehampton, SA

  9. Thank you very much for this post. Anyway, I have a problem: If I click OK,the form is validated but no attributes are stored in a table, just null in every column. Without validate function attributes are stored. I tried it with shape file – exactly the same example as in this post, and my own data in postgis table, but its the same. I cant find mistake :( Is the presented code working? I am on Ubuntu 10.04 with QGIS 1.7.1

    1. Hmm very strange. The code was copied directly from my working example that I made before the post. Can you run the latest build of QGIS to see if it still happens, in the mean time I will chase it up to see if there is a bug somewhere.

      1. Ivan,

        I have found what the issue was. There is a bug in the python code. You need to replace buttonBox.accepted.disconnect()
        with
        buttonBox.accepted.disconnect(myDialog.accept)

        QGIS is auto connecting the accept method of the button box to the accept method of the dialog box and also the method that writes the values back to the layer so when we call buttonBox.accepted.disconnect() the values are never written to the layer.

        I have updated the post.

  10. Thank you very much, Nathan. Now it works, but I also had to replace the line
    buttonBox.rejected.connect(dialog.reject)
    with
    buttonBox.rejected.connect(myDialog.reject)

    Without that, there was python error. And now it works really excellent with both 1.8.0 on Windows from osgeo installer and 1.7.1 on Ubuntu. You can update your post accordingly. Thanks once again.

  11. Hi Nathan,

    I’ve jumped into QGIS with both feet and am having a great time learning how to use it. This post is very timely for me.

    I was able to create a simple form but in the text field the word “NULL” appears automatically. If I delete “NULL” I get the message telling me no null values are allowed (i.e. it works like it should). However, if I don’t delete “NULL” it will accept (which isn’t what I want to happen). How do I keep “NULL” from appearing by default? At first I tried changing echoMode to NoEcho in Qt Designer but that didn’t seem to work. Any suggestions? I’m probably missing the obvious.

    Thanks!
    -Anne

    1. What kind of datasource are you using eg Shapefile, PostGIS etc?

      I would say you are getting that because you have a NULL value not just an empty string in the field. You can change how QGIS handles NULLs by going to Settings->Options->Representation for NULL values and changing this so that it is empty not “NULL”.

      1. I was getting the same result using both but your solution of modifying the settings worked. Thank you!

      1. Sure. Maybe I should explain first that I am brand new to Quantum GIS. So I have a custom form that after I create my polygon will pop up. When I hit the OK button it will load the data from the form into my attribute table, and when I save, it will move the data to my database. What I’m looking for is the same custom form to open and populate data if an existing polygon is selected. Does this make more sense?

        Thanks.

      2. I see what you mean. Yeah, if you use the Identify Feature tools (normally a arrow with a i icon) it will open the same custom form.

  12. I see. My form is mostly combo boxes; I see the data populating for the few text boxes on the form, but not for the combo boxes. I assume, I will have to add logic in the form code to determine if the polygon is new or existing and populate them manually? Or is there another way to populate the combo boxes with the data from the attribute table?

    Thanks for your help.

    1. Ahh yes combo boxes aren’t auto mapped. It’s something that could be added I think. For now you will have to populate them manual I’m afraid.

      What you can do is have a hidden text box for the value of the combo box (call textbox.hide() on form open) and get QGIS to bind to that, then when you form exits you can change the value in the text box to the selected value in the combo box in order to let QGIS handle the saving of the attribute value\ back to the layer.

      I will look into how hard it would be to add combo box value binding.

    2. I’m less than a rookie with Qgis, and i’m trying to create a custom form following Nathan’s example, but i have some problems. How can i load the data from the form to the attribute table?
      Thanks

  13. Thanks for your help. I was able to figure something out. I’m binding all of the combo boxes, and there is a findText method, so I was able to query the database and populate the combos accordingly.

    Thanks again.

  14. Hello Nathan:
    I wondered how it would work to distribute a custom form to other computers, without manually setting each one up. It seems that all I need to do is put the *.ui file in the same directory as the QGIS project, then enter just the *.ui file name – not the full path – into “Edit UI” field in the layer’s properties window. I also set the project to save relative paths (tho’ I don’t know if this matters). Now I can move the whole package: *.qps, data files and *.ui to a different machine and it works!

  15. Hello, I’m running QGIS 1.7.2,

    If i follow your steps the custom form does not appear when I use the identify tool… If I use the identify tool, then right click on the resulting standard form, I get the option to ‘View Feature Form’, which if I select, I can see my custom form, but it has got rid of the OK and Cancel buttons…

    Anyone else had a similar problem/experience? I’m using a single vector layer. Happens in linux and windoze…

    1. sorry should also state that I’m not doing the python section, and sorry, the buttons do appear… need to pay more attention. Is there just a general setting somewhere to ensure it defaults to the custom form?

  16. Is it possible to add a picture in the dialog ?
    I have the location of the picture in a string field :
    photo : c:/test/picture123.jpg

    I would really love to have a dialog, automatically show labels and a picture :)

      1. Just say that I have a Qlabel named : labelforPic
        I have a field named “Photo” with path to the picture
        I just want a snippet to be launched at dialog open and set the picture to qlabel

        Best regards

  17. Hi Nathan, quick query…after following the first part of your post, I got a basic form up and working…not fiddled with the Python bit yet. However if I toggle editing off, use the info tool to select a feature, right click and view form….I get my form displayed, but there’s no data in the field boxes…any ideas??

    regards
    James

  18. Hi nathan , How can we Customize ….Can u send me the details regarding the Customization of quantum GIS to my email id

  19. Dan or anyone else, did you figure out why the form isn’t displaying automatically when accessed through the Identify tool? I’m having the same problem on Linux and I am using Python validation.

    Nathan – Thanks for the post, this is great!

  20. Hello.
    Thanks for this post, custom forms are really useful and change the way I handle Qgis.
    Using QtDesigner, you can add other buttons to the QDialogButtonBox, like Save for example.
    When the .ui is loaded in QGis, the additionnal button is not displayed. Is it normal, or do I miss something?
    I use QGis 1.7.4 on windows 7.

    1. Yeah it’s normal. QGIS replaces the QButtonBox with the normal Ok | Cancel
      buttons when it binds to the form. Shouldn’t be to hard to replace this
      if needed.

      1. Thanks, Nathan.
        It’s not really needed at the moment, I can just add a pushbutton close to the Ok|Cancel, and that’s fine…

  21. Hi Nathan.
    I’ve setup some custom edit forms, with init function, following this post, and it works fine.
    I’m a bit ennoyed by using global python variable to keep a reference on the form. Would it be possible to have, in a QGis future version, a form init funtion like this:

    def formOpen(uiFilename,layerId,featureId):
    return MyCustomDialogClass(uiFilename,layerId,featureId)

    MyCustomDialogClass would have to deal with the .ui

    class MyCustomDialogClass(QDialog):
    def __init__(self,uiFilename,layerId,featureId):
    from PyQt4 import uic
    UiDialogClass,QtDialogClass = uic.loadUiType(uiFilename)
    uiDialog = UiDialogClass()
    uiDialog.setupUi(self)
    # here can start custom logic initialisation

    This way, every action can be handle by “self”, and there no need for global variable.
    For me, the best would be:
    def formOpen(MyCustomDialogClass,layerId,featureId):
    But I don’t know if this is possible to do from the C++ part of qgis…

    Well, just a detail!

    1. Good idea. I was hoping to get some time during my uni break to have a look over how this all custom form stuff works and rework it a bit. I’ll take your feedback on board for the rework.

  22. Back again, for another question…
    Is it possible to set default values to the layer’s attributes?
    When a user create a new feature in the layer, the custom form is called. I use hidden widgets (QLineEdit) in the form. If an attribute is a float, I use float(str(myHiddenQlineEdit.text())) to get the value and initialize a QDoubleSpinBox, for example. But at creation time, the QLineEdit is empty.
    For now, I consider that if it is empty, the feature is just created, and I fill a default value. Is there a better way to do it inside QGis? My custom form goes with a python plugin…
    Thanks

  23. I wonder if it would be easy to create a custom form that is reused when you click on a new layer, instead of opening another copy of the form.
    It is possible to make the form modal, but that’s kind of crippling…

      1. > when you click on a new layer

        Sorry, I meant to say “when you click on another _feature_”.

  24. Great tutorial. I have some geodatabases with child/parent tables. Can custom forms be used to implement a “subform” with all related objects from another table via a foreign key lookup?

  25. Hello,

    i followed this great tutorial and set up an own form wich works good.
    But when i add a combo box instead of a QLineEdit it displays nothing. The intention is to show a list of already available attributes in the postgis table and also make them editable. Has anyone some ideas or can provide me a code snipet? In the above posts its said, that it should work out of the box and qgis understands it (it works in qgis own form editing).

    And the next step i want to challenge, is how to integrate a button in the form to “jump” to the next attribute (id). So that i can edit my attributes in this form and after updating simple move to the next attribute. If somebody has a idea how to realize that in python i would be very thankful.

    Thanks

  26. Hi–thanks for the post, it is exactly what I need. Being a beginner, I have tried to follow your directions exactly and copied your code but get this error message:

    if not nameField.text().length() > 0:
    AttributeError: ‘unicode’ object has no attribute ‘length’

    any ideas?

  27. Hi Nathan,
    Long time after the last reply another newbie to QGIS/ QT designer/Spatialite / Python has learned from this post to make a custom form. That was exciting, but now I really need to know if it is possible to make a form / or subform for including elements linked to my layer with one to many relationships.
    I have no clue and this same question has been posted before in may places, maybe, now, with QGIS 2.2, someone has the answer???
    Thank you
    Yuliana

  28. Is this working with v 2.2 ?
    I have this erro:
    if not nameField.text().length() > 0:
    AttributeError: ‘unicode’ object has no attribute ‘length’

    Any ideia ?
    Thanks.

Leave a reply to Chris Medlin Cancel reply