QGIS Python code tips🔗
Introduction
At work, I spent time to develop a QGIS extension in order to make QGIS easier to use. This extension is coded in Python and try to minimise the QGis interface and add two more features: an interface to filter a city for the current view range and a radius selection tool (which displays the radius size). Coding this extension was quite easy thanks to QGIS and it's Python APIs. Much of my time was documentation reading and exemple searches.
Now that the extension is a good prototype, I think I can share some tips I've found to fix common problems under QGIS. So here are my tips…
Tip 0: Documentation
I guess this is something where every project should start: add some links to the online APIs documentation to your web browser bookmarks. For QGIS, you need to point to the API documentation and to PyQt Class Reference because a lot of objects are managed with Qt Bindings.
The QGIS API documentation is well designed (thanks to Doxygen): the class list is a good start (eg: the gui library). You can find nearly everything you want by starting your search from there.
Tip 1: How to manage toolbars
When you want to deal with QGIS toolbars, you first need to read the QgsInterface and the QToolbar documentation. QgsInterface is a Python object from QGIS.gui which manage the QGIS Interface the user interacts with. From this object, you can access to most of the QtObjects inside the user interface. For those tips, you need to learn about the QToolbar objects.
Remove a complete toolbar (eg: the File Toolbar)
# QGIS Toolbars are referenced as objects named under the QgsInterface object. # Consult the QgsInterface documentation to find all the names: # The object self.iface is a QgsInterface from the start of you plugin. # First, we get the Toolbar dedicated to File manipulation: toolbar = self.iface.fileToolBar() # Then, we get the parent of this toolbar and remove its child. parent = toolbar.parentWidget() parent.removeToolBar(toolbar)
Add an action from a toolbar
# You have to define an action (consult QAction doc) and modify or add some of its attributes: self.selC = QAction(QIcon(":/plugins/permanence/mActionSelectRadius.png"), \ u"Sélection par cercle", self.iface.mainWindow()) self.selC.setDisabled(True) # Then, you add this action to the toolbar of your choice: self.iface.attributesToolBar().addAction(self.selC)
Removing an action from a toolbar
# You have to save all of the actions from the toolbar actions = self.iface.attributesToolBar().actions() # then, you clear the complete toolbar self.iface.attributesToolBar().clear() # and you re-add only the actions yo uwant self.iface.attributesToolBar().addAction(actions[3]) self.iface.attributesToolBar().addAction(actions[4]) … # or some other actions: self.iface.attributesToolBar().addAction(another_action)
Tip 2: Howto manage menus
Same deal: use QgsInterface to access the menu objects. You need to learn about (Qt) QMenu objects.
Remove a complete menu
You could think that typing: menu.set_visible(False)
should do the trick… but it doesn't work. So here is what I found:
# you have to get a QMenu object from QgsInterface (let's say: file Menu) menu = self.iface.fileMenu() # Then you get its parent menubar = menu.parentWidget() # and remove the entire child QMenu QAction: menubar.removeAction(menu.menuAction())
Add a complete menu to the extension menu
The extension menu is a QMenu dedicated to plugins. You can create your own menus and add them to the global menu bar like the menu of fTools. But it is easier to start with the pluginMenu.
# You have to create some QAction which will be the last menu levels: # A first action: self.action = QAction(QIcon(":/plugins/permanence/icon.png"), u"Choisir la commune et les thèmes…", self.iface.mainWindow()) QObject.connect(self.action, SIGNAL("triggered()"), self.choix_commune) # a second one: self.param = QAction(u"Paramétrages…", self.iface.mainWindow()) QObject.connect(self.param, SIGNAL("triggered()"), self.afficheParam) # Create an empty menu… self.menu = QMenu() self.menu.setTitle(u"&Your specific Menu…") # Add the actions to this menu: self.menu.addActions([self.action, self.param]) # Add the menu to the Plugin Menu self.iface.pluginMenu().addMenu(self.menu)
Tip 3: Howto to load a project file
Loading a project file is something that is very easy… But I've noticed that when you load a project with a lot of layers or groups, it can take minutes to open ! In order to make it faster, you have to disable the rendering during the load and to enable it once the project is opened.
# You start by referencing the actual project instance project = QgsProject.instance() # Then you disable the rendering operations: self.iface.mapCanvas().setRenderFlag(False) # So you can open the project file faster: projet.read(QFileInfo('/somewhere/the_project.qgs')) # Never forget to re-enable the rendering… self.iface.mapCanvas().setRenderFlag(True) # At this point, the rendering should be launched.
Tip 4: Howto to manage layers with LegendInterface
From the QgsInterface, you can get a QgsLegendInterface object. This one manages the legend (the list of layers and groups that are shown or not). You can also learn about the layers from the QgsMapLayerRegistry object.
Get the list of layers
# Get the legend object from QgsInterface: legend = self.iface.legendInterface() # You can use QgsMapLayerRegistry.instance() object: # its mapLayers() object is an iterable dict you can use with for instructions: layermaps = QgsMapLayerRegistry.instance().mapLayers() # then you get a dict object: name <--> layer for name, layer in layermaps.iteritems(): # If you want to get the type of the layer (vector or raster), use: if layer.type() == QgsMapLayer.VectorLayer: # If you want to know if the layer is visible or not user: if legend.isLayerVisible(layer):
Get and set visible some groups of layers
# Get the legend and its groups: legend = self.iface.legendInterface() groups = legend.groups() # Loop to enable all the groups: for i in range(len(groups)): legend.setGroupVisible(i+1, True)
Tip 5: Zoom to the extent of an object
In my extension, I made a dialog to choose an object to zoom on. It is the same thing than the Attribute Table "Show selection" tool. But it is much more simple to use. Sometimes, QGIS doesn't make the rendering even if the extent of the current view has changed. You have to add a line to be sure that it do it by calling a re-rendering once the new extent is done.
# We have to find the good layer (its name is in the layer_city_name variable): # This code is much simpler if you already have the QgsLayerMap() object. layermaps = QgsMapLayerRegistry.instance().mapLayers() for name, layer in layermaps.iteritems(): if layer.name() == self.layer_city_name: city_layer = layer # Then we can consider that we have only the name of the city that has been selected. # We need its extent. This information is simply get from the extent of its geometry. # So we have to make something like a query to find the city object that matches or name. provider = city_layer.dataProvider() feat = QgsFeature() allAttrs = provider.attributeIndexes() provider.select(allAttrs) # Loop on all the name attributes of all of the objects from the layer: # The first loop is: extract all the objects from the layer: while provider.nextFeature(feat): # fetch map of attributes attrs = feat.attributeMap() # Second loop: look into the object attributes. # Name is the second attribute (so number 1 as attribute count start from 0) # in our layer (it could be something else for another layer: just a convention for the example). for (k,attr) in attrs.iteritems(): # the name of the city is kept in city_name variable if k == 1 and str(attr.toString()) == str(self.city_name): city_extent = feat.geometry().boundingBox() # With the extent, it is very easy to zoom on the city: self.iface.mapCanvas().setExtent(city_extent) # And never forget to refresh the canvas to re-render as said before: self.iface.mapCanvas().refresh()
Tip 6: Manage rubberband
QgsRubberband objects can be considered as geometrical objects (like any polygon in QGIS) on a dedicated and independant (non named) layer for drawing purposes. You can use those objects to deal with selection. When you use a selection object, you draw a geometry (sometime special like a circle) to select the objects that intersects with.
But you have to add some code to start the selection and to change the selection tool before.
Howto Make a circle selection tool with rubberband
(Taken from some piece of code from SelectPlus plugin).
# Create an action to start the selection process: self.selC = QAction(QIcon(":/plugins/permanence/mActionSelectRadius.png"), \ u"Sélection par cercle", self.iface.mainWindow()) # Link it to a trigger function (self.selectCircle in our case): QObject.connect(self.selC, SIGNAL("triggered()"), self.selectCircle) # … Add this action to a toolbar or a menu like shown above… # Here is the trigger function: def selectCircle(self): # Our tool is an elaborated object that uses QgsRubberband as shown below) self.tool = selectTools.selectCircle(self.iface) # When this object emits the signal "selectionDone()", # it means that the selection is done and that we have to do something (with # the do_something function) self.iface.connect(self.tool, SIGNAL("selectionDone()"), self.do_something) # We indicate to the interface that the tool that is activated is our special selection object. self.iface.mapCanvas().setMapTool(self.tool) # … On another python file (called selectTools.py which store ours selection class and functions)… # Here is our special object: selectCircle. class selectCircle(QgsMapTool): # The constructor starts with an empty object: def __init__(self,iface, couleur, largeur, cercle): canvas = iface.mapCanvas() QgsMapTool.__init__(self,canvas) self.canvas = canvas self.iface = iface self.status = 0 # Number of segments for the circle self.cercle = 30 # Our QgsRubberband: self.rb=QgsRubberBand(self.canvas,True) self.rb.setColor( Qt.Red ) self.rb.setWidth( 2 ) sb = self.iface.mainWindow().statusBar() sb.showMessage(u"You are drawing a circle") return None # When you press the mouse button the following is launched: # Actually, it means that we start the drawing work def canvasPressEvent(self,e): if not e.button() == Qt.LeftButton: return self.status = 1 self.center = self.toMapCoordinates(e.pos()) # The rbcircle function put a circle to the QgsRubberBand geometry: # for the moment, the radius is null ! rbcircle(self.rb, self.center, self.center, self.cercle) return # When you move your mouse, the following is launched: def canvasMoveEvent(self,e): # If you are not drawing a circle, nothing appears ! if not self.status == 1: return # else, construct a circle with N segments cp = self.toMapCoordinates(e.pos()) rbcircle(self.rb, self.center, cp, self.cercle) r = sqrt(self.center.sqrDist(cp)) # We add the radius and X/Y center coordinates in the status bar: sb = self.iface.mainWindow().statusBar() sb.showMessage(u"Centre: X=%s Y=%s, RADIUS: %s m" % (str(self.center.x()),str(self.center.y()),str(r))) self.rb.show() def canvasReleaseEvent(self,e): if not e.button() == Qt.LeftButton: return self.emit( SIGNAL("selectionDone()") ) def reset(self): self.status = 0 self.rb.reset( True ) def deactivate(self): QgsMapTool.deactivate(self) self.emit(SIGNAL("deactivated()")) # Here is our circle computation function: an algorithm to build a circle with two points. def rbcircle(rb,center,edgePoint,N): r = sqrt(center.sqrDist(edgePoint)) rb.reset( True ) for itheta in range(N+1): theta = itheta*(2.0 * pi/N) # You see that only the QgsRubberband geometry is modified rb.addPoint(QgsPoint(center.x()+r*cos(theta),center.y()+r*sin(theta))) return
Tip 7: Export results to CSV
It is much more a Python tip than a QGIS one. But you can use the csv module from Python to export QGIS attribute selection to a csv file. But this shows howto extract the attributes of the selection from the layer
# Preliminaries: import the module, a layer, its data provider, an empty feature, a selection geometry import csv provider = layer.dataProvider() feat = QgsFeature() g = a_rubber_band.asGeometry() # Then, open a file and make it a csv file object: f = open('./a_csv_file', 'wb') csv_file = csv.writer(f) # We want to extract the CSV header line from the layer fields name: header = [field.name().toUtf8() for field in provider.fields().values()] csv_file.writerow(header) # We must verify for all the layer objects that they intersect or not with the previous geometry… # So, we scan all the layer object: while provider.nextFeature(feat): # Get the object geometry geom = feat.geometry() # and test if it intersects the rubberband geometry (g): if geom.intersects(g): attrs = feat.attributeMap() # Then we extract the current object fields values… b = [attr.toString().toUtf8() for attr in attrs.values()] # And we export them to our CSV file fichier_csv.writerow(b) # Don't forget to close the csv file at the end ! f.close()
Tip 8: Open an independant application from QGIS
It is also more a Python tip than a QGIS one. Sometimes, you want to launch a specific program (like OpenOffice or another independant script) with a (temporary) file you have created from QGIS (like a CSV file).
# You want to launch LibreOffice and make it opens a CSV file # You need to use the subprocess module (from Python 2.6) import subprocess # with this call, you open an independant instance of Libreoffice. # The pid variable can be detroyed without affecting LibreOffice pid = subprocess.Popen(['libreoffice', csv_file_path]).pid
Conclusion
I hope that these tips will be useful to any Python developper who wants to go further with QGIS. From my point of view, the API is really easy to use and well documented. I really believe QGIS will become the reference in Desktop GIS !