Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
QGIS:Becoming a GIS Power User

You're reading from   QGIS:Becoming a GIS Power User Master data management, visualization, and spatial analysis techniques in QGIS and become a GIS power user

Arrow left icon
Product type Course
Published in Feb 2017
Publisher Packt
ISBN-13 9781788299725
Length 819 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (4):
Arrow left icon
Anita Graser Anita Graser
Author Profile Icon Anita Graser
Anita Graser
Víctor Olaya Ferrero Víctor Olaya Ferrero
Author Profile Icon Víctor Olaya Ferrero
Víctor Olaya Ferrero
Alex Mandel Alex Mandel
Author Profile Icon Alex Mandel
Alex Mandel
Ben Mearns Ben Mearns
Author Profile Icon Ben Mearns
Ben Mearns
Arrow right icon
View More author details
Toc

Chapter 6. Extending QGIS with Python

This chapter is an introduction to scripting QGIS with Python. Of course, a full-blown Python tutorial would be out of scope for this book. The examples here therefore assume a minimum proficiency of working with Python. Python is a very accessible programming language even if you are just getting started, and it has gained a lot of popularity in both the open source and proprietary GIS world, for example, ESRI's ArcPy or PyQGIS. QGIS currently supports Python 2.7, but there are plans to support Python 3 in the upcoming QGIS 3.x series. We will start with an introduction to actions and then move on to the QGIS Python Console, before we go into more advanced development of custom tools for the Processing Toolbox and an explanation of how to create our own plugins.

Adding functionality using actions

Actions are a convenient way of adding custom functionality to QGIS. Actions are created for specific layers, for example, our populated places dataset, popp.shp. Therefore, to create actions, we go to Layer Properties | Actions. There are different types of actions, such as the following:

  • Generic actions start external processes; for example, you run command-line applications such as ogr2ogr

    Note

    ogr2ogr is a command-line tool that can be used to convert file formats and, at the same time, perform operations such as spatial or attribute selections and reprojecting.

  • Python actions execute Python scripts
  • Open actions open a file using your computer's configured default application, that is, your PDF viewing application for .pdf files or your browser for websites
  • Operating system (Mac, Windows, and Unix) actions work like generic actions but are restricted to the respective operating system

Configuring your first Python action

Click on the Add default actions button on the right-hand side of the dialog to add some example actions to your popp layer. This is really handy to get started with actions. For example, the Python action called Selected field's value will display the specified attribute's value when we use the action tool. All that we need to do before we can give this action a try is update it so that it accesses a valid attribute of our layer. For example, we can make it display the popp layer's TYPE attribute value in a message box, as shown in the next screenshot:

  1. Select the Selected field's value action in Action list.
  2. Edit the Action code at the bottom of the dialog. You can manually enter the attribute name or select it from the drop-down list and click on Insert field.
  3. To save the changes, click on Update selected action:
    Configuring your first Python action

To use this action, close the Layer Properties dialog and click on the drop-down arrow next to the Run Feature Action button. This will expand the list of available layer actions, as shown in the following screenshot:

Configuring your first Python action

Click on the Selected field's value entry and then click on a layer feature. This will open a pop-up dialog in which the action will output the feature's TYPE value. Of course, we can also make this action output more information, for example, by extending it to this:

QtGui.QMessageBox.information(None, "Current field's value", "Type: [% "TYPE" %] \n[% "F_CODEDESC" %]")

This will display the TYPE value on the first line and the F_CODEDESC value on the second line.

Configuring your first Python action

Opening files using actions

To open files directly from within QGIS, we use the Open actions. If you added the default actions in the previous exercise, your layer will already have an Open file action. The action is as simple as [% "PATH" %] for opening the file path specified in the layer's path attribute. Since none of our sample datasets contain a path attribute, we'll add one now to test this feature. Check out Chapter 3, Data Creation and Editing, if you need to know the details of how to add a new attribute. For example, the paths added in the following screenshot will open the default image viewer and PDF viewer application, respectively:

Opening files using actions

While the previous example uses absolute paths stored in the attributes, you can also use relative paths by changing the action code so that it completes the partial path stored in the attribute value; for example, you can use C:\temp\[% "TYPE" %].png to open .png files that are named according to the TYPE attribute values.

Opening a web browser using actions

Another type of useful Open action is opening the web browser and accessing certain websites. For example, consider this action:

http://www.google.com/search?q=[% "TYPE" %]

It will open your default web browser and search for the TYPE value using Google, and this action:.

https://en.wikipedia.org/w/index.php?search=[% "TYPE" %]

will search on Wikipedia.

Getting to know the Python Console

The most direct way to interact with the QGIS API (short for Application Programming Interface) is through the Python Console, which can be opened by going to Plugins | Python Console. As you can see in the following screenshot, the Python Console is displayed within a new panel below the map:

Getting to know the Python Console

Our access point for interaction with the application, project, and data is the iface object. To get a list of all the functions available for iface, type help(iface). Alternatively, this information is available online in the API documentation at http://qgis.org/api/classQgisInterface.html.

Loading and exploring datasets

One of the first things we will want to do is to load some data. For example, to load a vector layer, we use the addVectorLayer() function of iface:

v_layer = iface.addVectorLayer('C:/Users/anita/Documents/Geodata/qgis_sample_data/shapefiles/airports.shp','airports','ogr')

When we execute this command, airports.shp will be loaded using the ogr driver and added to the map under the layer name of airports. Additionally, this function returns the created layer object. Using this layer object—which we stored in v_layer—we can access vector layer functions, such as name(), which returns the layer name and is displayed in the Layers list:

v_layer.name()

This is the output:

u'airports'

Note

The u in front of the airports layer name shows that the name is returned as a Unicode string.

Of course, the next logical step is to look at the layer's features. The number of features can be accessed using featureCount():

v_layer.featureCount()

Here is the output:

76L

This shows us that the airport layer contains 76 features. The L in the end shows that it's a numerical value of the long type. In our next step, we will access these features. This is possible using the getFeatures() function, which returns a QgsFeatureIterator object. With a simple for loop, we can then print the attributes() of all features in our layer:

my_features = v_layer.getFeatures()
for feature in my_features:
    print feature.attributes()

This is the output:

