-
Notifications
You must be signed in to change notification settings - Fork 2
Developer's Tutorial
This document is an introduction to developing analytical backends or single modules for the QGIS Processing Framework, aimed at developers. It is work in progress, may contain errors and certainly many omissions. If you have questions, please contact me at cpolymeris@gmail.com
The QGIS processing framework is written in python and consists of the following parts:
- The framework itself, also known as processing core, which resides in
python/processing
. - The processing manager, implemented as a QGIS plugin, resides in
python/plugins/processingplugin
, responsible for the GUI side of things, like displaying the module tree panel, and module execution dialogs. - The analytical backends, also implemented as plugins, like the SAGA interface in
python/plugins/saga
It is the first part's API we will be mostly concerned with in this tutorial, but because we are implementing a QGIS plugin, you should be familiar with their development and refer to the general QGIS API, when necessary.
Our first module will be called GDAL algorithms demo and will implement one of the GDAL tools, contour, as an example.
The module's __init__.py
file is easy, it will look like any other QGIS plugin:
def name():
return "GDAL algorithms demo"
def description():
return "Makes a GDAL algorithm available through the processing framework."
def version():
return "Version 0.1"
def qgisMinimumVersion():
return "1.0"
def authorName():
return "Camilo Polymeris"
def classFactory(iface):
import gdalalgorithms
return gdalalgorithms.Plugin(iface)
Implementation of the plugin's __init__
and __unload__
methods in gdalalgorithms.py
is also straightfoward, we actually do nothing. That is because, in general, we let the processing manager plugin handle GUI stuff, as said above.
class Plugin:
def __init__(self, iface):
# save reference to the QGIS interface
self.iface = iface
def unload(self):
pass
def initGui(self):
#TODO: implement this method
pass
It is with the initGui
& modules
methods that things start to get interesting. Despite the name of the former method, we again do nothing GUI related, but instead initialize our sample module. Let's first add the required imports to the top of gdalalgorithms.py
:
import processing
from processing.parameters import *
from osgeo import gdal
The modules
method should simply return a set of modules, that is instances of the processing.Module
class, which contain metadata about each module, like name, description, tags and parameters. In this case, because we are defining only one module, we write:
def modules(self):
return [self.contourModule]
It is in the initGui
method that we create this single module. We will implement the class later.
def initGui(self):
self.contourModule = ContourModule()
And then register our class with the framework:
processing.framework.registerModuleProvider(self)
By now, your gdalalgorithms.py
should look like this:
import processing
class Plugin:
def __init__(self, iface):
# save reference to the QGIS interface
self.iface = iface
def unload(self):
pass
def modules(self):
return [self.contourModule]
def initGui(self):
self.contourModule = ContourModule()
processing.framework.registerModuleProvider(self)
Now, that we have the basic module skeleton, we'll have to decide on an approach to implement our specific module. While using the QGIS Processing Framework, you will often have the chance to use either the provided classes (Module
, ModuleInstance
, Parameter
and its subclasses), or implement your own.
In this example, I will subclass Module
and ModuleInstance
, to show you how it could be done, but keep in mind that this is not always necessary. It depends on how much you want to customize the behaviour of your backend.
So, what are Modules
and ModuleInstances
?, and what is the difference?
You can see the former as containers of metadata of modules and the later as actual modules preparing to run, running or stopped. That is, each object of type Module
corresponds to a module with different functionality, while two objects of type ModuleInstance
may have the same functionality and only differ in the values of parameters. Of course, a Module object is associated to each ModuleInstance
object.
This distinction will become more apparent when we implement these classes, starting with ContourModule
.
This module will have only two parameters: An input raster layer and an output vector layer. These are built-in parameter types, defined in processing.paramters
We create these parameters in the initializer:
class ContourModule(processing.Module):
def __init__(self):
self.inParam = RasterLayerParameter("Input raster")
self.inParam.setRole(Parameter.Role.input)
self.outParam = VectorLayerParameter("Output layer")
self.outParam.setRole(Parameter.Role.output)
Easy, huh? There are more types of parameters, of course, and more options, but for now it will suffice.
Next we just call the parent __init__
method (or use super()
, if you prefer).
processing.Module.__init__(self, "Contour",
description = "GDAL demo module",
parameters = [self.inParam, self.outParam], tags = ["gdal"])
Each Module subclass should implement an instance
method, to return the right type of ModuleInstance
. The ModuleInstance
class is very generic, and while it has access to the module's parameters, of course, since we are dealing with a specific module, we'll pass them manually.
def instance(self):
return ContourModuleInstance(self, self.inParam, self.outParam)
The code for ContourModuleInstance
is only a bit longer. The init method is fairly self-explanatory, except for the last line:
class ContourModuleInstance(processing.ModuleInstance):
def __init__(self, module, inParam, outParam):
self.inParam = inParam
self.outParam = outParam
processing.ModuleInstance.__init__(self, module)
QObject.connect(self,
self.valueChangedSignal(self.stateParameter),
self.onStateParameterChanged)
The ModuleInstance.valueChangedSignal(parameter)
returns the Qt signal that is emitted when the parameters value changes. We are passing the pseudo-parameter stateParameter
to the method, to know when the module's state changes from stopped to running.
We then connect this signal to the self.onStateParameterChanged
slot, which we'll define next:
def onStateParameterChanged(self, state):
if state == StateParameter.State.running:
inLayer = self[self.inParam]
outLayer = self[self.outParam]
self.contour(inLayer, outLayer)
self.setState(StateParameter.State.stopped)
The parameter's value is passed to the slot, so we use this to determine if we are running the module, in which case we call self.contour
, method that does the actual work. When this method returns we set the state to stopped, again.
Note that you could use this signals & slots mechanism to react to any change in parameter values, not just state. This allows you to e.g. modify your module on-the-fly or give feedback to the user.
Finally, we just have to implement the ContourModuleInstance.contour(in, out)
method. I have just adapted the code from a script in GDAL's svn repository: