QGIS Python code tips🔗

Posted by Médéric Ribreux 🗓 In blog/ Qgis/

#python #qgis #gis

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 !