[1, u'US00157', 78.0, u'Airport/Airfield', u'PA', u'NOATAK' ...
[2, u'US00229', 264.0, u'Airport/Airfield', u'PA', u'AMBLER'...
[3, u'US00186', 585.0, u'Airport/Airfield', u'PABT', u'BETTL...
...

Tip

When using the preceding code snippet, it is worth noting that the Python syntax requires proper indentation. This means that, for example, the content of the for loop has to be indented, as shown in the preceding code. If Python encounters such errors, it will raise an Indentation Error.

You might have noticed that attributes() shows us the attribute values, but we don't know the field names yet. To get the field names, we use this code:

for field in v_layer.fields():
    print field.name()

The output is as follows:

ID
fk_region
ELEV
NAME
USE

Once we know the field names, we can access specific feature attributes, for example, NAME:

for feature in v_layer.getFeatures():
    print feature.attribute('NAME')

This is the output:

NOATAK
AMBLER
BETTLES
...

A quick solution to, for example, sum up the elevation values is as follows:

sum([feature.attribute('ELEV') for feature in v_layer.getFeatures()])

Here is the output:

22758.0

Note

In the previous example, we took advantage of the fact that Python allows us to create a list by writing a for loop inside square brackets. This is called list comprehension, and you can read more about it at https://docs.python.org/2/tutorial/datastructures.html#list-comprehensions.

Loading raster data is very similar to loading vector data and is done using addRasterLayer():

r_layer = iface.addRasterLayer('C:/Users/anita/Documents/Geodata/qgis_sample_data/raster/SR_50M_alaska_nad.tif','hillshade')
r_layer.name()

The following is the output:

u'hillshade'

To get the raster layer's size in pixels we can use the width() and height() functions, like this:

r_layer.width(), r_layer.height()

Here is the output:

(1754, 1394)

If we want to know more about the raster values, we use the layer's data provider object, which provides access to the raster band statistics. It's worth noting that we have to use bandStatistics(1) instead of bandStatistics(0) to access the statistics of a single-band raster, such as our hillshade layer (for example, for the maximum value):

r_layer.dataProvider().bandStatistics(1).maximumValue

The output is as follows:

251.0

Other values that can be accessed like this are minimumValue, range, stdDev, and sum. For a full list, use this line:

help(r_layer.dataProvider().bandStatistics(1))

Styling layers

Since we now know how to load data, we can continue to style the layers. The simplest option is to load a premade style (a .qml file):

v_layer.loadNamedStyle('C:/temp/planes.qml')
v_layer.triggerRepaint()

Make sure that you call triggerRepaint() to ensure that the map is redrawn to reflect your changes.

Note

You can create planes.qml by saving the airport style you created in Chapter 2, Viewing Spatial Data (by going to Layer Properties | Style | Save Style | QGIS Layer Style File), or use any other style you like.

Of course, we can also create a style in code. Let's take a look at a basic single symbol renderer. We create a simple symbol with one layer, for example, a yellow diamond:

from PyQt4.QtGui import QColor
symbol = QgsMarkerSymbolV2()
symbol.symbolLayer(0).setName('diamond')
symbol.symbolLayer(0).setSize(10)
symbol.symbolLayer(0).setColor(QColor('#ffff00'))
v_layer.rendererV2().setSymbol(symbol)
v_layer.triggerRepaint()

A much more advanced approach is to create a rule-based renderer. We discussed the basics of rule-based renderers in Chapter 5, Creating Great Maps. The following example creates two rules: one for civil-use airports and one for all other airports. Due to the length of this script, I recommend that you use the Python Console editor, which you can open by clicking on the Show editor button, as shown in the following screenshot:

Styling layers

Each rule in this example has a name, a filter expression, and a symbol color. Note how the rules are appended to the renderer's root rule:

from PyQt4.QtGui import QColor
rules = [['Civil','USE LIKE \'%Civil%\'','green'], ['Other','USE NOT LIKE \'%Civil%\'','red']]
symbol = QgsSymbolV2.defaultSymbol(v_layer.geometryType())
renderer = QgsRuleBasedRendererV2(symbol)
root_rule = renderer.rootRule()
for label, expression, color_name in rules:
    rule = root_rule.children()[0].clone()
    rule.setLabel(label)
    rule.setFilterExpression(expression)
    rule.symbol().setColor(QColor(color_name))
    root_rule.appendChild(rule)
root_rule.removeChildAt(0)
v_layer.setRendererV2(renderer)
v_layer.triggerRepaint()

To run the script, click on the Run script button at the bottom of the editor toolbar.

Tip

If you are interested in reading more about styling vector layers, I recommend Joshua Arnott's post at http://snorf.net/blog/2014/03/04/symbology-of-vector-layers-in-qgis-python-plugins/.

Filtering data

To filter vector layer features programmatically, we can specify a subset string. This is the same as defining a Feature subset query in in the Layer Properties | General section. For example, we can choose to display airports only if their names start with an A:

v_layer.setSubsetString("NAME LIKE 'A%'")

To remove the filter, just set an empty subset string:

v_layer.setSubsetString("")

Creating a memory layer

A great way to create a temporary vector layer is by using so-called memory layers. Memory layers are a good option for temporary analysis output or visualizations. They are the scripting equivalent of temporary scratch layers, which we used in Chapter 3, Data Creation and Editing. Like temporary scratch layers, memory layers exist within a QGIS session and are destroyed when QGIS is closed. In the following example, we create a memory layer and add a polygon feature to it.

Basically, a memory layer is a QgsVectorLayer like any other. However, the provider (the third parameter) is not 'ogr' as in the previous example of loading a file, but 'memory'. Instead of a file path, the first parameter is a definition string that specifies the geometry type, the CRS, and the attribute table fields (in this case, one integer field called MYNUM and one string field called MYTXT):

mem_layer = QgsVectorLayer("Polygon?crs=epsg:4326&field=MYNUM:integer&field=MYTXT:string", "temp_layer", "memory")
if not mem_layer.isValid():
    raise Exception("Failed to create memory layer")

Once we have created the QgsVectorLayer object, we can start adding features to its data provider:

mem_layer_provider = mem_layer.dataProvider()
my_polygon = QgsFeature()
my_polygon.setGeometry(QgsGeometry.fromRect(QgsRectangle(16,48,17,49)))
my_polygon.setAttributes([10,"hello world"])
mem_layer_provider.addFeatures([my_polygon])
QgsMapLayerRegistry.instance().addMapLayer(mem_layer)

Note

Note how we first create a blank QgsFeature, to which we then add geometry and attributes using setGeometry() and setAttributes(), respectively. When we add the layer to QgsMapLayerRegistry, the layer is rendered on the map.

Exporting map images

The simplest option for saving the current map is by using the scripting equivalent of Save as Image (under Project). This will export the current map to an image file in the same resolution as the map area in the QGIS application window:

iface.mapCanvas().saveAsImage('C:/temp/simple_export.png')

If we want more control over the size and resolution of the exported image, we need a few more lines of code. The following example shows how we can create our own QgsMapRendererCustomPainterJob object and configure to our own liking using custom QgsMapSettings for size (width and height), resolution (dpi), map extent, and map layers:

from PyQt4.QtGui import QImage, QPainter
from PyQt4.QtCore import QSize
# configure the output image
width = 800
height = 600
dpi = 92
img = QImage(QSize(width, height), QImage.Format_RGB32)
img.setDotsPerMeterX(dpi / 25.4 * 1000)
img.setDotsPerMeterY(dpi / 25.4 * 1000)
# get the map layers and extent
layers = [ layer.id() for layer in iface.legendInterface().layers() ]
extent = iface.mapCanvas().extent()
# configure map settings for export
mapSettings = QgsMapSettings()
mapSettings.setMapUnits(0)
mapSettings.setExtent(extent)
mapSettings.setOutputDpi(dpi)
mapSettings.setOutputSize(QSize(width, height))
mapSettings.setLayers(layers)
mapSettings.setFlags(QgsMapSettings.Antialiasing | QgsMapSettings.UseAdvancedEffects | QgsMapSettings.ForceVectorOutput | QgsMapSettings.DrawLabeling)
# configure and run painter
p = QPainter()
p.begin(img)
mapRenderer = QgsMapRendererCustomPainterJob(mapSettings, p)
mapRenderer.start()
mapRenderer.waitForFinished()
p.end()
# save the result
img.save("C:/temp/custom_export.png","png")

Loading and exploring datasets

One of the first things we will want to do is to load some data. For example, to load a vector layer, we use the addVectorLayer() function of iface:

v_layer = iface.addVectorLayer('C:/Users/anita/Documents/Geodata/qgis_sample_data/shapefiles/airports.shp','airports','ogr')

When we execute this command, airports.shp will be loaded using the ogr driver and added to the map under the layer name of airports. Additionally, this function returns the created layer object. Using this layer object—which we stored in v_layer—we can access vector layer functions, such as name(), which returns the layer name and is displayed in the Layers list:

v_layer.name()

This is the output:

u'airports'

Note

The u in front of the airports layer name shows that the name is returned as a Unicode string.

Of course, the next logical step is to look at the layer's features. The number of features can be accessed using featureCount():

v_layer.featureCount()

Here is the output:

76L

This shows us that the airport layer contains 76 features. The L in the end shows that it's a numerical value of the long type. In our next step, we will access these features. This is possible using the getFeatures() function, which returns a QgsFeatureIterator object. With a simple for loop, we can then print the attributes() of all features in our layer:

my_features = v_layer.getFeatures()
for feature in my_features:
    print feature.attributes()

This is the output:

[1, u'US00157', 78.0, u'Airport/Airfield', u'PA', u'NOATAK' ...
[2, u'US00229', 264.0, u'Airport/Airfield', u'PA', u'AMBLER'...
[3, u'US00186', 585.0, u'Airport/Airfield', u'PABT', u'BETTL...
...

Tip

When using the preceding code snippet, it is worth noting that the Python syntax requires proper indentation. This means that, for example, the content of the for loop has to be indented, as shown in the preceding code. If Python encounters such errors, it will raise an Indentation Error.

You might have noticed that attributes() shows us the attribute values, but we don't know the field names yet. To get the field names, we use this code:

for field in v_layer.fields():
    print field.name()

The output is as follows:

ID
fk_region
ELEV
NAME
USE

Once we know the field names, we can access specific feature attributes, for example, NAME:

for feature in v_layer.getFeatures():
    print feature.attribute('NAME')

This is the output:

NOATAK
AMBLER
BETTLES
...

A quick solution to, for example, sum up the elevation values is as follows:

sum([feature.attribute('ELEV') for feature in v_layer.getFeatures()])

Here is the output:

22758.0

Note

In the previous example, we took advantage of the fact that Python allows us to create a list by writing a for loop inside square brackets. This is called list comprehension, and you can read more about it at https://docs.python.org/2/tutorial/datastructures.html#list-comprehensions.

Loading raster data is very similar to loading vector data and is done using addRasterLayer():

r_layer = iface.addRasterLayer('C:/Users/anita/Documents/Geodata/qgis_sample_data/raster/SR_50M_alaska_nad.tif','hillshade')
r_layer.name()

The following is the output:

u'hillshade'

To get the raster layer's size in pixels we can use the width() and height() functions, like this:

r_layer.width(), r_layer.height()

Here is the output:

(1754, 1394)

If we want to know more about the raster values, we use the layer's data provider object, which provides access to the raster band statistics. It's worth noting that we have to use bandStatistics(1) instead of bandStatistics(0) to access the statistics of a single-band raster, such as our hillshade layer (for example, for the maximum value):

r_layer.dataProvider().bandStatistics(1).maximumValue

The output is as follows:

251.0

Other values that can be accessed like this are minimumValue, range, stdDev, and sum. For a full list, use this line:

help(r_layer.dataProvider().bandStatistics(1))

Styling layers

Since we now know how to load data, we can continue to style the layers. The simplest option is to load a premade style (a .qml file):

v_layer.loadNamedStyle('C:/temp/planes.qml')
v_layer.triggerRepaint()

Make sure that you call triggerRepaint() to ensure that the map is redrawn to reflect your changes.

Note

You can create planes.qml by saving the airport style you created in Chapter 2, Viewing Spatial Data (by going to Layer Properties | Style | Save Style | QGIS Layer Style File), or use any other style you like.

Of course, we can also create a style in code. Let's take a look at a basic single symbol renderer. We create a simple symbol with one layer, for example, a yellow diamond:

from PyQt4.QtGui import QColor
symbol = QgsMarkerSymbolV2()
symbol.symbolLayer(0).setName('diamond')
symbol.symbolLayer(0).setSize(10)
symbol.symbolLayer(0).setColor(QColor('#ffff00'))
v_layer.rendererV2().setSymbol(symbol)
v_layer.triggerRepaint()

A much more advanced approach is to create a rule-based renderer. We discussed the basics of rule-based renderers in Chapter 5, Creating Great Maps. The following example creates two rules: one for civil-use airports and one for all other airports. Due to the length of this script, I recommend that you use the Python Console editor, which you can open by clicking on the Show editor button, as shown in the following screenshot:

Styling layers

Each rule in this example has a name, a filter expression, and a symbol color. Note how the rules are appended to the renderer's root rule:

from PyQt4.QtGui import QColor
rules = [['Civil','USE LIKE \'%Civil%\'','green'], ['Other','USE NOT LIKE \'%Civil%\'','red']]
symbol = QgsSymbolV2.defaultSymbol(v_layer.geometryType())
renderer = QgsRuleBasedRendererV2(symbol)
root_rule = renderer.rootRule()
for label, expression, color_name in rules:
    rule = root_rule.children()[0].clone()
    rule.setLabel(label)
    rule.setFilterExpression(expression)
    rule.symbol().setColor(QColor(color_name))
    root_rule.appendChild(rule)
root_rule.removeChildAt(0)
v_layer.setRendererV2(renderer)
v_layer.triggerRepaint()

To run the script, click on the Run script button at the bottom of the editor toolbar.

Tip

If you are interested in reading more about styling vector layers, I recommend Joshua Arnott's post at http://snorf.net/blog/2014/03/04/symbology-of-vector-layers-in-qgis-python-plugins/.

Filtering data

To filter vector layer features programmatically, we can specify a subset string. This is the same as defining a Feature subset query in in the Layer Properties | General section. For example, we can choose to display airports only if their names start with an A:

v_layer.setSubsetString("NAME LIKE 'A%'")

To remove the filter, just set an empty subset string:

v_layer.setSubsetString("")

Creating a memory layer

A great way to create a temporary vector layer is by using so-called memory layers. Memory layers are a good option for temporary analysis output or visualizations. They are the scripting equivalent of temporary scratch layers, which we used in Chapter 3, Data Creation and Editing. Like temporary scratch layers, memory layers exist within a QGIS session and are destroyed when QGIS is closed. In the following example, we create a memory layer and add a polygon feature to it.

Basically, a memory layer is a QgsVectorLayer like any other. However, the provider (the third parameter) is not 'ogr' as in the previous example of loading a file, but 'memory'. Instead of a file path, the first parameter is a definition string that specifies the geometry type, the CRS, and the attribute table fields (in this case, one integer field called MYNUM and one string field called MYTXT):

mem_layer = QgsVectorLayer("Polygon?crs=epsg:4326&field=MYNUM:integer&field=MYTXT:string", "temp_layer", "memory")
if not mem_layer.isValid():
    raise Exception("Failed to create memory layer")

Once we have created the QgsVectorLayer object, we can start adding features to its data provider:

mem_layer_provider = mem_layer.dataProvider()
my_polygon = QgsFeature()
my_polygon.setGeometry(QgsGeometry.fromRect(QgsRectangle(16,48,17,49)))
my_polygon.setAttributes([10,"hello world"])
mem_layer_provider.addFeatures([my_polygon])
QgsMapLayerRegistry.instance().addMapLayer(mem_layer)

Note

Note how we first create a blank QgsFeature, to which we then add geometry and attributes using setGeometry() and setAttributes(), respectively. When we add the layer to QgsMapLayerRegistry, the layer is rendered on the map.

Exporting map images

The simplest option for saving the current map is by using the scripting equivalent of Save as Image (under Project). This will export the current map to an image file in the same resolution as the map area in the QGIS application window:

iface.mapCanvas().saveAsImage('C:/temp/simple_export.png')

If we want more control over the size and resolution of the exported image, we need a few more lines of code. The following example shows how we can create our own QgsMapRendererCustomPainterJob object and configure to our own liking using custom QgsMapSettings for size (width and height), resolution (dpi), map extent, and map layers:

from PyQt4.QtGui import QImage, QPainter
from PyQt4.QtCore import QSize
# configure the output image
width = 800
height = 600
dpi = 92
img = QImage(QSize(width, height), QImage.Format_RGB32)
img.setDotsPerMeterX(dpi / 25.4 * 1000)
img.setDotsPerMeterY(dpi / 25.4 * 1000)
# get the map layers and extent
layers = [ layer.id() for layer in iface.legendInterface().layers() ]
extent = iface.mapCanvas().extent()
# configure map settings for export
mapSettings = QgsMapSettings()
mapSettings.setMapUnits(0)
mapSettings.setExtent(extent)
mapSettings.setOutputDpi(dpi)
mapSettings.setOutputSize(QSize(width, height))
mapSettings.setLayers(layers)
mapSettings.setFlags(QgsMapSettings.Antialiasing | QgsMapSettings.UseAdvancedEffects | QgsMapSettings.ForceVectorOutput | QgsMapSettings.DrawLabeling)
# configure and run painter
p = QPainter()
p.begin(img)
mapRenderer = QgsMapRendererCustomPainterJob(mapSettings, p)
mapRenderer.start()
mapRenderer.waitForFinished()
p.end()
# save the result
img.save("C:/temp/custom_export.png","png")

Styling layers

Since we now know how to load data, we can continue to style the layers. The simplest option is to load a premade style (a .qml file):

v_layer.loadNamedStyle('C:/temp/planes.qml')
v_layer.triggerRepaint()

Make sure that you call triggerRepaint() to ensure that the map is redrawn to reflect your changes.

Note

You can create planes.qml by saving the airport style you created in Chapter 2, Viewing Spatial Data (by going to Layer Properties | Style | Save Style | QGIS Layer Style File), or use any other style you like.

Of course, we can also create a style in code. Let's take a look at a basic single symbol renderer. We create a simple symbol with one layer, for example, a yellow diamond:

from PyQt4.QtGui import QColor
symbol = QgsMarkerSymbolV2()
symbol.symbolLayer(0).setName('diamond')
symbol.symbolLayer(0).setSize(10)
symbol.symbolLayer(0).setColor(QColor('#ffff00'))
v_layer.rendererV2().setSymbol(symbol)
v_layer.triggerRepaint()

A much more advanced approach is to create a rule-based renderer. We discussed the basics of rule-based renderers in Chapter 5, Creating Great Maps. The following example creates two rules: one for civil-use airports and one for all other airports. Due to the length of this script, I recommend that you use the Python Console editor, which you can open by clicking on the Show editor button, as shown in the following screenshot:

Styling layers

Each rule in this example has a name, a filter expression, and a symbol color. Note how the rules are appended to the renderer's root rule:

from PyQt4.QtGui import QColor
rules = [['Civil','USE LIKE \'%Civil%\'','green'], ['Other','USE NOT LIKE \'%Civil%\'','red']]
symbol = QgsSymbolV2.defaultSymbol(v_layer.geometryType())
renderer = QgsRuleBasedRendererV2(symbol)
root_rule = renderer.rootRule()
for label, expression, color_name in rules:
    rule = root_rule.children()[0].clone()
    rule.setLabel(label)
    rule.setFilterExpression(expression)
    rule.symbol().setColor(QColor(color_name))
    root_rule.appendChild(rule)
root_rule.removeChildAt(0)
v_layer.setRendererV2(renderer)
v_layer.triggerRepaint()

To run the script, click on the Run script button at the bottom of the editor toolbar.

Tip

If you are interested in reading more about styling vector layers, I recommend Joshua Arnott's post at http://snorf.net/blog/2014/03/04/symbology-of-vector-layers-in-qgis-python-plugins/.

Filtering data

To filter vector layer features programmatically, we can specify a subset string. This is the same as defining a Feature subset query in in the Layer Properties | General section. For example, we can choose to display airports only if their names start with an A:

v_layer.setSubsetString("NAME LIKE 'A%'")

To remove the filter, just set an empty subset string:

v_layer.setSubsetString("")

Creating a memory layer

A great way to create a temporary vector layer is by using so-called memory layers. Memory layers are a good option for temporary analysis output or visualizations. They are the scripting equivalent of temporary scratch layers, which we used in Chapter 3, Data Creation and Editing. Like temporary scratch layers, memory layers exist within a QGIS session and are destroyed when QGIS is closed. In the following example, we create a memory layer and add a polygon feature to it.

Basically, a memory layer is a QgsVectorLayer like any other. However, the provider (the third parameter) is not 'ogr' as in the previous example of loading a file, but 'memory'. Instead of a file path, the first parameter is a definition string that specifies the geometry type, the CRS, and the attribute table fields (in this case, one integer field called MYNUM and one string field called MYTXT):

mem_layer = QgsVectorLayer("Polygon?crs=epsg:4326&field=MYNUM:integer&field=MYTXT:string", "temp_layer", "memory")
if not mem_layer.isValid():
    raise Exception("Failed to create memory layer")

Once we have created the QgsVectorLayer object, we can start adding features to its data provider:

mem_layer_provider = mem_layer.dataProvider()
my_polygon = QgsFeature()
my_polygon.setGeometry(QgsGeometry.fromRect(QgsRectangle(16,48,17,49)))
my_polygon.setAttributes([10,"hello world"])
mem_layer_provider.addFeatures([my_polygon])
QgsMapLayerRegistry.instance().addMapLayer(mem_layer)

Note

Note how we first create a blank QgsFeature, to which we then add geometry and attributes using setGeometry() and setAttributes(), respectively. When we add the layer to QgsMapLayerRegistry, the layer is rendered on the map.

Exporting map images

The simplest option for saving the current map is by using the scripting equivalent of Save as Image (under Project). This will export the current map to an image file in the same resolution as the map area in the QGIS application window:

iface.mapCanvas().saveAsImage('C:/temp/simple_export.png')

If we want more control over the size and resolution of the exported image, we need a few more lines of code. The following example shows how we can create our own QgsMapRendererCustomPainterJob object and configure to our own liking using custom QgsMapSettings for size (width and height), resolution (dpi), map extent, and map layers:

from PyQt4.QtGui import QImage, QPainter
from PyQt4.QtCore import QSize
# configure the output image
width = 800
height = 600
dpi = 92
img = QImage(QSize(width, height), QImage.Format_RGB32)
img.setDotsPerMeterX(dpi / 25.4 * 1000)
img.setDotsPerMeterY(dpi / 25.4 * 1000)
# get the map layers and extent
layers = [ layer.id() for layer in iface.legendInterface().layers() ]
extent = iface.mapCanvas().extent()
# configure map settings for export
mapSettings = QgsMapSettings()
mapSettings.setMapUnits(0)
mapSettings.setExtent(extent)
mapSettings.setOutputDpi(dpi)
mapSettings.setOutputSize(QSize(width, height))
mapSettings.setLayers(layers)
mapSettings.setFlags(QgsMapSettings.Antialiasing | QgsMapSettings.UseAdvancedEffects | QgsMapSettings.ForceVectorOutput | QgsMapSettings.DrawLabeling)
# configure and run painter
p = QPainter()
p.begin(img)
mapRenderer = QgsMapRendererCustomPainterJob(mapSettings, p)
mapRenderer.start()
mapRenderer.waitForFinished()
p.end()
# save the result
img.save("C:/temp/custom_export.png","png")

Filtering data

To filter vector layer features programmatically, we can specify a subset string. This is the same as defining a Feature subset query in in the Layer Properties | General section. For example, we can choose to display airports only if their names start with an A:

v_layer.setSubsetString("NAME LIKE 'A%'")

To remove the filter, just set an empty subset string:

v_layer.setSubsetString("")

Creating a memory layer

A great way to create a temporary vector layer is by using so-called memory layers. Memory layers are a good option for temporary analysis output or visualizations. They are the scripting equivalent of temporary scratch layers, which we used in Chapter 3, Data Creation and Editing. Like temporary scratch layers, memory layers exist within a QGIS session and are destroyed when QGIS is closed. In the following example, we create a memory layer and add a polygon feature to it.

Basically, a memory layer is a QgsVectorLayer like any other. However, the provider (the third parameter) is not 'ogr' as in the previous example of loading a file, but 'memory'. Instead of a file path, the first parameter is a definition string that specifies the geometry type, the CRS, and the attribute table fields (in this case, one integer field called MYNUM and one string field called MYTXT):

mem_layer = QgsVectorLayer("Polygon?crs=epsg:4326&field=MYNUM:integer&field=MYTXT:string", "temp_layer", "memory")
if not mem_layer.isValid():
    raise Exception("Failed to create memory layer")

Once we have created the QgsVectorLayer object, we can start adding features to its data provider:

mem_layer_provider = mem_layer.dataProvider()
my_polygon = QgsFeature()
my_polygon.setGeometry(QgsGeometry.fromRect(QgsRectangle(16,48,17,49)))
my_polygon.setAttributes([10,"hello world"])
mem_layer_provider.addFeatures([my_polygon])
QgsMapLayerRegistry.instance().addMapLayer(mem_layer)

Note

Note how we first create a blank QgsFeature, to which we then add geometry and attributes using setGeometry() and setAttributes(), respectively. When we add the layer to QgsMapLayerRegistry, the layer is rendered on the map.

Exporting map images

The simplest option for saving the current map is by using the scripting equivalent of Save as Image (under Project). This will export the current map to an image file in the same resolution as the map area in the QGIS application window:

iface.mapCanvas().saveAsImage('C:/temp/simple_export.png')

If we want more control over the size and resolution of the exported image, we need a few more lines of code. The following example shows how we can create our own QgsMapRendererCustomPainterJob object and configure to our own liking using custom QgsMapSettings for size (width and height), resolution (dpi), map extent, and map layers:

from PyQt4.QtGui import QImage, QPainter
from PyQt4.QtCore import QSize
# configure the output image
width = 800
height = 600
dpi = 92
img = QImage(QSize(width, height), QImage.Format_RGB32)
img.setDotsPerMeterX(dpi / 25.4 * 1000)
img.setDotsPerMeterY(dpi / 25.4 * 1000)
# get the map layers and extent
layers = [ layer.id() for layer in iface.legendInterface().layers() ]
extent = iface.mapCanvas().extent()
# configure map settings for export
mapSettings = QgsMapSettings()
mapSettings.setMapUnits(0)
mapSettings.setExtent(extent)
mapSettings.setOutputDpi(dpi)
mapSettings.setOutputSize(QSize(width, height))
mapSettings.setLayers(layers)
mapSettings.setFlags(QgsMapSettings.Antialiasing | QgsMapSettings.UseAdvancedEffects | QgsMapSettings.ForceVectorOutput | QgsMapSettings.DrawLabeling)
# configure and run painter
p = QPainter()
p.begin(img)
mapRenderer = QgsMapRendererCustomPainterJob(mapSettings, p)
mapRenderer.start()
mapRenderer.waitForFinished()
p.end()
# save the result
img.save("C:/temp/custom_export.png","png")

Creating a memory layer

A great way to create a temporary vector layer is by using so-called memory layers. Memory layers are a good option for temporary analysis output or visualizations. They are the scripting equivalent of temporary scratch layers, which we used in Chapter 3, Data Creation and Editing. Like temporary scratch layers, memory layers exist within a QGIS session and are destroyed when QGIS is closed. In the following example, we create a memory layer and add a polygon feature to it.

Basically, a memory layer is a QgsVectorLayer like any other. However, the provider (the third parameter) is not 'ogr' as in the previous example of loading a file, but 'memory'. Instead of a file path, the first parameter is a definition string that specifies the geometry type, the CRS, and the attribute table fields (in this case, one integer field called MYNUM and one string field called MYTXT):

mem_layer = QgsVectorLayer("Polygon?crs=epsg:4326&field=MYNUM:integer&field=MYTXT:string", "temp_layer", "memory")
if not mem_layer.isValid():
    raise Exception("Failed to create memory layer")

Once we have created the QgsVectorLayer object, we can start adding features to its data provider:

mem_layer_provider = mem_layer.dataProvider()
my_polygon = QgsFeature()
my_polygon.setGeometry(QgsGeometry.fromRect(QgsRectangle(16,48,17,49)))
my_polygon.setAttributes([10,"hello world"])
mem_layer_provider.addFeatures([my_polygon])
QgsMapLayerRegistry.instance().addMapLayer(mem_layer)

Note

Note how we first create a blank QgsFeature, to which we then add geometry and attributes using setGeometry() and setAttributes(), respectively. When we add the layer to QgsMapLayerRegistry, the layer is rendered on the map.

Exporting map images

The simplest option for saving the current map is by using the scripting equivalent of Save as Image (under Project). This will export the current map to an image file in the same resolution as the map area in the QGIS application window:

iface.mapCanvas().saveAsImage('C:/temp/simple_export.png')

If we want more control over the size and resolution of the exported image, we need a few more lines of code. The following example shows how we can create our own QgsMapRendererCustomPainterJob object and configure to our own liking using custom QgsMapSettings for size (width and height), resolution (dpi), map extent, and map layers:

from PyQt4.QtGui import QImage, QPainter
from PyQt4.QtCore import QSize
# configure the output image
width = 800
height = 600
dpi = 92
img = QImage(QSize(width, height), QImage.Format_RGB32)
img.setDotsPerMeterX(dpi / 25.4 * 1000)
img.setDotsPerMeterY(dpi / 25.4 * 1000)
# get the map layers and extent
layers = [ layer.id() for layer in iface.legendInterface().layers() ]
extent = iface.mapCanvas().extent()
# configure map settings for export
mapSettings = QgsMapSettings()
mapSettings.setMapUnits(0)
mapSettings.setExtent(extent)
mapSettings.setOutputDpi(dpi)
mapSettings.setOutputSize(QSize(width, height))
mapSettings.setLayers(layers)
mapSettings.setFlags(QgsMapSettings.Antialiasing | QgsMapSettings.UseAdvancedEffects | QgsMapSettings.ForceVectorOutput | QgsMapSettings.DrawLabeling)
# configure and run painter
p = QPainter()
p.begin(img)
mapRenderer = QgsMapRendererCustomPainterJob(mapSettings, p)
mapRenderer.start()
mapRenderer.waitForFinished()
p.end()
# save the result
img.save("C:/temp/custom_export.png","png")

Exporting map images

The simplest option for saving the current map is by using the scripting equivalent of Save as Image (under Project). This will export the current map to an image file in the same resolution as the map area in the QGIS application window:

iface.mapCanvas().saveAsImage('C:/temp/simple_export.png')

If we want more control over the size and resolution of the exported image, we need a few more lines of code. The following example shows how we can create our own QgsMapRendererCustomPainterJob object and configure to our own liking using custom QgsMapSettings for size (width and height), resolution (dpi), map extent, and map layers:

from PyQt4.QtGui import QImage, QPainter
from PyQt4.QtCore import QSize
# configure the output image
width = 800
height = 600
dpi = 92
img = QImage(QSize(width, height), QImage.Format_RGB32)
img.setDotsPerMeterX(dpi / 25.4 * 1000)
img.setDotsPerMeterY(dpi / 25.4 * 1000)
# get the map layers and extent
layers = [ layer.id() for layer in iface.legendInterface().layers() ]
extent = iface.mapCanvas().extent()
# configure map settings for export
mapSettings = QgsMapSettings()
mapSettings.setMapUnits(0)
mapSettings.setExtent(extent)
mapSettings.setOutputDpi(dpi)
mapSettings.setOutputSize(QSize(width, height))
mapSettings.setLayers(layers)
mapSettings.setFlags(QgsMapSettings.Antialiasing | QgsMapSettings.UseAdvancedEffects | QgsMapSettings.ForceVectorOutput | QgsMapSettings.DrawLabeling)
# configure and run painter
p = QPainter()
p.begin(img)
mapRenderer = QgsMapRendererCustomPainterJob(mapSettings, p)
mapRenderer.start()
mapRenderer.waitForFinished()
p.end()
# save the result
img.save("C:/temp/custom_export.png","png")

Creating custom geoprocessing scripts using Python

In Chapter 4, Spatial Analysis, we used the tools of Processing Toolbox to analyze our data, but we are not limited to these tools. We can expand processing with our own scripts. The advantages of processing scripts over normal Python scripts, such as the ones we saw in the previous section, are as follows:

  • Processing automatically generates a graphical user interface for the script to configure the script parameters
  • Processing scripts can be used in Graphical modeler to create geoprocessing models

As the following screenshot shows, the Scripts section is initially empty, except for some Tools to add and create new scripts:

Creating custom geoprocessing scripts using Python

Writing your first Processing script

We will create our first simple script; which fetches some layer information. To get started, double-click on the Create new script entry in Scripts | Tools. This opens an empty Script editor dialog. The following screenshot shows the Script editor with a short script that prints the input layer's name on the Python Console:

Writing your first Processing script

The first line means our script will be put into the Learning QGIS group of scripts, as shown in the following screenshot. The double hashes (##) are Processing syntax and they indicate that the line contains Processing-specific information rather than Python code. The script name is created from the filename you chose when you saved the script. For this example, I have saved the script as my_first_script.py. The second line defines the script input, a vector layer in this case. On the following line, we use Processing's getObject() function to get access to the input layer object, and finally the layer name is printed on the Python Console.

You can run the script either directly from within the editor by clicking on the Run algorithm button, or by double-clicking on the entry in the Processing Toolbox. If you want to change the code, use Edit script from the entry context menu, as shown in this screenshot:

Writing your first Processing script

Tip

A good way of learning how to write custom scripts for Processing is to take a look at existing scripts, for example, at https://github.com/qgis/QGIS-Processing/tree/master/scripts. This is the official script repository, where you can also download scripts using the built-in Get scripts from on-line scripts collection tool in the Processing Toolbox.

Writing a script with vector layer output

Of course, in most cases, we don't want to just output something on the Python Console. That is why the following example shows how to create a vector layer. More specifically, the script creates square polygons around the points in the input layer. The numeric size input parameter controls the size of the squares in the output vector layer. The default size that will be displayed in the automatically generated dialog is set to 1000000:

##Learning QGIS=group
##input_layer=vector
##size=number 1000000
##squares=output vector
from qgis.core import *
from processing.tools.vector import VectorWriter
# get the input layer and its fields
my_layer = processing.getObject(input_layer)
fields = my_layer.dataProvider().fields()
# create the output vector writer with the same fields
writer = VectorWriter(squares, None, fields, QGis.WKBPolygon, my_layer.crs())
# create output features
feat = QgsFeature()
for input_feature in my_layer.getFeatures():
    # copy attributes from the input point feature
    attributes = input_feature.attributes()
    feat.setAttributes(attributes)
    # create square polygons
    point = input_feature.geometry().asPoint()
    xmin = point.x() - size/2
    ymin = point.y() - size/2
    square = QgsRectangle(xmin,ymin,xmin+size,ymin+size)
    feat.setGeometry(QgsGeometry.fromRect(square))
    writer.addFeature(feat)
del writer

In this script, we use a VectorWriter to write the output vector layer. The parameters for creating a VectorWriter object are fileName, encoding, fields, geometryType, and crs.

Note

The available geometry types are QGis.WKBPoint, QGis.WKBLineString, QGis.WKBPolygon, QGis.WKBMultiPoint, QGis.WKBMultiLineString, and QGis.WKBMultiPolygon. You can also get this list of geometry types by typing VectorWriter.TYPE_MAP in the Python Console.

Note how we use the fields of the input layer (my_layer.dataProvider().fields()) to create the VectorWriter. This ensures that the output layer has the same fields (attribute table columns) as the input layer. Similarly, for each feature in the input layer, we copy its attribute values (input_feature.attributes()) to the corresponding output feature.

After running the script, the resulting layer will be loaded into QGIS and listed using the output parameter name; in this case, the layer is called squares. The following screenshot shows the automatically generated input dialog as well as the output of the script when applied to the airports from our sample dataset:

Writing a script with vector layer output

Visualizing the script progress

Especially when executing complex scripts that take a while to finish, it is good practice to display the progress of the script execution in a progress bar. To add a progress bar to the previous script, we can add the following lines of code before and inside the for loop that loops through the input features:

i = 0
n = my_layer.featureCount()
for input_feature in my_layer.getFeatures():
    progress.setPercentage(int(100*i/n))
    i+=1

Note

Note that we initialize the i counter before the loop and increase it inside the loop after updating the progress bar using progress.setPercentage().

Writing your first Processing script

We will create our first simple script; which fetches some layer information. To get started, double-click on the Create new script entry in Scripts | Tools. This opens an empty Script editor dialog. The following screenshot shows the Script editor with a short script that prints the input layer's name on the Python Console:

Writing your first Processing script

The first line means our script will be put into the Learning QGIS group of scripts, as shown in the following screenshot. The double hashes (##) are Processing syntax and they indicate that the line contains Processing-specific information rather than Python code. The script name is created from the filename you chose when you saved the script. For this example, I have saved the script as my_first_script.py. The second line defines the script input, a vector layer in this case. On the following line, we use Processing's getObject() function to get access to the input layer object, and finally the layer name is printed on the Python Console.

You can run the script either directly from within the editor by clicking on the Run algorithm button, or by double-clicking on the entry in the Processing Toolbox. If you want to change the code, use Edit script from the entry context menu, as shown in this screenshot:

Writing your first Processing script

Tip

A good way of learning how to write custom scripts for Processing is to take a look at existing scripts, for example, at https://github.com/qgis/QGIS-Processing/tree/master/scripts. This is the official script repository, where you can also download scripts using the built-in Get scripts from on-line scripts collection tool in the Processing Toolbox.

Writing a script with vector layer output

Of course, in most cases, we don't want to just output something on the Python Console. That is why the following example shows how to create a vector layer. More specifically, the script creates square polygons around the points in the input layer. The numeric size input parameter controls the size of the squares in the output vector layer. The default size that will be displayed in the automatically generated dialog is set to 1000000:

##Learning QGIS=group
##input_layer=vector
##size=number 1000000
##squares=output vector
from qgis.core import *
from processing.tools.vector import VectorWriter
# get the input layer and its fields
my_layer = processing.getObject(input_layer)
fields = my_layer.dataProvider().fields()
# create the output vector writer with the same fields
writer = VectorWriter(squares, None, fields, QGis.WKBPolygon, my_layer.crs())
# create output features
feat = QgsFeature()
for input_feature in my_layer.getFeatures():
    # copy attributes from the input point feature
    attributes = input_feature.attributes()
    feat.setAttributes(attributes)
    # create square polygons
    point = input_feature.geometry().asPoint()
    xmin = point.x() - size/2
    ymin = point.y() - size/2
    square = QgsRectangle(xmin,ymin,xmin+size,ymin+size)
    feat.setGeometry(QgsGeometry.fromRect(square))
    writer.addFeature(feat)
del writer

In this script, we use a VectorWriter to write the output vector layer. The parameters for creating a VectorWriter object are fileName, encoding, fields, geometryType, and crs.

Note

The available geometry types are QGis.WKBPoint, QGis.WKBLineString, QGis.WKBPolygon, QGis.WKBMultiPoint, QGis.WKBMultiLineString, and QGis.WKBMultiPolygon. You can also get this list of geometry types by typing VectorWriter.TYPE_MAP in the Python Console.

Note how we use the fields of the input layer (my_layer.dataProvider().fields()) to create the VectorWriter. This ensures that the output layer has the same fields (attribute table columns) as the input layer. Similarly, for each feature in the input layer, we copy its attribute values (input_feature.attributes()) to the corresponding output feature.

After running the script, the resulting layer will be loaded into QGIS and listed using the output parameter name; in this case, the layer is called squares. The following screenshot shows the automatically generated input dialog as well as the output of the script when applied to the airports from our sample dataset:

Writing a script with vector layer output

Visualizing the script progress

Especially when executing complex scripts that take a while to finish, it is good practice to display the progress of the script execution in a progress bar. To add a progress bar to the previous script, we can add the following lines of code before and inside the for loop that loops through the input features:

i = 0
n = my_layer.featureCount()
for input_feature in my_layer.getFeatures():
    progress.setPercentage(int(100*i/n))
    i+=1

Note

Note that we initialize the i counter before the loop and increase it inside the loop after updating the progress bar using progress.setPercentage().

Writing a script with vector layer output

Of course, in most cases, we don't want to just output something on the Python Console. That is why the following example shows how to create a vector layer. More specifically, the script creates square polygons around the points in the input layer. The numeric size input parameter controls the size of the squares in the output vector layer. The default size that will be displayed in the automatically generated dialog is set to 1000000:

##Learning QGIS=group
##input_layer=vector
##size=number 1000000
##squares=output vector
from qgis.core import *
from processing.tools.vector import VectorWriter
# get the input layer and its fields
my_layer = processing.getObject(input_layer)
fields = my_layer.dataProvider().fields()
# create the output vector writer with the same fields
writer = VectorWriter(squares, None, fields, QGis.WKBPolygon, my_layer.crs())
# create output features
feat = QgsFeature()
for input_feature in my_layer.getFeatures():
    # copy attributes from the input point feature
    attributes = input_feature.attributes()
    feat.setAttributes(attributes)
    # create square polygons
    point = input_feature.geometry().asPoint()
    xmin = point.x() - size/2
    ymin = point.y() - size/2
    square = QgsRectangle(xmin,ymin,xmin+size,ymin+size)
    feat.setGeometry(QgsGeometry.fromRect(square))
    writer.addFeature(feat)
del writer

In this script, we use a VectorWriter to write the output vector layer. The parameters for creating a VectorWriter object are fileName, encoding, fields, geometryType, and crs.

Note

The available geometry types are QGis.WKBPoint, QGis.WKBLineString, QGis.WKBPolygon, QGis.WKBMultiPoint, QGis.WKBMultiLineString, and QGis.WKBMultiPolygon. You can also get this list of geometry types by typing VectorWriter.TYPE_MAP in the Python Console.

Note how we use the fields of the input layer (my_layer.dataProvider().fields()) to create the VectorWriter. This ensures that the output layer has the same fields (attribute table columns) as the input layer. Similarly, for each feature in the input layer, we copy its attribute values (input_feature.attributes()) to the corresponding output feature.

After running the script, the resulting layer will be loaded into QGIS and listed using the output parameter name; in this case, the layer is called squares. The following screenshot shows the automatically generated input dialog as well as the output of the script when applied to the airports from our sample dataset:

Writing a script with vector layer output

Visualizing the script progress

Especially when executing complex scripts that take a while to finish, it is good practice to display the progress of the script execution in a progress bar. To add a progress bar to the previous script, we can add the following lines of code before and inside the for loop that loops through the input features:

i = 0
n = my_layer.featureCount()
for input_feature in my_layer.getFeatures():
    progress.setPercentage(int(100*i/n))
    i+=1

Note

Note that we initialize the i counter before the loop and increase it inside the loop after updating the progress bar using progress.setPercentage().

Visualizing the script progress

Especially when executing complex scripts that take a while to finish, it is good practice to display the progress of the script execution in a progress bar. To add a progress bar to the previous script, we can add the following lines of code before and inside the for loop that loops through the input features:

i = 0
n = my_layer.featureCount()
for input_feature in my_layer.getFeatures():
    progress.setPercentage(int(100*i/n))
    i+=1

Note

Note that we initialize the i counter before the loop and increase it inside the loop after updating the progress bar using progress.setPercentage().

Developing your first plugin

When you want to implement interactive tools or very specific graphical user interfaces, it is time to look into plugin development. In the previous exercises, we introduced the QGIS Python API. Therefore, we can now focus on the necessary steps to get our first QGIS plugin started. The great thing about creating plugins for QGIS is that there is a plugin for this! It's called Plugin Builder. And while you are at it, also install Plugin Reloader, which is very useful for plugin developers. Because it lets you quickly reload your plugin without having to restart QGIS every time you make changes to the code. When you have installed both plugins, your Plugins toolbar will look like this:

Developing your first plugin

Before we can get started, we also need to install Qt Designer, which is the application we will use to design the user interface. If you are using Windows, I recommend WinPython (http://winpython.github.io/) version 2.7.10.3 (the latest version with Python 2.7 at the time of writing this book), which provides Qt Designer and Spyder (an integrated development environment for Python). On Ubuntu, you can install Qt Designer using sudo apt-get install qt4-designer. On Mac, you can get the Qt Creator installer (which includes Qt Designer) from http://qt-project.org/downloads.

Creating the plugin template with Plugin Builder

Plugin Builder will create all the files that we need for our plugin. To create a plugin template, follow these steps:

  1. Start Plugin Builder and input the basic plugin information, including:
    • Class name (one word in camel case; that is, each word starts with an upper case letter)
    • Plugin name (a short description)
    • Module name (the Python module name for the plugin)

    When you hover your mouse over the input fields in the Plugin Builder dialog, it displays help information, as shown in the following screenshot:

    Creating the plugin template with Plugin Builder
  2. Click on Next to get to the About dialog, where you can enter a more detailed description of what your plugin does. Since we are planning to create the first plugin for learning purposes only, we can just put some random text here and click on Next.
  3. Now we can select a plugin Template and specify a Text for the menu item as well as which Menu the plugin should be listed in, as shown in the following screenshot. The available templates include Tool button with dialog, Tool button with dock widget, and Processing provider. In this exercise, we'll create a Tool button with dialog and click on Next:
    Creating the plugin template with Plugin Builder
  4. The following dialog presents checkboxes, where we can chose which non-essential plugin files should be created. You can select any subset of the provided options and click on Next.
  5. In the next dialog, we need to specify the plugin Bug tracker and the code Repository. Again, since we are creating this plugin only for learning purposes, I'm just making up some URLs in the next screenshot, but you should use the appropriate trackers and code repositories if you are planning to make your plugin publicly available:
    Creating the plugin template with Plugin Builder
  6. Once you click on Next, you will be asked to select a folder to store the plugin. You can save it directly in the QGIS plugin folder, ~\.qgis2\python\plugins on Windows, or ~/.qgis2/python/plugins on Linux and Mac.
  7. Once you have selected the plugin folder, it displays a Plugin Builder Results confirmation dialog, which confirms the location of your plugin folder as well as the location of your QGIS plugin folder. As mentioned earlier, I saved directly in the QGIS plugin folder, as you can see in the following screenshot. If you have saved in a different location, you can now move the plugin folder into the QGIS plugins folder to make sure that QGIS can find and load it:
    Creating the plugin template with Plugin Builder

One thing we still have to do is prepare the icon for the plugin toolbar. This requires us to compile the resources.qrc file, which Plugin Builder created automatically, to turn the icon into usable Python code. This is done on the command line. On Windows, I recommend using the OSGeo4W shell, because it makes sure that the environment variables are set in such a way that the necessary tools can be found. Navigate to the plugin folder and run this:

pyrcc4 -o resources.py resources.qrc

Tip

You can replace the default icon (icon.png) to add your own plugin icon. Afterwards, you just have to recompile resources_rc.qrc as shown previously.

Restart QGIS and you should now see your plugin listed in the Plugin Manager, as shown here:

Creating the plugin template with Plugin Builder

Activate your plugin in the Plugin Manager and you should see it listed in the Plugins menu. When you start your plugin, it will display a blank dialog that is just waiting for you to customize it.

Customizing the plugin GUI

To customize the blank default plugin dialog, we use Qt Designer. You can find the dialog file in the plugin folder. In my case, it is called my_first_plugin_dialog_base.ui (derived from the module name I specified in Plugin Builder). When you open your plugin's .ui file in Qt Designer, you will see the blank dialog. Now you can start adding widgets by dragging and dropping them from the Widget Box on the left-hand side of the Qt Designer window. In the following screenshot, you can see that I added a Label and a drop-down list widget (listed as Combo Box in the Widgetbox). You can change the label text to Layer by double-clicking on the default label text. Additionally, it is good practice to assign descriptive names to the widget objects; for example, I renamed the combobox to layerCombo, as you can see here in the bottom-right corner:

Customizing the plugin GUI

Once you are finished with the changes to the plugin dialog, you can save them. Then you can go back to QGIS. In QGIS, you can now configure Plugin Reloader by clicking on the Choose a plugin to be reloaded button in the Plugins toolbar and selecting your plugin. If you now click on the Reload Plugin button and the press your plugin button, your new plugin dialog will be displayed.

Implementing plugin functionality

As you have certainly noticed, the layer combobox is still empty. To populate the combobox with a list of loaded layers, we need to add a few lines of code to my_first_plugin.py (located in the plugin folder). More specifically, we expand the run() method:

def run(self):
    """Run method that performs all the real work"""
    # show the dialog
    self.dlg.show()
    # clear the combo box to list only current layers
    self.dlg.layerCombo.clear()
    # get the layers and add them to the combo box
    layers = QgsMapLayerRegistry.instance().mapLayers().values()
    for layer in layers:
        if layer.type() == QgsMapLayer.VectorLayer:
            self.dlg.layerCombo.addItem( layer.name(), layer )
    # Run the dialog event loop
    result = self.dlg.exec_()
    # See if OK was pressed
    if result:
        # Check which layer was selected
        index = self.dlg.layerCombo.currentIndex()
        layer = self.dlg.layerCombo.itemData(index)
        # Display information about the layer
        QMessageBox.information(self.iface.mainWindow(),"Learning QGIS","%s has %d features." %(layer.name(),layer.featureCount()))

You also have to add the following import line at the top of the script to avoid NameErrors concerning QgsMapLayerRegistry and QMessageBox:

from qgis.core import *
from PyQt4.QtGui import QMessageBox

Once you are done with the changes to my_first_plugin.py, you can save the file and use the Reload Plugin button in QGIS to reload your plugin. If you start your plugin now, the combobox will be populated with a list of all layers in the current QGIS project, and when you click on OK, you will see a message box displaying the number of features in the selected layer.

Creating a custom map tool

While the previous exercise showed how to create a custom GUI that enables the user to interact with QGIS, in this exercise, we will go one step further and implement our own custom map tool similar to the default Identify tool. This means that the user can click on the map and the tool reports which feature on the map was clicked on.

To this end, we create another Tool button with dialog plugin template called MyFirstMapTool. For this tool, we do not need to create a dialog. Instead, we have to write a bit more code than we did in the previous example. First, we create our custom map tool class, which we call IdentifyFeatureTool. Besides the __init__() constructor, this tool has a function called canvasReleaseEvent() that defines the actions of the tool when the mouse button is released (that is, when you let go of the mouse button after pressing it):

class IdentifyFeatureTool(QgsMapToolIdentify):
    def __init__(self, canvas):
        QgsMapToolIdentify.__init__(self, canvas)
    def canvasReleaseEvent(self, mouseEvent):
        print "canvasReleaseEvent"
        # get features at the current mouse position
        results = self.identify(mouseEvent.x(),mouseEvent.y(),
                        self.TopDownStopAtFirst, self.VectorLayer)
        if len(results) > 0:
            # signal that a feature was identified
            self.emit( SIGNAL( "geomIdentified" ),
                       results[0].mLayer, results[0].mFeature)

You can paste the preceding code at the end of the my_first_map_tool.py code. Of course, we now have to put our new map tool to good use. In the initGui() function, we replace the run() method with a new map_tool_init() function. Additionally, we define that our map tool is checkable; this means that the user can click on the tool icon to activate it and click on it again to deactivate it:

def initGui(self):
    # create the toolbar icon and menu entry
    icon_path = ':/plugins/MyFirstMapTool/icon.png'
    self.map_tool_action=self.add_action(
        icon_path,
        text=self.tr(u'My 1st Map Tool'),
        callback=self.map_tool_init,
        parent=self.iface.mainWindow())
    self.map_tool_action.setCheckable(True)

The new map_tool_init()function takes care of activating or deactivating our map tool when the button is clicked on. During activation, it creates an instance of our custom IdentifyFeatureTool, and the following line connects the map tool's geomIdentified signal to the do_something() function, which we will discuss in a moment. Similarly, when the map tool is deactivated, we disconnect the signal and restore the previous map tool:

def map_tool_init(self):
    # this function is called when the map tool icon is clicked
    print "maptoolinit"
    canvas = self.iface.mapCanvas()
    if self.map_tool_action.isChecked():
        # when the user activates the tool
        self.prev_tool = canvas.mapTool()
        self.map_tool_action.setChecked( True )
        self.map_tool = IdentifyFeatureTool(canvas)
        QObject.connect(self.map_tool,SIGNAL("geomIdentified"),
                        self.do_something )
        canvas.setMapTool(self.map_tool)
        QObject.connect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                        self.map_tool_changed)
    else:
        # when the user deactivates the tool
        QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"
                                         ),self.map_tool_changed)
        canvas.unsetMapTool(self.map_tool)
        print "restore prev tool %s" %(self.prev_tool)
        canvas.setMapTool(self.prev_tool)

Our new custom do_something() function is called when our map tool is used to successfully identify a feature. For this example, we simply print the feature's attributes on the Python Console. Of course, you can get creative here and add your desired custom functionality:

def do_something(self, layer, feature):
    print feature.attributes()

Finally, we also have to handle the case when the user switches to a different map tool. This is similar to the case of the user deactivating our tool in the map_tool_init() function:

def map_tool_changed(self):
    print "maptoolchanged"
    canvas = self.iface.mapCanvas()
    QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                       self.map_tool_changed)
    canvas.unsetMapTool(self.map_tool)
    self.map_tool_action.setChecked(False)

You also have to add the following import line at the top of the script to avoid errors concerning QObject, QgsMapTool, and others:

from qgis.core import *
from qgis.gui import *
from PyQt4.QtCore import *

When you are ready, you can reload the plugin and try it. You should have the Python Console open to be able to follow the plugin's outputs. The first thing you will see when you activate the plugin in the toolbar is that it prints maptoolinit on the console. Then, if you click on the map, it will print canvasReleaseEvent, and if you click on a feature, it will also display the feature's attributes. Finally, if you change to another map tool (for example, the Pan Map tool) it will print maptoolchanged on the console and the icon in the plugin toolbar will be unchecked.

Creating the plugin template with Plugin Builder

Plugin Builder will create all the files that we need for our plugin. To create a plugin template, follow these steps:

  1. Start Plugin Builder and input the basic plugin information, including:
    • Class name (one word in camel case; that is, each word starts with an upper case letter)
    • Plugin name (a short description)
    • Module name (the Python module name for the plugin)

    When you hover your mouse over the input fields in the Plugin Builder dialog, it displays help information, as shown in the following screenshot:

    Creating the plugin template with Plugin Builder
  2. Click on Next to get to the About dialog, where you can enter a more detailed description of what your plugin does. Since we are planning to create the first plugin for learning purposes only, we can just put some random text here and click on Next.
  3. Now we can select a plugin Template and specify a Text for the menu item as well as which Menu the plugin should be listed in, as shown in the following screenshot. The available templates include Tool button with dialog, Tool button with dock widget, and Processing provider. In this exercise, we'll create a Tool button with dialog and click on Next:
    Creating the plugin template with Plugin Builder
  4. The following dialog presents checkboxes, where we can chose which non-essential plugin files should be created. You can select any subset of the provided options and click on Next.
  5. In the next dialog, we need to specify the plugin Bug tracker and the code Repository. Again, since we are creating this plugin only for learning purposes, I'm just making up some URLs in the next screenshot, but you should use the appropriate trackers and code repositories if you are planning to make your plugin publicly available:
    Creating the plugin template with Plugin Builder
  6. Once you click on Next, you will be asked to select a folder to store the plugin. You can save it directly in the QGIS plugin folder, ~\.qgis2\python\plugins on Windows, or ~/.qgis2/python/plugins on Linux and Mac.
  7. Once you have selected the plugin folder, it displays a Plugin Builder Results confirmation dialog, which confirms the location of your plugin folder as well as the location of your QGIS plugin folder. As mentioned earlier, I saved directly in the QGIS plugin folder, as you can see in the following screenshot. If you have saved in a different location, you can now move the plugin folder into the QGIS plugins folder to make sure that QGIS can find and load it:
    Creating the plugin template with Plugin Builder

One thing we still have to do is prepare the icon for the plugin toolbar. This requires us to compile the resources.qrc file, which Plugin Builder created automatically, to turn the icon into usable Python code. This is done on the command line. On Windows, I recommend using the OSGeo4W shell, because it makes sure that the environment variables are set in such a way that the necessary tools can be found. Navigate to the plugin folder and run this:

pyrcc4 -o resources.py resources.qrc

Tip

You can replace the default icon (icon.png) to add your own plugin icon. Afterwards, you just have to recompile resources_rc.qrc as shown previously.

Restart QGIS and you should now see your plugin listed in the Plugin Manager, as shown here:

Creating the plugin template with Plugin Builder

Activate your plugin in the Plugin Manager and you should see it listed in the Plugins menu. When you start your plugin, it will display a blank dialog that is just waiting for you to customize it.

Customizing the plugin GUI

To customize the blank default plugin dialog, we use Qt Designer. You can find the dialog file in the plugin folder. In my case, it is called my_first_plugin_dialog_base.ui (derived from the module name I specified in Plugin Builder). When you open your plugin's .ui file in Qt Designer, you will see the blank dialog. Now you can start adding widgets by dragging and dropping them from the Widget Box on the left-hand side of the Qt Designer window. In the following screenshot, you can see that I added a Label and a drop-down list widget (listed as Combo Box in the Widgetbox). You can change the label text to Layer by double-clicking on the default label text. Additionally, it is good practice to assign descriptive names to the widget objects; for example, I renamed the combobox to layerCombo, as you can see here in the bottom-right corner:

Customizing the plugin GUI

Once you are finished with the changes to the plugin dialog, you can save them. Then you can go back to QGIS. In QGIS, you can now configure Plugin Reloader by clicking on the Choose a plugin to be reloaded button in the Plugins toolbar and selecting your plugin. If you now click on the Reload Plugin button and the press your plugin button, your new plugin dialog will be displayed.

Implementing plugin functionality

As you have certainly noticed, the layer combobox is still empty. To populate the combobox with a list of loaded layers, we need to add a few lines of code to my_first_plugin.py (located in the plugin folder). More specifically, we expand the run() method:

def run(self):
    """Run method that performs all the real work"""
    # show the dialog
    self.dlg.show()
    # clear the combo box to list only current layers
    self.dlg.layerCombo.clear()
    # get the layers and add them to the combo box
    layers = QgsMapLayerRegistry.instance().mapLayers().values()
    for layer in layers:
        if layer.type() == QgsMapLayer.VectorLayer:
            self.dlg.layerCombo.addItem( layer.name(), layer )
    # Run the dialog event loop
    result = self.dlg.exec_()
    # See if OK was pressed
    if result:
        # Check which layer was selected
        index = self.dlg.layerCombo.currentIndex()
        layer = self.dlg.layerCombo.itemData(index)
        # Display information about the layer
        QMessageBox.information(self.iface.mainWindow(),"Learning QGIS","%s has %d features." %(layer.name(),layer.featureCount()))

You also have to add the following import line at the top of the script to avoid NameErrors concerning QgsMapLayerRegistry and QMessageBox:

from qgis.core import *
from PyQt4.QtGui import QMessageBox

Once you are done with the changes to my_first_plugin.py, you can save the file and use the Reload Plugin button in QGIS to reload your plugin. If you start your plugin now, the combobox will be populated with a list of all layers in the current QGIS project, and when you click on OK, you will see a message box displaying the number of features in the selected layer.

Creating a custom map tool

While the previous exercise showed how to create a custom GUI that enables the user to interact with QGIS, in this exercise, we will go one step further and implement our own custom map tool similar to the default Identify tool. This means that the user can click on the map and the tool reports which feature on the map was clicked on.

To this end, we create another Tool button with dialog plugin template called MyFirstMapTool. For this tool, we do not need to create a dialog. Instead, we have to write a bit more code than we did in the previous example. First, we create our custom map tool class, which we call IdentifyFeatureTool. Besides the __init__() constructor, this tool has a function called canvasReleaseEvent() that defines the actions of the tool when the mouse button is released (that is, when you let go of the mouse button after pressing it):

class IdentifyFeatureTool(QgsMapToolIdentify):
    def __init__(self, canvas):
        QgsMapToolIdentify.__init__(self, canvas)
    def canvasReleaseEvent(self, mouseEvent):
        print "canvasReleaseEvent"
        # get features at the current mouse position
        results = self.identify(mouseEvent.x(),mouseEvent.y(),
                        self.TopDownStopAtFirst, self.VectorLayer)
        if len(results) > 0:
            # signal that a feature was identified
            self.emit( SIGNAL( "geomIdentified" ),
                       results[0].mLayer, results[0].mFeature)

You can paste the preceding code at the end of the my_first_map_tool.py code. Of course, we now have to put our new map tool to good use. In the initGui() function, we replace the run() method with a new map_tool_init() function. Additionally, we define that our map tool is checkable; this means that the user can click on the tool icon to activate it and click on it again to deactivate it:

def initGui(self):
    # create the toolbar icon and menu entry
    icon_path = ':/plugins/MyFirstMapTool/icon.png'
    self.map_tool_action=self.add_action(
        icon_path,
        text=self.tr(u'My 1st Map Tool'),
        callback=self.map_tool_init,
        parent=self.iface.mainWindow())
    self.map_tool_action.setCheckable(True)

The new map_tool_init()function takes care of activating or deactivating our map tool when the button is clicked on. During activation, it creates an instance of our custom IdentifyFeatureTool, and the following line connects the map tool's geomIdentified signal to the do_something() function, which we will discuss in a moment. Similarly, when the map tool is deactivated, we disconnect the signal and restore the previous map tool:

def map_tool_init(self):
    # this function is called when the map tool icon is clicked
    print "maptoolinit"
    canvas = self.iface.mapCanvas()
    if self.map_tool_action.isChecked():
        # when the user activates the tool
        self.prev_tool = canvas.mapTool()
        self.map_tool_action.setChecked( True )
        self.map_tool = IdentifyFeatureTool(canvas)
        QObject.connect(self.map_tool,SIGNAL("geomIdentified"),
                        self.do_something )
        canvas.setMapTool(self.map_tool)
        QObject.connect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                        self.map_tool_changed)
    else:
        # when the user deactivates the tool
        QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"
                                         ),self.map_tool_changed)
        canvas.unsetMapTool(self.map_tool)
        print "restore prev tool %s" %(self.prev_tool)
        canvas.setMapTool(self.prev_tool)

Our new custom do_something() function is called when our map tool is used to successfully identify a feature. For this example, we simply print the feature's attributes on the Python Console. Of course, you can get creative here and add your desired custom functionality:

def do_something(self, layer, feature):
    print feature.attributes()

Finally, we also have to handle the case when the user switches to a different map tool. This is similar to the case of the user deactivating our tool in the map_tool_init() function:

def map_tool_changed(self):
    print "maptoolchanged"
    canvas = self.iface.mapCanvas()
    QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                       self.map_tool_changed)
    canvas.unsetMapTool(self.map_tool)
    self.map_tool_action.setChecked(False)

You also have to add the following import line at the top of the script to avoid errors concerning QObject, QgsMapTool, and others:

from qgis.core import *
from qgis.gui import *
from PyQt4.QtCore import *

When you are ready, you can reload the plugin and try it. You should have the Python Console open to be able to follow the plugin's outputs. The first thing you will see when you activate the plugin in the toolbar is that it prints maptoolinit on the console. Then, if you click on the map, it will print canvasReleaseEvent, and if you click on a feature, it will also display the feature's attributes. Finally, if you change to another map tool (for example, the Pan Map tool) it will print maptoolchanged on the console and the icon in the plugin toolbar will be unchecked.

Customizing the plugin GUI

To customize the blank default plugin dialog, we use Qt Designer. You can find the dialog file in the plugin folder. In my case, it is called my_first_plugin_dialog_base.ui (derived from the module name I specified in Plugin Builder). When you open your plugin's .ui file in Qt Designer, you will see the blank dialog. Now you can start adding widgets by dragging and dropping them from the Widget Box on the left-hand side of the Qt Designer window. In the following screenshot, you can see that I added a Label and a drop-down list widget (listed as Combo Box in the Widgetbox). You can change the label text to Layer by double-clicking on the default label text. Additionally, it is good practice to assign descriptive names to the widget objects; for example, I renamed the combobox to layerCombo, as you can see here in the bottom-right corner:

Customizing the plugin GUI

Once you are finished with the changes to the plugin dialog, you can save them. Then you can go back to QGIS. In QGIS, you can now configure Plugin Reloader by clicking on the Choose a plugin to be reloaded button in the Plugins toolbar and selecting your plugin. If you now click on the Reload Plugin button and the press your plugin button, your new plugin dialog will be displayed.

Implementing plugin functionality

As you have certainly noticed, the layer combobox is still empty. To populate the combobox with a list of loaded layers, we need to add a few lines of code to my_first_plugin.py (located in the plugin folder). More specifically, we expand the run() method:

def run(self):
    """Run method that performs all the real work"""
    # show the dialog
    self.dlg.show()
    # clear the combo box to list only current layers
    self.dlg.layerCombo.clear()
    # get the layers and add them to the combo box
    layers = QgsMapLayerRegistry.instance().mapLayers().values()
    for layer in layers:
        if layer.type() == QgsMapLayer.VectorLayer:
            self.dlg.layerCombo.addItem( layer.name(), layer )
    # Run the dialog event loop
    result = self.dlg.exec_()
    # See if OK was pressed
    if result:
        # Check which layer was selected
        index = self.dlg.layerCombo.currentIndex()
        layer = self.dlg.layerCombo.itemData(index)
        # Display information about the layer
        QMessageBox.information(self.iface.mainWindow(),"Learning QGIS","%s has %d features." %(layer.name(),layer.featureCount()))

You also have to add the following import line at the top of the script to avoid NameErrors concerning QgsMapLayerRegistry and QMessageBox:

from qgis.core import *
from PyQt4.QtGui import QMessageBox

Once you are done with the changes to my_first_plugin.py, you can save the file and use the Reload Plugin button in QGIS to reload your plugin. If you start your plugin now, the combobox will be populated with a list of all layers in the current QGIS project, and when you click on OK, you will see a message box displaying the number of features in the selected layer.

Creating a custom map tool

While the previous exercise showed how to create a custom GUI that enables the user to interact with QGIS, in this exercise, we will go one step further and implement our own custom map tool similar to the default Identify tool. This means that the user can click on the map and the tool reports which feature on the map was clicked on.

To this end, we create another Tool button with dialog plugin template called MyFirstMapTool. For this tool, we do not need to create a dialog. Instead, we have to write a bit more code than we did in the previous example. First, we create our custom map tool class, which we call IdentifyFeatureTool. Besides the __init__() constructor, this tool has a function called canvasReleaseEvent() that defines the actions of the tool when the mouse button is released (that is, when you let go of the mouse button after pressing it):

class IdentifyFeatureTool(QgsMapToolIdentify):
    def __init__(self, canvas):
        QgsMapToolIdentify.__init__(self, canvas)
    def canvasReleaseEvent(self, mouseEvent):
        print "canvasReleaseEvent"
        # get features at the current mouse position
        results = self.identify(mouseEvent.x(),mouseEvent.y(),
                        self.TopDownStopAtFirst, self.VectorLayer)
        if len(results) > 0:
            # signal that a feature was identified
            self.emit( SIGNAL( "geomIdentified" ),
                       results[0].mLayer, results[0].mFeature)

You can paste the preceding code at the end of the my_first_map_tool.py code. Of course, we now have to put our new map tool to good use. In the initGui() function, we replace the run() method with a new map_tool_init() function. Additionally, we define that our map tool is checkable; this means that the user can click on the tool icon to activate it and click on it again to deactivate it:

def initGui(self):
    # create the toolbar icon and menu entry
    icon_path = ':/plugins/MyFirstMapTool/icon.png'
    self.map_tool_action=self.add_action(
        icon_path,
        text=self.tr(u'My 1st Map Tool'),
        callback=self.map_tool_init,
        parent=self.iface.mainWindow())
    self.map_tool_action.setCheckable(True)

The new map_tool_init()function takes care of activating or deactivating our map tool when the button is clicked on. During activation, it creates an instance of our custom IdentifyFeatureTool, and the following line connects the map tool's geomIdentified signal to the do_something() function, which we will discuss in a moment. Similarly, when the map tool is deactivated, we disconnect the signal and restore the previous map tool:

def map_tool_init(self):
    # this function is called when the map tool icon is clicked
    print "maptoolinit"
    canvas = self.iface.mapCanvas()
    if self.map_tool_action.isChecked():
        # when the user activates the tool
        self.prev_tool = canvas.mapTool()
        self.map_tool_action.setChecked( True )
        self.map_tool = IdentifyFeatureTool(canvas)
        QObject.connect(self.map_tool,SIGNAL("geomIdentified"),
                        self.do_something )
        canvas.setMapTool(self.map_tool)
        QObject.connect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                        self.map_tool_changed)
    else:
        # when the user deactivates the tool
        QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"
                                         ),self.map_tool_changed)
        canvas.unsetMapTool(self.map_tool)
        print "restore prev tool %s" %(self.prev_tool)
        canvas.setMapTool(self.prev_tool)

Our new custom do_something() function is called when our map tool is used to successfully identify a feature. For this example, we simply print the feature's attributes on the Python Console. Of course, you can get creative here and add your desired custom functionality:

def do_something(self, layer, feature):
    print feature.attributes()

Finally, we also have to handle the case when the user switches to a different map tool. This is similar to the case of the user deactivating our tool in the map_tool_init() function:

def map_tool_changed(self):
    print "maptoolchanged"
    canvas = self.iface.mapCanvas()
    QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                       self.map_tool_changed)
    canvas.unsetMapTool(self.map_tool)
    self.map_tool_action.setChecked(False)

You also have to add the following import line at the top of the script to avoid errors concerning QObject, QgsMapTool, and others:

from qgis.core import *
from qgis.gui import *
from PyQt4.QtCore import *

When you are ready, you can reload the plugin and try it. You should have the Python Console open to be able to follow the plugin's outputs. The first thing you will see when you activate the plugin in the toolbar is that it prints maptoolinit on the console. Then, if you click on the map, it will print canvasReleaseEvent, and if you click on a feature, it will also display the feature's attributes. Finally, if you change to another map tool (for example, the Pan Map tool) it will print maptoolchanged on the console and the icon in the plugin toolbar will be unchecked.

Implementing plugin functionality

As you have certainly noticed, the layer combobox is still empty. To populate the combobox with a list of loaded layers, we need to add a few lines of code to my_first_plugin.py (located in the plugin folder). More specifically, we expand the run() method:

def run(self):
    """Run method that performs all the real work"""
    # show the dialog
    self.dlg.show()
    # clear the combo box to list only current layers
    self.dlg.layerCombo.clear()
    # get the layers and add them to the combo box
    layers = QgsMapLayerRegistry.instance().mapLayers().values()
    for layer in layers:
        if layer.type() == QgsMapLayer.VectorLayer:
            self.dlg.layerCombo.addItem( layer.name(), layer )
    # Run the dialog event loop
    result = self.dlg.exec_()
    # See if OK was pressed
    if result:
        # Check which layer was selected
        index = self.dlg.layerCombo.currentIndex()
        layer = self.dlg.layerCombo.itemData(index)
        # Display information about the layer
        QMessageBox.information(self.iface.mainWindow(),"Learning QGIS","%s has %d features." %(layer.name(),layer.featureCount()))

You also have to add the following import line at the top of the script to avoid NameErrors concerning QgsMapLayerRegistry and QMessageBox:

from qgis.core import *
from PyQt4.QtGui import QMessageBox

Once you are done with the changes to my_first_plugin.py, you can save the file and use the Reload Plugin button in QGIS to reload your plugin. If you start your plugin now, the combobox will be populated with a list of all layers in the current QGIS project, and when you click on OK, you will see a message box displaying the number of features in the selected layer.

Creating a custom map tool

While the previous exercise showed how to create a custom GUI that enables the user to interact with QGIS, in this exercise, we will go one step further and implement our own custom map tool similar to the default Identify tool. This means that the user can click on the map and the tool reports which feature on the map was clicked on.

To this end, we create another Tool button with dialog plugin template called MyFirstMapTool. For this tool, we do not need to create a dialog. Instead, we have to write a bit more code than we did in the previous example. First, we create our custom map tool class, which we call IdentifyFeatureTool. Besides the __init__() constructor, this tool has a function called canvasReleaseEvent() that defines the actions of the tool when the mouse button is released (that is, when you let go of the mouse button after pressing it):

class IdentifyFeatureTool(QgsMapToolIdentify):
    def __init__(self, canvas):
        QgsMapToolIdentify.__init__(self, canvas)
    def canvasReleaseEvent(self, mouseEvent):
        print "canvasReleaseEvent"
        # get features at the current mouse position
        results = self.identify(mouseEvent.x(),mouseEvent.y(),
                        self.TopDownStopAtFirst, self.VectorLayer)
        if len(results) > 0:
            # signal that a feature was identified
            self.emit( SIGNAL( "geomIdentified" ),
                       results[0].mLayer, results[0].mFeature)

You can paste the preceding code at the end of the my_first_map_tool.py code. Of course, we now have to put our new map tool to good use. In the initGui() function, we replace the run() method with a new map_tool_init() function. Additionally, we define that our map tool is checkable; this means that the user can click on the tool icon to activate it and click on it again to deactivate it:

def initGui(self):
    # create the toolbar icon and menu entry
    icon_path = ':/plugins/MyFirstMapTool/icon.png'
    self.map_tool_action=self.add_action(
        icon_path,
        text=self.tr(u'My 1st Map Tool'),
        callback=self.map_tool_init,
        parent=self.iface.mainWindow())
    self.map_tool_action.setCheckable(True)

The new map_tool_init()function takes care of activating or deactivating our map tool when the button is clicked on. During activation, it creates an instance of our custom IdentifyFeatureTool, and the following line connects the map tool's geomIdentified signal to the do_something() function, which we will discuss in a moment. Similarly, when the map tool is deactivated, we disconnect the signal and restore the previous map tool:

def map_tool_init(self):
    # this function is called when the map tool icon is clicked
    print "maptoolinit"
    canvas = self.iface.mapCanvas()
    if self.map_tool_action.isChecked():
        # when the user activates the tool
        self.prev_tool = canvas.mapTool()
        self.map_tool_action.setChecked( True )
        self.map_tool = IdentifyFeatureTool(canvas)
        QObject.connect(self.map_tool,SIGNAL("geomIdentified"),
                        self.do_something )
        canvas.setMapTool(self.map_tool)
        QObject.connect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                        self.map_tool_changed)
    else:
        # when the user deactivates the tool
        QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"
                                         ),self.map_tool_changed)
        canvas.unsetMapTool(self.map_tool)
        print "restore prev tool %s" %(self.prev_tool)
        canvas.setMapTool(self.prev_tool)

Our new custom do_something() function is called when our map tool is used to successfully identify a feature. For this example, we simply print the feature's attributes on the Python Console. Of course, you can get creative here and add your desired custom functionality:

def do_something(self, layer, feature):
    print feature.attributes()

Finally, we also have to handle the case when the user switches to a different map tool. This is similar to the case of the user deactivating our tool in the map_tool_init() function:

def map_tool_changed(self):
    print "maptoolchanged"
    canvas = self.iface.mapCanvas()
    QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                       self.map_tool_changed)
    canvas.unsetMapTool(self.map_tool)
    self.map_tool_action.setChecked(False)

You also have to add the following import line at the top of the script to avoid errors concerning QObject, QgsMapTool, and others:

from qgis.core import *
from qgis.gui import *
from PyQt4.QtCore import *

When you are ready, you can reload the plugin and try it. You should have the Python Console open to be able to follow the plugin's outputs. The first thing you will see when you activate the plugin in the toolbar is that it prints maptoolinit on the console. Then, if you click on the map, it will print canvasReleaseEvent, and if you click on a feature, it will also display the feature's attributes. Finally, if you change to another map tool (for example, the Pan Map tool) it will print maptoolchanged on the console and the icon in the plugin toolbar will be unchecked.

Creating a custom map tool

While the previous exercise showed how to create a custom GUI that enables the user to interact with QGIS, in this exercise, we will go one step further and implement our own custom map tool similar to the default Identify tool. This means that the user can click on the map and the tool reports which feature on the map was clicked on.

To this end, we create another Tool button with dialog plugin template called MyFirstMapTool. For this tool, we do not need to create a dialog. Instead, we have to write a bit more code than we did in the previous example. First, we create our custom map tool class, which we call IdentifyFeatureTool. Besides the __init__() constructor, this tool has a function called canvasReleaseEvent() that defines the actions of the tool when the mouse button is released (that is, when you let go of the mouse button after pressing it):

class IdentifyFeatureTool(QgsMapToolIdentify):
    def __init__(self, canvas):
        QgsMapToolIdentify.__init__(self, canvas)
    def canvasReleaseEvent(self, mouseEvent):
        print "canvasReleaseEvent"
        # get features at the current mouse position
        results = self.identify(mouseEvent.x(),mouseEvent.y(),
                        self.TopDownStopAtFirst, self.VectorLayer)
        if len(results) > 0:
            # signal that a feature was identified
            self.emit( SIGNAL( "geomIdentified" ),
                       results[0].mLayer, results[0].mFeature)

You can paste the preceding code at the end of the my_first_map_tool.py code. Of course, we now have to put our new map tool to good use. In the initGui() function, we replace the run() method with a new map_tool_init() function. Additionally, we define that our map tool is checkable; this means that the user can click on the tool icon to activate it and click on it again to deactivate it:

def initGui(self):
    # create the toolbar icon and menu entry
    icon_path = ':/plugins/MyFirstMapTool/icon.png'
    self.map_tool_action=self.add_action(
        icon_path,
        text=self.tr(u'My 1st Map Tool'),
        callback=self.map_tool_init,
        parent=self.iface.mainWindow())
    self.map_tool_action.setCheckable(True)

The new map_tool_init()function takes care of activating or deactivating our map tool when the button is clicked on. During activation, it creates an instance of our custom IdentifyFeatureTool, and the following line connects the map tool's geomIdentified signal to the do_something() function, which we will discuss in a moment. Similarly, when the map tool is deactivated, we disconnect the signal and restore the previous map tool:

def map_tool_init(self):
    # this function is called when the map tool icon is clicked
    print "maptoolinit"
    canvas = self.iface.mapCanvas()
    if self.map_tool_action.isChecked():
        # when the user activates the tool
        self.prev_tool = canvas.mapTool()
        self.map_tool_action.setChecked( True )
        self.map_tool = IdentifyFeatureTool(canvas)
        QObject.connect(self.map_tool,SIGNAL("geomIdentified"),
                        self.do_something )
        canvas.setMapTool(self.map_tool)
        QObject.connect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                        self.map_tool_changed)
    else:
        # when the user deactivates the tool
        QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"
                                         ),self.map_tool_changed)
        canvas.unsetMapTool(self.map_tool)
        print "restore prev tool %s" %(self.prev_tool)
        canvas.setMapTool(self.prev_tool)

Our new custom do_something() function is called when our map tool is used to successfully identify a feature. For this example, we simply print the feature's attributes on the Python Console. Of course, you can get creative here and add your desired custom functionality:

def do_something(self, layer, feature):
    print feature.attributes()

Finally, we also have to handle the case when the user switches to a different map tool. This is similar to the case of the user deactivating our tool in the map_tool_init() function:

def map_tool_changed(self):
    print "maptoolchanged"
    canvas = self.iface.mapCanvas()
    QObject.disconnect(canvas,SIGNAL("mapToolSet(QgsMapTool *)"),
                       self.map_tool_changed)
    canvas.unsetMapTool(self.map_tool)
    self.map_tool_action.setChecked(False)

You also have to add the following import line at the top of the script to avoid errors concerning QObject, QgsMapTool, and others:

from qgis.core import *
from qgis.gui import *
from PyQt4.QtCore import *

When you are ready, you can reload the plugin and try it. You should have the Python Console open to be able to follow the plugin's outputs. The first thing you will see when you activate the plugin in the toolbar is that it prints maptoolinit on the console. Then, if you click on the map, it will print canvasReleaseEvent, and if you click on a feature, it will also display the feature's attributes. Finally, if you change to another map tool (for example, the Pan Map tool) it will print maptoolchanged on the console and the icon in the plugin toolbar will be unchecked.

Summary

In this chapter, we covered the different ways to extend QGIS using actions and Python scripting. We started with different types of actions and then continued to the Python Console, which offers a direct, interactive way to interact with the QGIS Python API. We also used the editor that is part of the Python Console panel and provides a better way to work on longer scripts containing loops or even multiple class and function definitions. Next, we applied our knowledge of PyQGIS to develop custom tools for the Processing Toolbox. These tools profit from Processing's automatic GUI generation capabilities, and they can be used in Graphical modeler to create geopreocessing models. Last but not least, we developed a basic plugin based on a Plugin Builder template.

With this background knowledge, you can now start your own PyQGIS experiments. There are several web and print resources that you can use to learn more about QGIS Python scripting. For the updated QGIS API documentation, check out http://qgis.org/api/. If you are interested in more PyQGIS recipes, take a look at PyQGIS Developer Cookbook at http://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook and QGIS programming books offered by Packt Publishing, as well as Gary Sherman's book The PyQGIS Programmer's Guide, Locate Press.

You have been reading a chapter from
QGIS:Becoming a GIS Power User
Published in: Feb 2017
Publisher: Packt
ISBN-13: 9781788299725
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime