Introduction

This class introduces the concepts of Python programming within the QGIS environment. We will cover the full breadth of topics that involve everything from using the Python Console to building a fully functional plugin. We will also explore GUI programming techniques for customizing the QGIS interface using Qt widgets.

This course requires basic knowledge of Python. If you are not familiar with Python, it is strongly recommended you complete our Python Foundation for Spatial Analysis course. We will build upon the exercises covered in that course.

Watch the video

Watch the Video ↗

Access the Presentation ↗

Get the Data Package

The code examples in this class use a variety of datasets. All the required layers, project files, icons etc. are supplied to you in the pyqgis_masterclass.zip file. Unzip this file to the Downloads directory. All scripts assume the data is available in the <home folder>/Downloads/pyqgis_masterclass/ directory.

Download pyqgis_masterclass.zip.

Note: Certification and Support are only available for participants in our paid instructor-led classes.

Get the Course Videos

The course is accompanied by a set of videos covering the all the modules. These videos are recorded from our live instructor-led classes and are edited to make them easier to consume for self-study. We have 2 versions of the videos:

YouTube

We have created a YouTube Playlist with separate videos for each notebook and exercise to enable effective online-learning. Access the YouTube Playlist ↗

Vimeo

We are also making combined full-length video for each module available on Vimeo. These videos can be downloaded for offline learning. Access the Vimeo Playlist ↗

Installation and Setting up the Environment

Install QGIS

This course requires QGIS LTR version 3.34. Please review QGIS-LTR Installation Guide for step-by-step instructions.

Get a Text Editor

Any kind of software development requires a good text editor. If you already have a favorite text editor or an IDE (Integrated Development Environment), you may use it for this course. Otherwise, each platform offers a wide variety of free or paid options for text editors. Choose the one that fits your needs.

Below are my recommendations editors that are simple to use for beginners.

  • Windows: Notepad++ is a good free editor for windows. Download and install the Notepad++ editor. Tip: Before writing Python code in Notepad+++, make sure to go to Settings → Preferences → Language and enable Replace by space. Python is very sensitive about whitespace and this setting will ensure tabs and spaces are treated properly.
  • Mac: TextMate is an open-source editor for Mac that is currently available for free.

1. Hello World!

Watch the Video

Watch the Video ↗

QGIS Comes with a built-in Python Console and a code editor where you can write and run Python code.

Go to Plugins → Python Console to open the console.

At the >>> prompt, type in the following command and press Enter.

print('Hello World!')

Here you are running Python’s print() function with the text ‘Hello World’. The output of the statement will be printed below.

While console is useful for typing 1-2 lines of code or printing information contained in a variable, you should use the built-in editor for typing longer scripts or code snippets. Click the Show Editor button to open the editor panel. Enter the code and click the Run Script button to execute it. The results will appear in the console as before. If you are working on a longer script, you can also click the Save button in the editor to save the script for future use.

All code snippets below should be run from the Editor.

2. Hello PyQGIS!

Watch the Video

Watch the Video ↗

QGIS provides a Python API (Application Programming Interface), commonly known as PyQGIS. The API is vast and very capable. Almost every operation that you can do using QGIS - can be done using the API. This allows developers to write code to build new tools, customize the interface and automate workflows.

Let’s try out the API to perform some GIS data management tasks.

Browse to the data directory and load the shoreline.shp layer. Open the Attribute Table. This layer has 6 attribute columns. Let’s say we want to delete the 2nd column (SDE_SFGIS_) from the layer.

This can be done using the QGIS GUI as follows

  1. Right-click the shoreline layer and click Open Attribute Table.
  2. In the Attribute Table, click the Toggle Editing mode button.
  3. Click the Delete field button. Select the SDE_SFGIS_ column and click OK.
  4. Click the Save edits button and click the Toggle Editing mode to stop editing.

QGIS Provides an API to accomplish all of this using Python code. We will now do this task - but using only Python code. Open the Editor and enter the following code. Click the Run Script button to execute it.

Make sure you have selected the shoreline layer in the Layers panel before running the code.

layer = iface.activeLayer()
layer.startEditing()
layer.deleteAttribute(1)
layer.commitChanges()

You will see that the 2nd column is now deleted from the attribute table.

Let’s understand the code step-by-step

  1. layer = iface.activeLayer(): This line uses the iface object and runs the activeLayer() method which returns the currently selected layer in QGIS. We will learn more about iface in the QGIS Interface API section. The method returns the reference to the layer which is saved in the layer variable.
  2. layer.startEditing(): This is equivalent to putting the layer in the editing mode.
  3. layer.deleteAttribute(1): The deleteAttribute() is a method from QgsVectorLayer class. It takes the index of the attribute to be deleted. Here we pass on index 1 for the second attribute. (index 0 is the first attribute)
  4. layer.commitChanges(): This method saves the edit buffer and also disables the editing mode.

This gives you a preview of the power of the API. To harness the full power of the PyQGIS API, we must first understand how classes work.

Note that you can also trigger the Start Editing and Stop Editing actions using the QgsVectorLayerTools class. Below is an alternative code that presents the confirmation dialog to the user before committing changes.

layer = iface.activeLayer()
iface.vectorLayerTools().startEditing(layer)
layer.deleteAttribute(1)
iface.vectorLayerTools().stopEditing(layer)

3. Understanding Classes

Before we dive it to PyQGIS, it is important to understand certain concepts related to C++ and Python Classes. Qt as well as QGIS is written in C++ language. Functionality of each Qt/QGIS Widget is implemented as a class - having certain properties and functions. When we use PyQt or PyQGIS classes, it is executing the code in the C++ classes via the python bindings.

Watch the Video

Watch the Video ↗

View the Presentation ↗

Here’s the code to create a class called Car demonstrating the example we covered in the Classes and Objects presentation. It creates a class, initializes it to create new instances and demonstrates the concept of inheritance. A new class is defined using the word class. All classes have a function called__init__(), which is always executed when a new object is being created. There is also the keyword self which refers to the current instance of the class. The code uses the super keyword to refer to the parent class.

class Car:
    
    model = 'Civic'
    
    def __init__(self, color, type):
        self.color = color
        self.type = type
        self.started = False
        self.stopped = False
    
    def start(self):
        print('Car Started')
        self.started = True
        self.stopped = False
        
    def stop(self):
        print('Car Stopped')
        self.stopped = True
        sefl.started = False

# Instantiate the class      
my_car = Car('blue', 'automatic')

print(my_car)

# Call a method
my_car.start()

# Check the value of an instance variable
print('Car Started?', my_car.started)

# Check the value of a class variable
print('Car model', Car.model)

# Inheritance

class Sedan(Car):
        
    def __init__(self, color, type, seats):
        super().__init__(color, type)
        self.seats = seats
        
class ElectricSedan(Sedan):

    def __init__(self, color, type, seats, range_km):
        super().__init__(color, type, seats)
        self.range_km = range_km
        
my_car = Sedan('blue', 'automatic', 5)
print(my_car.color)
print(my_car.seats)
my_car.start()


my_future_car = ElectricSedan('red', 'automatic', 5, 500)
print(my_future_car.color)
print(my_future_car.seats)
print(my_future_car.range_km)
my_future_car.start()

4. Using PyQGIS Classes

Let’s start working with PyQGIS classes now. QGIS has classes for the whole range of operations - from building the user interface to doing geoprocessing. We will see how to access these classes via the PyQGIS API.

4.1 Calculating distance using PyQGIS

A basic but important operation in a GIS is the calculation of distance and areas. We will see how you can use PyQGIS APIs to compute distances.

We will compute distance between the following 2 coordinates

san_francisco = (37.7749, -122.4194)
new_york = (40.661, -73.944)

QGIS provides the class QgsDistanceArea that has methods to compute distances and areas.

To use this class, we must create an object by instantiating it. Looking at the class documentation, the class constructor doesn’t take any arguments. We can use the default constructor to create an object and assign it to the d variable.

d = QgsDistanceArea()

The documentation mentions that if a valid ellipsoid has been set for the QgsDistanceArea, all calculations will be performed using ellipsoidal algorithms (e.g. using Vincenty’s formulas). As we want to compute the distance between a pair of latitude/longitudes, we need to use the ellipsoidal algorithms. Let’s set the ellipsoid WGS84 for our calculation. We can use the method setEllipsoid(). Remember, methods should be applied on objects, and not classes directly.

d.setEllipsoid('WGS84')

Tip: All valid ellipsoids can be found by calling QgsEllipsoidUtils.acronyms()

Now our object d is capable of performing ellipsoidal distance computations. Browsing through the available methods in the QgsDistanceArea class, we can see a measureLine() method. This method takes a list QgsPointXY objects. We can create these objects from our coordinate pairs and pass them on to the measureLine() method to get the distance. The output will be in meters. We divide it by 1000 to convert it to kilometers.

lat1, lon1 = san_francisco
lat2, lon2 = new_york
# Remember the order is X,Y
point1 = QgsPointXY(lon1, lat1)
point2 = QgsPointXY(lon2, lat2)

distance = d.measureLine([point1, point2])
print(distance/1000)

Putting it all together, below is the complete code to calculate the distance between a pair of coordinates using PyQGIS. When you run the code in the Python Console of QGIS, all the PyQGIS classes are already imported. If you are running this code from a script or a plugin, you must explicitly import the QgsDistanceArea class.

from qgis.core import QgsDistanceArea

san_francisco = (37.7749, -122.4194)
new_york = (40.661, -73.944)

d = QgsDistanceArea()
d.setEllipsoid('WGS84')


lat1, lon1 = san_francisco
lat2, lon2 = new_york
# Remember the order is X,Y
point1 = QgsPointXY(lon1, lat1)
point2 = QgsPointXY(lon2, lat2)

distance = d.measureLine([point1, point2])
print(distance/1000)

Exercise 1

Let’s say you want to make a stop at Las Vegas on the way from San Francisco to New York.

Calculate the total ellipsoidal distance considering a stop at Las Vegas.

san_francisco = (37.7749, -122.4194)
new_york = (40.661, -73.944)
las_vegas = (36.1699, -115.1398)

If your code is correct, you should see the output distance to be 4271.02 kilometers.

4.2 Distance Conversion

The distance returned by the measureLine() method in the previous section was in meters, and we divided it by 1000 to convert it to kilometers. Rather than doing this conversion manually, we can use the PyQGIS API. The QgsDistanceArea class has a method convertLengthMeasurement() that can convert the measured distance to any supported unit. The convertLengthMeasurement() method takes 2 arguments - the length measured by measureLine() method and the unit to convert the measurement to. The unit should be a value of the type Qgis.DistanceUnit. The permitted values are defined in the Qgis.DistanceUnit documentation. The code below shows how to convert the measured distance to Kilometers and Miles.

Prior to QGIS 3.34, the unit types were specified as QgsUnitTypes.DistanceKilometers.

from qgis.core import QgsDistanceArea
from qgis.core import Qgis

san_francisco = (37.7749, -122.4194)
new_york = (40.661, -73.944)

d = QgsDistanceArea()
d.setEllipsoid('WGS84')


lat1, lon1 = san_francisco
lat2, lon2 = new_york
# Remember the order is X,Y
point1 = QgsPointXY(lon1, lat1)
point2 = QgsPointXY(lon2, lat2)

distance = d.measureLine([point1, point2])
print('Distance in meters', distance)

distance_km = d.convertLengthMeasurement(distance, Qgis.DistanceUnit.DistanceKilometers)
print('Distance in kilometers', distance_km)

distance_mi = d.convertLengthMeasurement(distance, Qgis.DistanceUnit.DistanceMiles)
print('Distance in miles', distance_mi)

5. Graphical User Interface (GUI) Programming Basics

Watch the Video

Watch the Video ↗

View the Presentation ↗

5.1 Qt and PyQt

Qt is a free and open-source widget toolkit for creating graphical user interfaces as well as cross-platform applications. QGIS is built using the Qt platform. Both Qt and QGIS itself have well-documented APIs that should be used when writing Python code to be run within QGIS.

PyQt is the Python interface to Qt. PyQt provides classes and functions to interact with Qt widgets.

5.2 Building a Dialog Box

Let’s learn how to use PyQt classes to create and interact with GUI elements. Here we will create a simple dialog box that prompts a user for confirmation. You can type the code in the Editor and click Run Script.

mb = QMessageBox()

The QMessageBox is a PyQt class for creating a dialog with buttons. To use the class, you create an object by instantiating the class. Here mb is an object, which is an instance of the QMessageBox class, created using the default parameters.

type() tells you what is the class of the object

type(mb)

dir returns list of the attributes and methods of any object

dir(mb)

Classes have methods that provide functionality. You can run the class methods on instance objects. For the QMessageBox class, setText() method will add a text to the dialog.

mb = QMessageBox()
mb.setText('Click OK to confirm')

Classes also have class attributes which are shared across all instances. The QMessageBox class has Ok and Cancel attributes, which can be referred using QMessageBox.Ok and QMessageBox.Cancel.

mb = QMessageBox()
mb.setText('Click OK to confirm')
mb.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)

To see the dialog, we need to use the exec() method. The user input is then captured and saved in the return_value variable.

The complete code snippet is as follows.Try it out and see the result of your action reflect in the Python Console.

mb = QMessageBox()
mb.setText('Click OK to confirm')
mb.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
return_value = mb.exec()
if return_value == QMessageBox.Ok:
    print('You pressed OK')
elif return_value == QMessageBox.Cancel:
    print('You pressed Cancel')

6. Deep Dive into PyQGIS

PyQGIS is the Python interface to QGIS. It is created using SIP and integrates with PyQt.

Fun Fact: Most QGIS class names start with the prefix Qgs. Q is for Qt and gs stands for Gary Sherman - the founder of the QGIS project.

QGIS C++ API documentation is available at https://qgis.org/api/3.34/

QGIS Python API documentation is available at https://qgis.org/pyqgis/3.34/

Both C++ and Python APIs are identical for most part, but certain functions are not available in the Python API. 1

6.1 QGIS Interface API (QgisInterface)

You are ready to dive into the PyQGIS API now. In this section, we will focus on the QgisInterface class - which provides methods for interaction with the QGIS environment. When QGIS is running, a variable called iface is set up to provide an object of the class QgisInterface to interact with the running QGIS environment. This interface allows access to the map canvas, menus, toolbars and other parts of the QGIS application. Python Console and Plugins can use iface to access various parts of the QGIS interface.

Watch the Video

Watch the Video ↗

6.1.1 Change Title of QGIS Main Window

title = iface.mainWindow().windowTitle()
new_title = title.replace('QGIS', 'My QGIS')
iface.mainWindow().setWindowTitle(new_title)

6.1.2 Change Icon of QGIS Main Window

os.path.expanduser('~') returns the path to the home directory of the user.

import os

icon_image = 'qgis-black.png'
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
icon_path = os.path.join(data_dir, icon_image)
icon = QIcon(icon_path)
iface.mainWindow().setWindowIcon(icon)

6.1.3 Remove Raster and Vector Menus

vector_menu = iface.vectorMenu()
raster_menu = iface.rasterMenu()
menubar = vector_menu.parentWidget()
menubar.removeAction(vector_menu.menuAction())
menubar.removeAction(raster_menu.menuAction())

6.1.4 Understanding Signals and Slots

GUI programming requires responding to user’s actions. All objects in Qt have a mechanism where they can emit a signal when there is a change in status. i.e. when a user clicks a button, or a window is closed. As a programmer, you can connect the signal to a slot (i.e. a python function) which will be called when the signal is emitted. The general syntax for connecting the signal to a slot is <object>.<signal>.connect(function).

6.1.5 Add A New Menu Item

A new button or menu item is created using QAction(). Here we create an action and then connect the click signal to a method that opens a website.

import webbrowser

def open_website():
    webbrowser.open('https://gis.stackexchange.com')

website_action = QAction('Go to gis.stackexchange')
website_action.triggered.connect(open_website)
iface.helpMenu().addSeparator()
iface.helpMenu().addAction(website_action)

6.1.6 Change Visibility of a Toolbar

iface.pluginToolBar().setVisible(True)

6.1.7 Add a button to a toolbar

import os
from datetime import datetime

icon = 'question.svg'
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
icon_path = os.path.join(data_dir, icon)

def show_time():
    now = datetime.now()
    current_time = now.strftime("%H:%M:%S")
    iface.messageBar().pushInfo('Current Time', current_time)

action = QAction('Show Time')
action.triggered.connect(show_time)
action.setIcon(QIcon(icon_path))
iface.addToolBarIcon(action)

Exercise 2

In the previous example, the alert is displayed in the QGIS message bar as an Info message. Change the type of the message to a Warning message.

Hint: Look at the appropriate method in the QgsMessageBar class

6.1.8 Add New Layers

Data sources are identified by an URI (Uniform Resource Identifier) - For files on computer the URI is the file path - For databases, the URI is constructed using the QgsDataSourceUri class and encodes,the database path, table, username, password etc. - For web layers, such as WMF/WFS etc, the URI is the web URL

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

filename = 'seismic_zones.shp'
uri = os.path.join(data_dir, filename)
iface.addVectorLayer(uri, 'seismic_zones', 'ogr')

filename = 'sf.gpkg|layername=zoning'
uri = os.path.join(data_dir, filename)
iface.addVectorLayer(uri, 'zoning', 'ogr')

6.1.9 Change name of a Layer

layer = iface.activeLayer()
name = layer.name()
layer.setName('sf_' + name)

Exercise 3

Write a code snippet that checks the active layer selected by the user. If the active layer is named zoning, display a success message in the message bar, else display an error message.

6.2 QGIS Project API (QgsProject)

Another very important QGIS class is QgsProject. This class is used for all operations in a QGIS project - including adding/removing map layers, styling, print layouts etc. The QgsProject is a Singleton Class - meaning it can have only 1 instance at a time. The instance refers to the current QGIS project that is loaded. When QGIS starts, a blank project is created. When you load another project, the existing project is closed and a new project instance is created. You can get the current instance of the QgsProject class by calling the QgsProject.instance() method.

Watch the Video

Watch the Video ↗

6.2.1 Load a project

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

project = QgsProject.instance()
project_name = 'sf.qgz'
project_path = os.path.join(data_dir, project_name)
project.read(project_path)

6.2.2 Load Projects using a Dropdown Menu

The code snippet below shows how to create a new toolbar with a label and a drop-down menu. Selecting an item will load the project with that name.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

projectToolbar = iface.addToolBar('Project Selector')

label = QLabel('Select a project to load', parent=projectToolbar)
projectSelector = QComboBox(parent=projectToolbar)
projectSelector.addItem('sf.qgz')
projectSelector.addItem('places.qgz')
projectSelector.setCurrentIndex(-1)

def loadProject(projectName):
    project = QgsProject.instance()
    project_path = os.path.join(data_dir, projectName)
    project.read(project_path)

projectSelector.currentTextChanged.connect(loadProject)
projectToolbar.addWidget(label)
projectToolbar.addWidget(projectSelector)

6.2.3 Create a New Vector Layer

We will now create a temporary memory layer using PyQGIS. Memory layers are not saved to the disk and ideal to store intermediate results. The code snippet below creates a polygon layer with the extent of the current map canvas.

mc = iface.mapCanvas()
extent = mc.extent()

vlayer = QgsVectorLayer('Polygon', 'extent', 'memory')
crs = QgsProject.instance().crs()
vlayer.setCrs(crs)

provider = vlayer.dataProvider()

f = QgsFeature()
geometry = QgsGeometry.fromRect(extent)
f.setGeometry(geometry)

provider.addFeature(f)
vlayer.updateExtents() 

QgsProject.instance().addMapLayer(vlayer)

Exercise 4

The following code snippet creates a toolbar called CRS Toolbar with a label, textbox and a button. When the button is clicked, the function changeCRS is called. Implement this function so that it changes the CRS of the current project to the EPSG code entered by the user.

Hint: Use QgsProject.instance().setCrs() method.

crsToolbar = iface.addToolBar('CRS Toolbar')

label = QLabel('Enter an EPSG Code', parent=crsToolbar)
crsTextBox = QLineEdit('4326', parent=crsToolbar)
crsTextBox.setFixedWidth(80)
button = QPushButton('Go!', parent=crsToolbar)

crsToolbar.addWidget(label)
crsToolbar.addWidget(crsTextBox)
crsToolbar.addWidget(button)

def changeCrs(crsText):
    epsgCode = int(crsTextBox.text())
    iface.messageBar().pushInfo('Function called', f'You entered {epsgCode}')
    # Add code to change the project CRS to the EPSG code

button.clicked.connect(changeCrs)

7. Running Python Code at QGIS Launch

Watch the Video

Watch the Video ↗

It is possible to execute some PyQGIS code every time QGIS starts. QGIS looks for a file named startup.py in the user’s Python home directory, and if it is found, executes it. This file is very useful in customizing QGIS interface with techniques learnt in the previous section.

If you are running multiple versions of QGIS, a very useful customization is to display the QGIS version number and name in the main window. The version name is stored in a global QGIS variable called qgis_version. We can read that variable and set the main window’s title with it. We connect this code to the signal iface.initializationCompleted signal when the main window is loaded.

Create a new file named startup.py with the following code. Note the imports at the top - including iface. When we ran the code snippets in the Python Console, we did not have to import any modules since they are done automatically when the console starts. For pyqgis scripts elsewhere, we have to explicitly import the modules (classes) that we want to use.

from qgis.utils import iface
from qgis.core import QgsExpressionContextUtils

def customize():
    version = QgsExpressionContextUtils.globalScope().variable('qgis_version')
    title = iface.mainWindow().windowTitle()
    iface.mainWindow().setWindowTitle('{} | {}'.format(title,version))


iface.initializationCompleted.connect(customize)

This file needs to be copied to the appropriate directory on your system. See QGIS documentation for details on the path for your platform.

Once you copy the file at that location, restart QGIS. The title bar should now have the QGIS version name in it.

Pro Tip: It is possible to put the startup.py file on a shared drive for enterprise deployment of QGIS customizations. Learn more.

Exercise 5

Trying opening a new project in QGIS after you have restarted GIS with startup.py file in place. You will notice that the custom title with the version name is replaced with the default title.

Make a change to your startup.py so that the customization is applied even when a new project is loaded.

8. Running Processing Algorithms

While the PyQGIS API providers many functions to work with layers, features, attributes and geometry - it is a much better practice to use the built-in processing algorithms to alter the layers or do any analysis. This will give you better performance and result in much lesser code. Here are some examples on how to use processing algorithms from Python to do vector and raster layer editing. You will find more information about various options and techniques in the article Using processing algorithms from the console of the QGIS User Guide

Watch the Video

Watch the Video ↗

8.1 Creating Hillshade from a DEM

To use any Processing Algorithm via Python, you need to know how to specify all the required parameters. This is easiest to obtain by running the algorithm via the GUI first.

In this section we will learn how to create a hillshade raster from a DEM. We will first carry out the task using QGIS.

  1. Browse to the data directory and load the srtm.tif layer. Search and locate the Processing Toolbox → Raster terrain analysis → Hillshade algorithm from the Processing Toolbox. Double-click to open it.

  1. Select srtm as the Elevation layer and keep all the other parameters to their default value. Click Run.

  1. A new Hillshade layer will be added to the Layers panel. Now we will locate the Python command for this operation from the Processing History. Go to Processing → History.

  1. The first entry in the top panel will show the last algorithm that was ran from the toolbox. Click on it to select it. The full Python command will be shown at the bottom. Copy it.

You can now use the parameters in your Python code and replace the path of the input. Below is the code snippet that runs the same algorithm from a Python script. Note that we are using the processing.runAndLoadResults() method that adds the resulting layer to the canvas.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

filename = 'srtm.tif'
srtm = os.path.join(data_dir, filename)
iface.addRasterLayer(srtm, 'srtm', 'gdal')

results = processing.runAndLoadResults("native:hillshade", 
    {'INPUT': srtm, 
    'Z_FACTOR':2,
    'AZIMUTH':300,
    'V_ANGLE':40,
    'OUTPUT': 'TEMPORARY_OUTPUT'})

8.2 Running Multiple Processing Algorithms

We can also chain multiple processing tools to build a script to build a data processing pipeline. In the example below, we will do 2 steps

  1. Clip srtm.tif raster using the shoreline.shp layer.
  2. Calculate the hillshade on the clipped raster and load it in QGIS.

Note that we are using the processing.run() method for the first step. This method calculates the output, but does not load the result to QGIS. This allows us to carry out multiple processing steps and not load intermediate layers.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

filename = 'srtm.tif'
srtm = os.path.join(data_dir, filename)
filename = 'shoreline.shp'
shoreline = os.path.join(data_dir, filename) 

results = processing.run("gdal:cliprasterbymasklayer", 
    {'INPUT':srtm,
    'MASK': shoreline,
    'OUTPUT':'TEMPORARY_OUTPUT'})

clipped_dem = results['OUTPUT']
   
results = processing.runAndLoadResults("native:hillshade", 
    {'INPUT': clipped_dem, 
    'Z_FACTOR':2,
    'AZIMUTH':300,
    'V_ANGLE':40,
    'OUTPUT': 'TEMPORARY_OUTPUT'})

You can also do batch-processing by iterating through multiple layers and running the processing algorithm in a for-loop. Doing it via Python allows you greater flexibility - such as combining the results into a single layer. See Running Processing Algorithms via Python tutorial for a complete example.

Exercise 6

Your data package contains a polygon layer of seismic zones in San Francisco. We want to calculate the average elevation within each seismic zone from this layer. The code below reads the vector layer seismic_zones.shp of seismic zones and the raster layer srtm.tif containing elevation values. The vector layer contains some invalid polygons, so we run the native:fixgeometries algorithm to fix them.

Use the resulting layer with the fixes geometries and calculate Zonal Statistics algorithm native:zonalstatisticsfb to calculate the average elevation from the raster layer.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

vector_layer = 'seismic_zones.shp'
vector_layer_path = os.path.join(data_dir, vector_layer)

raster_layer = 'srtm.tif'
raster_layer_path = os.path.join(data_dir, raster_layer)

# Input vector has invalid geometries
# Fix them first
results = processing.run("native:fixgeometries", {
  'INPUT':vector_layer_path,
  'METHOD': 0,
  'OUTPUT':'TEMPORARY_OUTPUT'})

fixed_vector_layer = results['OUTPUT']

# Run Zonal Statistics and load the resulting layer

Assignment

Watch the Video

Watch the Video ↗

The following assignment is designed to help you practice the skills learnt so far in the course and explore the PyQGIS API.

Your task is to write a PyQGIS Script to show the average value of the selected raster layer within the current map extent. For example, if you load the srtm layer from the data package, select it and click the button - it should calculate and display the average elevation within the canvas extent. If you zoom/pan the map and click the button again - it should compute the display the average elevation within the new extent. Here is the recommended structure for your script.

Hint: QgsRasterInterface class provides a bandStatistics() method for calculating statistics from a raster band. You can get the reference to the instance of this class for a raster layer using layer.dataProvider().

Extra credit if your script also does error checking and displays an error message on the message bar for the following conditions:

9. Writing Plugins

Plugins are a great way to extend the functionality of QGIS. You can write plugins using Python that can range from adding a simple button to sophisticated tool-kits.

Watch the Video

Watch the Video ↗

View the Presentation ↗

There is a plugin named Plugin Builder that can help you generate a starter plugin. We have published step-by-step instructions for both GUI plugins and Processing Plugins using the Plugin Builder method. While this method gives you an easy way to have a functional plugin, it is not the ideal way to learn plugin development. We recommend starting from a minimal template and adding elements as and when needed. Here we will learn the basics of plugin framework using a minimal plugin and learn how to add various element to make it a full plugin.

9.1 Understanding Plugins

Plugins are much more integrated into the QGIS system than Python Scripts. They are managed by Plugin Manager and are initialized when QGIS starts. To understand the required structure, let’s see what a minimal plugin looks like. You can learn more about this structure at QGIS Minimalist Plugin Skeleton.

We will now build a simple plugin named Basemap Loader that adds a button in the Plugin Toolbar that loads a basemap from OpenStreetMap to the current project.

The first requirement for plugins is a file called metadata.txt. This file contains general info, version, name and some other metadata used by plugins website and plugin manager.

metadata.txt

[general]
name=Basemap Loader
description=This plugin adds a basemap layer to QGIS.
version=1.0
qgisMinimumVersion=3.0
author=Ujaval Gandhi
email=ujaval@spatialthoughts.com
icon=logo.png

Second is the file that contains the main logic of the plugin. It must have __init__() method that gives the plugin access to the QGIS Interface (iface). The initGui() method is called when the plugin is loaded and unload() method which is called when the plugin is unloaded. For now, we are creating a minimal plugin that just add a button and a menu entry that displays message when clicked.

load_basemap.py

import os
import inspect
from PyQt5.QtWidgets import QAction
from PyQt5.QtGui import QIcon

cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0]

class BasemapLoaderPlugin:
    def __init__(self, iface):
        self.iface = iface

    def initGui(self):
        icon = os.path.join(os.path.join(cmd_folder, 'logo.png'))
        self.action = QAction(QIcon(icon), 'Load Basemap', self.iface.mainWindow())
        self.iface.addToolBarIcon(self.action)
        self.action.triggered.connect(self.run)
      
    def unload(self):
        self.iface.removeToolBarIcon(self.action)
        del self.action
        
    def run(self):
        self.iface.messageBar().pushMessage('Hello from Plugin')

Third file is called __init__.py which is the starting point of the plugin. It imports the plugin class created in the second file and creates an instance of it.

__init__.py

from .load_basemap import BasemapLoaderPlugin

def classFactory(iface):
    return BasemapLoaderPlugin(iface)

Create these 3 files and put them in a folder named basemap_loader. Copy the logo.png file from <home folder>/Downloads/pyqgis_masterclass/logo.png to this folder. Copy the folder to the python plugins directory at {profile folder}/python/plugins.

Restart QGIS. Go to Plugins → Manage and Install plugins… → Installed and enable the Basemap Loader plugin. You will the toolbar icon from the plugin. Click on the button and the Hello from Plugin message is displayed.

9.2 Adding Functionality

Now let’s build on the basic plugin structure and add the functionality to load a XYZ Tile Layer when the button is clicked. We will be using the OpenStreetMap Standard XYZ layer. The PyQGIS code to load a XYZ tile layer is adapted from the PyQGIS Cookbook. Modify the load_basemap.py file with the content from below.

load_basemap.py

import os
import inspect
from PyQt5.QtWidgets import QAction
from PyQt5.QtGui import QIcon
from qgis.core import QgsRasterLayer, QgsProject

cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0]

class BasemapLoaderPlugin:
    def __init__(self, iface):
        self.iface = iface

    def initGui(self):
        icon = os.path.join(os.path.join(cmd_folder, 'logo.png'))
        self.action = QAction(QIcon(icon), 'Load Basemap', self.iface.mainWindow())
        self.iface.addToolBarIcon(self.action)
        self.action.triggered.connect(self.run)
      
    def unload(self):
        self.iface.removeToolBarIcon(self.action)
        del self.action

    def run(self):
        basemap_url = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
        zmin = 0
        zmax = 19
        crs = 'EPSG:3857'
        
        uri = f'type=xyz&url={basemap_url}&zmax={zmax}&zmin={zmin}$crs={crs}'
        rlayer = QgsRasterLayer(uri, 'OpenStreetMap', 'wms')
        if rlayer.isValid():
            QgsProject.instance().addMapLayer(rlayer)
            self.iface.messageBar().pushSuccess('Success', 'Basemap Layer Loaded')
        else:
            self.iface.messageBar().pushCritical('Error', 'Invalid Basemap Layer')

To see the result of our changes, we must restart QGIS. This can be quite tedious while developing plugins, so there is a handy plugin named Plugin Reloader that can reload a selected plugin without having to restart QGIS. Go to Plugins → Manage and Install plugins… → All and search for the plugin named Plugin Reloader. Click Install Plugin. Once the plugin is installed, locate the Configure button from the Plugin Toolbar and select the Basemap Loader plugin. Click Reload to reload the plugin.

Once reloaded, click the Load Basemap button from the toolbar and you will see the basemap layer loaded in QGIS.

Exercise 7

Load the places.qgz project from your data package.

Modify the plugin to change the Project CRS to the crs of the tile layer once the basemap is loaded.

Hint: Use QgsProject.instance().setCrs() method.

By default, the new layer will be inserted at the top of the layer tree. If you want to insert the layer at a specific place, you can use the code snippet below.

# Add the layer, but not to the legend
QgsProject.instance().addMapLayer(rlayer, False)
# Insert layer at the bottom of Layer Tree
root = QgsProject.instance().layerTreeRoot()
position = len(root.children())
root.insertLayer(position, rlayer)

10. Advanced Python Concepts

10.1 Understanding Python Iterators

An Iterator is a type of Python object that contains items that can be iterator upon. They are similar to other objects, like lists - but with a key difference. When you create an iterator, you don’t store all the items in memory. The iterator loads a single item at a time and then fetches the next item when asked for it. This makes it very efficient for reading large amounts of data without having to read the entire dataset. QGIS implements iterators for many different object types.

We will continue to work with the sf.qgz project. Open the project and select the blocks layer. In the example below, the result of calling layer.getFeatures() is an iterator. You can call the next() function to fetch the next item from the iterator.

layer = iface.activeLayer()
features = layer.getFeatures()
f = next(features)
print(f.attributes())
f = next(features)
print(f.attributes())

You can also use for-loops to iterate through an iterator. Here we look up the Feature ID of each feature using the id() method and store it in a list.

layer = iface.activeLayer()
features = layer.getFeatures()
ids = []
for f in features:
  id = f.id()
  ids.append(id)

print(ids)

10.2 List Comprehensions

A common data processing task in Python is to read items from a list or an iterator, doing some processing on each item and creating a new list with the results. The regular way to do this is to first create an empty list, iterate over each item of the existing list, and append the results to the new empty list. Python provides a powerful alternative to this workflow in the form of List Comprehension. The snippet below shows the syntax.

my_list = [1, 2, 3, 4, 5]

# Create a new list by adding 1 to each element
new_list = []
for x in my_list:
    new_list.append(x+1)

print(new_list)

# Use list comprehension syntax

new_list = [x+1 for x in my_list]
print(new_list)

Exercise 8

The following code creates a list of field names for the selected layer. Convert the code to use a list comprehension.

layer = iface.activeLayer()
fields = layer.fields()
field_names = []

for field in fields:
  field_names.append(field.name())

print(field_names)

11. Writing Processing Plugins

The new and preferred way to write plugins in QGIS is using the Processing Framework. It removes the need for you to design the user interface. The resulting plugin integrates seamlessly in the Processing Toolbox and is interoperable with other processing algorithms.

In this section, we will learn how to build a plugin called Save Attributes that adds a new algorithm to iterate over all features of a layer, extract their attribute values and save the results as a CSV file. We will learn about different components of a processing plugin and learn the skills required to create a functional processing plugin.

11.1 Iterating over Features

Let’s start by learning how to iterate over features of a vector layer. We will iterate over each feature and extract the value of all attributes. To keep things simple for now, we will use Pandas library to create a DataFrame and save the results. We will later modify this code to use native PyQGIS API.

Open the Python Console and click the Show Editor button. Copy/paste the following code. Select the blocks layer and run this script by clicking the Run Script button. The script will process the layer and write the file at the given location.

import os
import pandas as pd

data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

output_name = 'output.csv'
output_path = os.path.join(data_dir, output_name)

layer = iface.activeLayer()

# Check if a layer is selected
if not layer:
    iface.messageBar().pushMessage(
      'Please select a layer',  level=Qgis.Critical)
      
# Check if the selected layer is a vector layer
if layer.type() != QgsMapLayer.VectorLayer:
    iface.messageBar().pushMessage(
      'Please select a vector layer',  level=Qgis.Critical)
    
output_name = 'output.csv'
output_path = os.path.join(data_dir, output_name)
fields = layer.fields()

# Get a list of field names
fieldnames = [field.name() for field in layer.fields()]
# Get a list of attributes for each feature
data = [f.attributes() for f in layer.getFeatures()]

# Create a Pandas DataFrame
df = pd.DataFrame(data, columns=fieldnames)

# Save to file
df.to_csv(output_path, index=False)

iface.messageBar().pushMessage(
      'Success:','Output file written at ' + output_path, level=Qgis.Success)

Exercise 9

Modify the above code to save attributes of only selected features. Bonus points if your script checks whether the user has selected any features on the layer and display an error if no features are selected.

Hint1: See the available methods for the QgsVectorLayer class.

Hint2: To check if the layer has any selected features, check for a method that gives you the count of selected features.

11.2 Saving Vector Layers

Let’s take the script we wrote above and learn how to save the results a vector layer as a file using PyQGIS classes. This is the preferred method while developing scripts that can be used as Processing algorithms or plugins. We will use the QgsVectorFileWriter class to create a file in any of the supported vector data formats.

We will use QgsVectorFileWriter.create() method which takes the following parameters

  • fileName: Path to the file
  • fields: Fields to write
  • geometryType: geometry type of output file
  • srs: CRS of the output file
  • transformContext: Datum transformation settings
  • options: Save Options such as format, encoding etc.

Once we initialize a QgsVectorFileWriter object, we iterate over the original layer and call the addFeature() method to add features to the writer.

Open the Python Console and click the Show Editor button. Copy/paste the following code. Select the blocks layer and run this script by clicking the Run Script button. The script will process the layer and write the file at the given location.

import os
import time

data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

layer = iface.activeLayer()

# Check if a layer is selected
if not layer:
    iface.messageBar().pushMessage('Please select a layer',  level=Qgis.Critical) 
# Check if the selected layer is a vector layer
if layer.type() != QgsMapLayer.VectorLayer:
    iface.messageBar().pushMessage('Please select a vector layer',  level=Qgis.Critical)

# Define parameters for QgsVectorFileWriter
output_name = 'output.csv'
output_path = os.path.join(data_dir, output_name)

# Define the options for saving the layer
save_options = QgsVectorFileWriter.SaveVectorOptions()
save_options.driverName = 'CSV'
save_options.fileEncoding = 'UTF-8'
# We can also add some format-specific layer options 
# These come from GDAL/OGR
# https://gdal.org/drivers/vector/csv.html
save_options.layerOptions = ['SEPARATOR=COMMA']

# Create the writer
writer = QgsVectorFileWriter.create(
    fileName=output_path,
    fields=layer.fields(),
    geometryType=QgsWkbTypes.NoGeometry,
    srs=layer.crs(),
    transformContext=QgsProject.instance().transformContext(),
    options=save_options)

# Check if we were able to create the writer
if writer.hasError() != QgsVectorFileWriter.NoError:
    iface.messageBar().pushMessage(
        'Error:', writer.errorMessage, level=Qgis.Critical)

# Add features
for f in layer.getFeatures():
    writer.addFeature(f)

# delete the writer to flush features to disk
del writer

iface.messageBar().pushMessage(
    'Success:','Output file written at ' + output_path, level=Qgis.Success)

11.3. Writing a Processing Script

We saw how to write a Python script in the QGIS Python Console Code Editor. But there is another way - and it is the preferred approach to write scripts. Whenever you are writing a new script, consider using the built-in Processing Framework. This has several advantages. First, taking user input and writing output files is far easier because Processing Framework offers standardized user interface for these. Second, having your script in the Processing Toolbox also allows it to be part of any Processing Model or be run as a Batch job with multiple inputs. This tutorial will show how to write a custom python script that can be part of the Processing Framework in QGIS.

Watch the Video

Watch the Video ↗

View the Presentation ↗

You can review additional information and tips in the Writing new Processing algorithms as Python scripts section of the QGIS User Guide.

The Processing algorithms follow the structure as described below:

  • Processing scripts inherit from the base QgsProcessingAlgorithm class.
  • The initAlgorithm() method is called to set-up the inputs and the outputs. The inputs and outputs to the processing algorithms must be defined with appropriate class for the type of parameter. You can see all the available classes for defining parameters that inherit the QgsProcessingParameterDefinition class.
  • The processAlgorithm() method contains the main logic of the script that gets executed when the user clicks the Run button. You can get the references to the user selected inputs and outputs using the methods from the QgsProcessingAlgorithm class.

There is additional terminology and classes that you must understand for writing Processing Scripts:

  • context: This is an object of the QgsProcessingContext class that contains information about the current project and settings. Learn more at what does context mean in PyQGIS?.
  • feedback: This is an object of the QgsProcessingFeedback class that is used to communicate with the user during the execution of the algorithm. This is used for updaitng the progress bar, displaying error messages etc.
  • source and sink: Processing algorithms use specialized classes that make is simple to read and write vector layers. The QgsFeatureSource class has methods to easily read features and QgsFeatureSink has methods to write features.

We will now see how the Python Console script can be converted to a Processing script.

To create a new processing script, go to Processing → Processing Toolbox. Click the Scripts button and select Create New Script...

Copy/paste the following code into the Processing Script Editor.

save_attributes_processing.py

from PyQt5.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFileDestination,
                       QgsVectorFileWriter,
                       QgsWkbTypes,
                       QgsProject)


class SaveAttributesAlgorithm(QgsProcessingAlgorithm):
    """Saves the attributes of a vector layer to a CSV file."""

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                'INPUT',
                'Input layer',
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )

        # We add a file output of type CSV.
        self.addParameter(
            QgsProcessingParameterFileDestination(
                'OUTPUT',
                'Output File',
                'CSV files (*.csv)',
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        layer = self.parameterAsVectorLayer(
            parameters,
            'INPUT',
            context)

        output = self.parameterAsFileOutput(
            parameters,
            'OUTPUT',
            context)

        # get features from source
        total = layer.featureCount()
        features = layer.getFeatures()
        
        # Define the options for saving the layer
        save_options = QgsVectorFileWriter.SaveVectorOptions()
        save_options.driverName = 'CSV'
        save_options.fileEncoding = 'UTF-8'

        # Create the writer
        writer = QgsVectorFileWriter.create(
            fileName=output,
            fields=layer.fields(),
            geometryType=QgsWkbTypes.NoGeometry,
            srs=layer.crs(),
            transformContext=QgsProject.instance().transformContext(),
            options=save_options)

    
        for current, f in enumerate(features):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            # Add the feature
            writer.addFeature(f)

            # Update the progress bar
            if total != 0:
              progress = int(100*(current/total))
            else:
              progress = 0
            feedback.setProgress(progress)

        return {'OUTPUT': output}

    def name(self):
        return 'save_attributes'

    def displayName(self):
        return self.tr('Save Attributes As CSV')
 
    def group(self):
        return self.tr(self.groupId())

    def groupId(self):
        return ''

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return SaveAttributesAlgorithm()

Click the Save Script button and save the script as save_attributes_algorithm.py. This script must be saved inside the {profile folder}/processing/scripts/ directory so it can be loaded when QGIS starts.

Once saved, the algorithm will appear in the Processing Toolbox under Scripts → Save Attributes As CSV. Double-click to launch it. You will see the standard processing algorithm dialog where the user can select inputs and outputs easily. Progress bar is shown correctly and the execution also stops if the user presses the Cancel button. If the large is large and the algorithm would take time to process it, the user can also close the window and the algorithm will continue to run in the background.

11.4 Processing Plugin

Let’s take the the processing script and package it in a processing plugin. We will need to create the following files.

  • metadata.txt: Plugin information
  • __init__.py: Initialization file
  • save_attributes.py: The main file containing the plugin’s class
  • save_attributes_algorithm.py: The processing script. This is identical to the script we wrote earlier.
  • save_attributes_provider.py: Processing provider that loads the algorithm.
  • logo.png: Logo for the plugin action.

Create these files with the content from below.

metadata.txt

[general]
name=Save Attributes
description=This plugin saves the attributes of the selected vector layer as a CSV file
version=1.0
qgisMinimumVersion=3.0
author=Ujaval Gandhi
email=ujaval@spatialthoughts.com
icon=logo.png

__init__.py

from .save_attributes import SaveAttributesPlugin

def classFactory(iface):
    return SaveAttributesPlugin(iface)

save_attributes_algorithm.py

from PyQt5.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFileDestination,
                       QgsVectorFileWriter,
                       QgsWkbTypes,
                       QgsProject)


class SaveAttributesAlgorithm(QgsProcessingAlgorithm):
    """Saves the attributes of a vector layer to a CSV file."""

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                'INPUT',
                'Input layer',
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )

        # We add a file output of type CSV.
        self.addParameter(
            QgsProcessingParameterFileDestination(
                'OUTPUT',
                'Output File',
                'CSV files (*.csv)',
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        layer = self.parameterAsVectorLayer(
            parameters,
            'INPUT',
            context)

        output = self.parameterAsFileOutput(
            parameters,
            'OUTPUT',
            context)

        # get features from source
        total = layer.featureCount()
        features = layer.getFeatures()
        
        # Define the options for saving the layer
        save_options = QgsVectorFileWriter.SaveVectorOptions()
        save_options.driverName = 'CSV'
        save_options.fileEncoding = 'UTF-8'

        # Create the writer
        writer = QgsVectorFileWriter.create(
            fileName=output,
            fields=layer.fields(),
            geometryType=QgsWkbTypes.NoGeometry,
            srs=layer.crs(),
            transformContext=QgsProject.instance().transformContext(),
            options=save_options)

    
        for current, f in enumerate(features):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            # Add the feature
            writer.addFeature(f)

            # Update the progress bar
            if total != 0:
              progress = int(100*(current/total))
            else:
              progress = 0
            feedback.setProgress(progress)

        return {'OUTPUT': output}

    def name(self):
        return 'save_attributes'

    def displayName(self):
        return self.tr('Save Attributes As CSV')
 
    def group(self):
        return self.tr(self.groupId())

    def groupId(self):
        return ''

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return SaveAttributesAlgorithm()

save_attributes_provider.py

import os
import inspect
from PyQt5.QtGui import QIcon

from qgis.core import QgsProcessingProvider
from .save_attributes_algorithm import SaveAttributesAlgorithm


class SaveAttributesProvider(QgsProcessingProvider):

    def __init__(self):
        QgsProcessingProvider.__init__(self)

    def unload(self):
        QgsProcessingProvider.unload(self)

    def loadAlgorithms(self):
        self.addAlgorithm(SaveAttributesAlgorithm())

    def id(self):
        return 'save_attributes'

    def name(self):
        return self.tr('Save Attributes')

    def icon(self):
        cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0]
        icon = QIcon(os.path.join(os.path.join(cmd_folder, 'logo.png')))
        return icon

    def longName(self):
        return self.name()

save_attributes.py

import os
import sys
import inspect
from PyQt5.QtWidgets import QAction
from PyQt5.QtGui import QIcon

from qgis.core import QgsProcessingAlgorithm, QgsApplication
import processing
from .save_attributes_provider import SaveAttributesProvider


cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0]

class SaveAttributesPlugin:
    def __init__(self, iface):
        self.iface = iface

    def initProcessing(self):
      self.provider = SaveAttributesProvider()
      QgsApplication.processingRegistry().addProvider(self.provider)
        
    def initGui(self):
      self.initProcessing()
      icon = os.path.join(os.path.join(cmd_folder, 'logo.png'))
      self.action = QAction(QIcon(icon), 'Save Attributes as CSV', self.iface.mainWindow())
      self.action.triggered.connect(self.run)
      self.iface.addPluginToMenu('&Save Attributes', self.action)
      self.iface.addToolBarIcon(self.action)

    def unload(self):
      QgsApplication.processingRegistry().removeProvider(self.provider)
      self.iface.removeToolBarIcon(self.action)
      self.iface.removePluginMenu('&Save Attributes', self.action)  
      del self.action

    def run(self):
      processing.execAlgorithmDialog('save_attributes:save_attributes')

logo.png

Copy the logo file from your data package.

The plugin folder should look like below.

Restart QGIS and enable the plugin from Plugins → Manage and Install plugins… → Installed → Save Attributes

Once loaded, you can click on the toolbar button or Plugin → Save Attributes → Save Attributes As CSV to launch the processing algorithm.

12. QGIS Actions

Actions in QGIS provide a quick and easy way to trigger custom behavior in response to a user’s action - such as click on a feature in the canvas or an attribute value in the attribute table. Actions are defined for a layer and are saved in QGIS projects. This makes actions the easiest way to add and distribute QGIS customizations without having to write plugins.

Watch the Video

Watch the Video ↗

View the Presentation ↗

12.1 Hello World

Let’s get started by learning the basics of QGIS Actions. We will create an action that takes a layer of all countries in the world and allows you to extract any country polygon by clicking on it.

  1. Open QGIS. QGIS comes with a few hidden Easter Eggs that can be triggered by typing a keyword in the Coordinate box at the bottom of the main window. While many of these are for fun, some provide useful functionality. One such hidden feature is the ability to load a world map by typing a keyword. Enter the keyword world into the coordinate box at the bottom of the QGIS window rm and click Enter.

  1. A new layer named World Map will be added to the Layers panel. This layer is the Admin0 - Countries boundaries dataset from Natural Earth. Let’s define an action on this layer. Right-click the layer and select Properties.

  1. Switch to the Actions tab and click the Add a new action (+) button.

  1. Select Python as the Type. Enter Hello World as the Description. This will be the name of the action as it appears in various menu items. Leave the Action Scopes to the default selected values of Feature and Canvas. Under the Action Text enter the following Python code and click OK.
print('Hello World')

  1. Click OK again to get back to the main QGIS Canvas. We now have an action that will print Hello World when we click on a feature. To see the output, open the Python Console from Plugins → Python Console. Locate the Actions button on the Attributes Toolbar. Click the dropdown menu next to it and select Hello World.

  1. Once the action is selected, click on any feature from the layer. You will see Hello World printed in the console.

  1. You now know how to define and trigger a Python action. Let’s make the action more useful. Instead of printing a static text, let’s modify the action to print the name of the country where we clicked. Open the Attribute Table of the World Map layer. You will notice that the NAME attribute contains the country names.

  1. Right click the World Map layer and select Properties. From the Actions tab, double click the already defined Hello World action.

  1. Our action is triggered by clicking on a feature and we can access the attributes of that feature. Select the NAME attribute and click Insert.

  1. You will see the value [%NAME%] entered in the Action Text text box. This is a special expression syntax which indicates that the value surrounded by [% and %] will be replaced with the value of the attribute from the feature when the action is triggered.

  1. Update the code to print the attribute value as below.
print('[%NAME%]')

  1. Click OK and get back to the main window. Select the action and click on any feature. You will now see the name of the country printed in the console.

  1. Along with all the attributes of the clicked feature, we also have access to other project and layer variables. Update the code as below to print the values of the feature id and layer id along with the value of the NAME attribute. Note that we are using the Python f-strings for formatting the output.
feature_name = '[%NAME%]'
feature_id = [%$id%]
layer_id = '[%@layer_id%]'
print(f'feature name: {feature_name}')
print(f'feature id: {feature_id}')
print(f'layer id: {layer_id}')

  1. Click OK and try the action again. This time you will see the feature id and layer id printed along with the feature name.

  1. Now you know how to access the @layer_id and $id values of the current feature. We can now use it to extract the current feature and create a new layer from it. Replace the Action Text with the following code which uses QgsFeatureSource.materialize() method to create a new memory based vector layer with the query containing the feature id.
feature_name = '[%NAME%]'
feature_id = [%$id%]
layer_id = '[%@layer_id%]'

layer = QgsProject.instance().mapLayer(layer_id)
new_layer = layer.materialize(
    QgsFeatureRequest().setFilterFids([feature_id]))
new_layer.setName(feature_name)
QgsProject.instance().addMapLayer(new_layer)

  1. Go back to the main Canvas and close the Python Console. Select the Hello World action and click on any feature. You will see a new layer added to the Layers panel that contains the feature. Really useful!

  1. We have a small problem. As the new layer is created and added to the Layers panel, it becomes the active layer. To trigger the action again, we have to select the World Map layer again. To prevent this, we can use QgisInterface.setActiveLayer() method to set the current layer as the active layer. We import iface in the code to access the current instance of the QgisInterface class.
from qgis.utils import iface

feature_name = '[%NAME%]'
feature_id = [%$id%]
layer_id = '[%@layer_id%]'

layer = QgsProject.instance().mapLayer(layer_id)
new_layer = layer.materialize(
    QgsFeatureRequest().setFilterFids([feature_id]))
new_layer.setName(feature_name)
QgsProject.instance().addMapLayer(new_layer)
iface.setActiveLayer(layer)

  1. Now you are able to click around and extract multiple countries without having to manually switch layers.

Exercise 10

Update the action to display an Info message on the QGIS message bar as shown below.

Hint: You can use iface.messageBar().pushInfo() method to display a message.

12.2 Selecting Neighbors

In this section, we will work with a dataset of US States and implement QGIS Actions for selecting easily selecting neighbors of any state.

  1. Locate the neighbors.gpkg from the data package in the Browser panel. Expand it and click on the neighbors.qgz project to open it.

  1. Right-click the states layer and select Properties.

  1. Switch to the Actions tab and click the Add a new action (+) button. Select Python as the Type. Enter Select First-degree Neighbors as the Description. Leave the Action Scopes to the default selected values of Feature and Canvas. Under the Action Text enter the following Python code and click OK. Click OK again to get back to the main QGIS Canvas.

layer_id = '[%@layer_id%]'
fid = [% $id %]

layer = QgsProject.instance().mapLayer(layer_id)

def get_neighbors(fid):
    f = layer.getFeature(fid)
    # Use list comprehension to get all intersecting features
    # You may also use touches() if your data is topologically correct
    # Supply the bounding box to getFeatures() to use Spatial Index
    neighbors = [
        c.id()
        for c in layer.getFeatures(f.geometry().boundingBox())
        if c.geometry().intersects(f.geometry()) and c.id() != f.id()
    ]
    return neighbors


first_degree_neighbors = get_neighbors(fid)

# Apply the selection
layer.selectByIds(first_degree_neighbors)
  1. Locate the Actions button on the Attributes Toolbar. Trigger the action Select First-degree Neighbors and click on any states. You will see all the neighbors selected.

  1. We can add one more action to select second-degree neighbors (i.e. neighbors of neighbors). Right-click the states layer and select Properties. Switch to the Actions tab and click the Add a new action (+) button. Select Python as the Type. Enter Select Second-degree Neighbors as the Description. Leave the Action Scopes to the default selected values of Feature and Canvas. Under the Action Text enter the following Python code and click OK. Click OK again to get back to the main QGIS Canvas.

layer_id = '[%@layer_id%]'
fid = [% $id %]

layer = QgsProject.instance().mapLayer(layer_id)

def get_neighbors(fid):
    f = layer.getFeature(fid)
    # Use list comprehension to get all intersecting features
    # You may also use touches() if your data is topologically correct
    # Supply the bounding box to getFeatures() to use Spatial Index
    neighbors = [
        c.id()
        for c in layer.getFeatures(f.geometry().boundingBox())
        if c.geometry().intersects(f.geometry()) and c.id() != f.id()
    ]
    return neighbors


first_degree_neighbors = get_neighbors(fid)

second_degree_neighbors = set()

for n in first_degree_neighbors:
    neighbors = get_neighbors(n)
    second_degree_neighbors.update(neighbors)

# Remove all first-degree neighbors from the set
second_degree_neighbors = second_degree_neighbors.difference(
    set(first_degree_neighbors))
    
# Remove the feature itself from the set if it exists
second_degree_neighbors.discard(fid)

# Apply the selection
layer.selectByIds(list(second_degree_neighbors))
  1. Locate the Actions button on the Attributes Toolbar. Trigger the action Select Second-degree Neighbors and click on any states. You will see all the neighbors of neighbors selected.

Exercise 11

If you use the action again, the current selection will go away and a new selection will be made. We want to change this behavior by adding to the existing selection. Update the code for the action Select Second-degree Neighbors so when used, it adds the second-degree neighbors to already selected features.

Hint: Supply an additional behavior option for the QgsVectorLayer.selectByIds() method.

12.3 Learn More

If you are intrigued by the power of QGIS Actions, check out our full-length QGIS Actions workshop which cover many more practical examples of automating tasks in QGIS using Actions.

13. Writing Standalone Python Scripts

Having the python script run within QGIS is useful and desired most of the time. But there is a way to write python scripts that run on your system without QGIS being open. Ability to run PyQGIS scripts in a headless mode allows you to automate your workflow and run it on a server without human intervention. Let’s use some QGIS processing algorithms to carry out zonal statistics using a standalone python script.

Watch the Video

Watch the Video ↗

Create a new file with the code below and save it as zonal_stats.py.

import os
from qgis.core import QgsApplication

qgs = QgsApplication([], False)
qgs.initQgis()

import processing
from processing.core.Processing import Processing
Processing.initialize()

data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

vector_layer = 'seismic_zones.shp'
vector_layer_path = os.path.join(data_dir, vector_layer)

raster_layer = 'srtm.tif'
raster_layer_path = os.path.join(data_dir, raster_layer)

# Input vector has invalid geometries
# Fix them first
results = processing.run("native:fixgeometries", {
  'INPUT':vector_layer_path,
  'METHOD': 0,
  'OUTPUT':'TEMPORARY_OUTPUT'})

fixed_vector_layer = results['OUTPUT']

# Run Zonal Statistics

# Save output to a geopackage
output_name = 'seismic_zones_with_elevation.gpkg'
output_path = os.path.join(data_dir, output_name)

processing.run("native:zonalstatisticsfb", {
  'INPUT': fixed_vector_layer,
  'INPUT_RASTER': raster_layer_path,
  'RASTER_BAND':1,
  'COLUMN_PREFIX':'elevation_',
  'STATISTICS':[2],
  'OUTPUT':output_path})

print('Output Layer Created', output_path)
qgs.exitQgis()

You will notice that the script has a few notable parts. First there is the import statement at the top, to explicitly import the required modules. Next, we create an instance of the QgsApplication class and run initQis() method to load the QGIS data providers and layer registry. We also import the Processing module and initialize it. Finally we call exitQgis() to remove them from memory. We have hard-coded the path to the input layers but they can easily be taken as command-line flags using Python’s argparse module. Also we don’t have the QGIS GUI, we have no way of displaying the messages, so we remove those statements.

When you run the script within QGIS, all the paths to QGIS libraries and environment variables are already set and python is able to find and use it. But when you run the script outside of QGIS, you need to set them yourself. Also if you are using Processing Algorithms, you need to make sure the path to core plugin folder is included in the PYTHONPATH environment variable.

Our recommended environment setup takes care of all the requirements and allows you to run PyQGIS scripts easily. The next sections describe this setup and show you how to run the script on various platforms.

You may need to change these paths slightly based on where QGIS is installed. The following scripts assume you are running QGIS-LTR installed at the default location.

Windows Configuration

On Windows, you can do this using a batch file. Create a new file named run_script.bat with the following code. Make sure to save it in the same directory.

@echo off 

:: If you get an error, check that the folder below exists.

set OSGEO4W_ROOT=C:\OSGeo4W

:: The following script will set all the required environment variables.

call "%OSGEO4W_ROOT%\bin\o4w_env.bat" 

:: If you are not using QGIS LTR version, change 'qgis-ltr' to 'qgis' in all lines below

set PATH=%OSGEO4W_ROOT%\bin;%OSGEO4W_ROOT%\apps\qgis-ltr\bin;C:\OSGeo4W64\apps\Qt5\bin;%PATH%
set PYTHONPATH=%OSGEO4W_ROOT%\apps\qgis-ltr\python;%OSGEO4W_ROOT%\apps\qgis-ltr\python\plugins;%PYTHONPATH%
set QGIS_PREFIX_PATH=%OSGEO4W_ROOT%\apps\qgis-ltr
set QT_QPA_PLATFORM_PLUGIN_PATH=%OSGEO4W_ROOT%\apps\Qt5\plugins

:: Verify that the correct python3 binary is being used

for /f %%i in ('where python3') do set current_python=%%i
echo Using python3 from %current_python%

:: Finally run the script
python3 zonal_stats.py

Open the command prompt, browse to the directory with the above files, type run_script.bat and press Enter.

You can also just double-click the run_script.bat to run it, but you will not see any error or success messages. So it is always a good idea to run the script from the shell.

The script will run and produce the output at the given path.

The resulting vector layer has an additional column containing the statistics from the raster.

MacOS Configuration

On MacOS, you can set the required environment variables and run the script using a shell script. Create a file named run_script.sh and save it in the same directory as the script.

OLD_PATH=$PATH
OLD_PYTHONPATH=$PYTHONPATH

QGIS_VERSION="QGIS-LTR"

export PATH=/Applications/$QGIS_VERSION.app/Contents/MacOS/bin:$PATH
export PYTHONPATH=/Applications/$QGIS_VERSION.app/Contents/Resources/python/:/Applications/$QGIS_VERSION.app/Contents/Resources/python/plugins
export QGIS_PREFIX_PATH=/Applications/$QGIS_VERSION.app/Contents/MacOS
export QT_QPA_PLATFORM_PLUGIN_PATH=/Applications/$QGIS_VERSION.app/Contents/PlugIns/platforms/
export DYLD_INSERT_LIBRARIES=/Applications/$QGIS_VERSION.app/Contents/MacOS/lib/libsqlite3.dylib
echo "Using python3 from $(which python3)"

python3 zonal_stats.py

# restore and clean up
export PATH=$OLD_PATH
export PYTHONPATH=$OLD_PYTHONPATH
unset QT_QPA_PLATFORM_PLUGIN_PATH
unset DYLD_INSERT_LIBRARIES

Open a Terminal and browse to the directory with the script. Type bash run_script.sh and press enter. The script will run and produce the output at the given path.

Supplement

This section contains code snippets for common PyQGIS operations.

All code snipets assume you have loaded the sf.qgz project. You can load the project using the code below.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
project = QgsProject.instance()
project_name = 'sf.qgz'
project_path = os.path.join(data_dir, project_name)
project.read(project_path)

Create a Geodesic Line

Connects 2 points using the great circle arc along an ellipsoid. Uses the geodesicLine() method provided by the QgsDistanceArea() class for the computation.

from qgis.core import QgsDistanceArea

new_york = (40.661, -73.944)
london = (51.5072, 0.1276)

lat1, lon1 = new_york
lat2, lon2 = london

# Remember the order is X,Y
point1 = QgsPointXY(lon1, lat1)
point2 = QgsPointXY(lon2, lat2)

d = QgsDistanceArea()
d.setEllipsoid('WGS84')

# Create a geodesic line with vertices every 100km
vertices = d.geodesicLine(point1, point2, 100000)

# Create a polyline from the vertices
# The method returns 1 geometry unless the line crosses antimeridien
geodesic_line = QgsGeometry.fromPolylineXY(vertices[0])

# Create a line layer to display the route
vlayer = QgsVectorLayer('LineString?crs=EPSG:4326', 'route', 'memory')
provider = vlayer.dataProvider()

f = QgsFeature()
f.setGeometry(geodesic_line)
provider.addFeature(f)
vlayer.updateExtents() 
QgsProject.instance().addMapLayer(vlayer)

Save Map Rendering as an Image

saveAsImage() method is quick and easy, but you do not have much control over the resulting image. You can’t control the resolution, size or how each layer will be rendered. There is another way to achieve this. You can look at the code for exporting map as an image in QGIS and you will discover 2 classes QgsMapRendererParallelJob and QgsMapRendererSequentialJob that lets you achieve a better result. The code snippet below exports a hi-resolution image of the project.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
image_name = 'sf_hires.png'
image_path = os.path.join(data_dir, image_name)

settings = iface.mapCanvas().mapSettings()
settings.setOutputSize(QSize(1000,1000))

settings.setFlag(QgsMapSettings.DrawLabeling, False)
settings.setFlag(QgsMapSettings.Antialiasing, True)

job = QgsMapRendererSequentialJob(settings)
job.start()
job.waitForFinished()
image = job.renderedImage()
image.save(image_path)

Search for a layer

blocks = QgsProject.instance().mapLayersByName('blocks')[0]

Turn a layer on/off

blocks = QgsProject.instance().mapLayersByName('blocks')[0]
QgsProject.instance().layerTreeRoot().findLayer(blocks.id()).setItemVisibilityChecked(True)

Get all Layers

for layer in QgsProject.instance().mapLayers().values():
    print(layer.name())

Get only checked (visible) Layers

for layer in iface.mapCanvas().layers():
    print(layer.name())

Get only selected Layers

for layer in iface.layerTreeView().selectedLayers():
  print(layer.name())

Set Canvas Extent to a Layer Extent

Select one of the layers in the Layers panel and run the following code.

layer = iface.activeLayer()
mc = iface.mapCanvas()
mc.setExtent(layer.extent())
mc.refresh()

Get Layers with Hierarchy

This code snippet is taken from the Cheat Sheet for PyQGIS , but contains an important modification. If you notice carefully, the function getGroupLayers is called recursively from within getGroupLayers. This allows one to even get layers that have sub-groups within layer groups.

def getGroupLayers(group):
    print('- group:' + group.name())
    for child in group.children():
        if isinstance(child, QgsLayerTreeGroup):
            getGroupLayers(child)
        else:
            print('  - layer:' + child.name())


root = QgsProject.instance().layerTreeRoot()
for child in root.children():
    if isinstance(child, QgsLayerTreeGroup):
        getGroupLayers(child)
    elif isinstance(child, QgsLayerTreeLayer):
        print ("- layer: " + child.name())

Remove a Specific Button from a Toolbar

The following code snippet shows how once can find names of different widget in the QGIS Application and locate specific toolbars and buttons. The following code will disable to Deselect All Features from Layers button from the Selection Toolbar.

# Find all child objects of the mainWindow that are type QToolbar
for x in iface.mainWindow().findChildren(QToolBar): 
    print(x.objectName())
    
# In the printed list, we observe that 
# the selection toolbar is named mSelectionToolBar
# Get a reference to it by name
selectionToolbar = iface.mainWindow().findChild(QToolBar,'mSelectionToolBar')

# Find the buttons on the toolbar
for x in selectionToolbar.findChildren(QAction):
    print(x.objectName())

# Alternatively, you can also use the following
for x in selectionToolbar.actions():
    print(x.objectName())

# If the toolbar button is not an action, you
# We know the name of the deselection button 
# Get a reference to it by name
deselection = selectionToolbar.findChild(QAction,'ActionDeselection')

# We can disable a widget by calling setEnabled(False)
deselection.setEnabled(False)

# We can remove it completely by calling removeAction()
selectionToolbar.removeAction(deselection)

Add a Drop-down Menu to Toolbar

This example shows how to add a QCombobox widget to a toolbar and populate it with attribute values from a layer. On click of the button, the selection is read from the combo box and used to apply a filter to the layer

You must have a layer with an attribute called NAME for this example to work. Tested with the Natural Earth Admin 0 - Countries layer.

toolBar = iface.addToolBar("My Toolbar")
toolBar.setObjectName("My Toolbar")

countryCombo = QComboBox(toolBar)
toolBar.addWidget(countryCombo)
countryCombo.setToolTip("Select a country")


filterButton = QPushButton('Filter')
filterButton.setToolTip('Filter')
toolBar.addWidget(filterButton)

resetButton = QPushButton('Reset')
resetButton.setToolTip('Reset')
toolBar.addWidget(resetButton)

# We want to sort the layer by country names
# configure QgsFeatureRequest and use it with getFeatures()
request = QgsFeatureRequest()
clause = QgsFeatureRequest.OrderByClause('NAME')
orderby = QgsFeatureRequest.OrderBy([clause])
request.setOrderBy(orderby)
layer = iface.activeLayer()

# Read the country names and save in a list
countries = []
for f in layer.getFeatures(request):
  name = f['NAME']
  if name:
    countries.append(name)

# Add items to the combobox
for country in countries:
    countryCombo.addItem(country)

# Define functions to apply and reset filters
def filter_layer():
    layer = iface.activeLayer()
    country = countryCombo.currentText()
    expression = '\"NAME\" = \'{}\''.format(country)
    layer.setSubsetString(expression)

def reset_layer():
    layer = iface.activeLayer()
    layer.setSubsetString('')
    countryCombo.clear()
    for country in countries:
      countryCombo.addItem(country)
      
filterButton.clicked.connect(filter_layer)
resetButton.clicked.connect(reset_layer)

Adding CSV Layers

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

filename = 'trees.csv'
csvpath = 'file:///' + data_dir + filename
uri = '{}?type=csv&xField={}&yField={}&crs={}'.format(
  csvpath, 'Longitude', 'Latitude', 'EPSG:4326')
iface.addVectorLayer(uri, 'trees', 'delimitedtext')

Inserting Layers in the Layer Tree

We can use the QgsLayerTree class to insert the layer at an appropriate place.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

filename = 'srtm.tif'
uri = os.path.join(data_dir, filename)
rlayer = QgsRasterLayer(uri, 'srtm', 'gdal')

rastergroup = QgsLayerTreeGroup('raster layers')
treelayer = QgsLayerTreeLayer(rlayer)
rastergroup.insertChildNode(0, treelayer)

root = QgsProject.instance().layerTreeRoot()
root.insertChildNode(1, rastergroup)

Saving Layers to Disk

Use the QgsRasterFileWriter or QgsVectorFileWriter classes for writing layers to disk.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

options = QgsVectorFileWriter.SaveVectorOptions()
options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer 
options.layerName = 'point'

filename = 'sf.gpkg'
path = os.path.join(data_dir, filename)
QgsVectorFileWriter.writeAsVectorFormat(vlayer, path, options)

Displaying a label with a background color

This is a vast topic, but you can get a taste of the flexibility offered by the API to control all aspects of labeling. Notice the class name QgsPalLayerSettings - this is because QGIS uses a labeling library called PAL for labels. The code snippet below shows how to create a label for a point layer with a background color. [^8]

vlayer = QgsProject.instance().mapLayersByName('point')[0]
symbol = QgsMarkerSymbol.createSimple({'name': 'square', 'color': 'red'})
vlayer.renderer().setSymbol(symbol)

label_settings = QgsPalLayerSettings()
#label_settings.drawBackground = True
label_settings.fieldName = 'name'

text_format = QgsTextFormat()
background_color = QgsTextBackgroundSettings()
background_color.setFillColor(QColor('white'))
background_color.setEnabled(True)
text_format.setBackground(background_color )
label_settings.setFormat(text_format)

vlayer.setLabeling(QgsVectorLayerSimpleLabeling(label_settings))
vlayer.setLabelsEnabled(True)
vlayer.triggerRepaint()

Edit Attribute Table of a Vector Layer

When you use processing, a new layer is created by each algorithm. This example shows, how to use processing to overwrite the original layer with the results of processing.

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

filename = 'sf.gpkg|layername=blocks'
uri = os.path.join(data_dir, filename)
blocks = QgsVectorLayer(uri, 'blocks', 'ogr')

output = processing.run(
    "qgis:deletecolumn", 
    {'INPUT': blocks,'COLUMN':['multigeom'],'OUTPUT':'memory:'})
outputlayer = output['OUTPUT']

final = processing.run("qgis:fieldcalculator",
    {'INPUT':outputlayer,
    'FIELD_NAME':'area',
    'FIELD_TYPE':0,
    'FIELD_LENGTH':10,
    'FIELD_PRECISION':3,
    'NEW_FIELD':True,
    'FORMULA':'$area',
    'OUTPUT':'memory:'})
finallayer = final['OUTPUT']

options = QgsVectorFileWriter.SaveVectorOptions()
# We overwrite the original layer
options.layerName = 'blocks'
options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer 

output_file = 'sf.gpkg'
output_path = os.path.join(data_dir, output_file)
QgsVectorFileWriter.writeAsVectorFormat(finallayer, output_path, options)
QgsProject.instance().reloadAllLayers()

Creating a PDF with Title

import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')

project = QgsProject.instance()
extent = QgsRectangle(-122.52, 37.71, -122.35, 37.83)

layout = QgsPrintLayout(project)
layout.initializeDefaults()

pages = layout.pageCollection()
pages.beginPageSizeChange()
page = pages.page(0)
page.setPageSize('A4',  QgsLayoutItemPage.Landscape)
pages.endPageSizeChange()
page_center = page.pageSize().width() / 2


map = QgsLayoutItemMap(layout)
map.setRect(QRectF(-122.52, 37.71, -122.35, 37.83))
map.setExtent(extent)
a4 = QPageSize().size(QPageSize.A4, QPageSize.Millimeter)
map.attemptResize(QgsLayoutSize(a4.height(),  a4.width()))

layout.addItem(map)
title = QgsLayoutItemLabel(layout)
title.setText('San Francisco')
title.setFont(QFont('Arial', 36))
title.adjustSizeToText()
title.setReferencePoint(QgsLayoutItem.UpperMiddle)
title.attemptMove(QgsLayoutPoint(page_center, 10))
layout.addItem(title)

output_file = 'sf.pdf'
output_path = os.path.join(data_dir, output_file)

exporter = QgsLayoutExporter(layout)
exporter.exportToPdf(
    output_path, QgsLayoutExporter.PdfExportSettings())

Simplifying Processing Scripts

The Processing Framework providers a simpler way to write processing scripts by using the @alg decorator. Using this approach, you are able to remove a lot of boilerplate code and focus on just writing the main algorithm function. There are some limitations of this approach - the main one being that scripts written using the @alg decorator cannot be used in plugins. But if your goal is to create a processing scripts, this is a much simpler style.

Note: the @alg decorator cannot be used in plugins. If you are writing processing algorithms for a plugin, use the full script template shown in the previous section.

Let’s modify the save_attributes_algorithm.py script covered in the previous section to use the built-in @alg decorator. Locate the script in Processing Toolbox under Scripts → Save Attributes As CSV. Right-click on it and select Edit Script…. Replace all the code in the Processing Script Editor with the following and click the Save Script button

from qgis.processing import alg

@alg(name='save_attributes', label='Save Attributes As CSV',
    group='', group_label='')
@alg.input(type=alg.SOURCE, name='INPUT', label='Input Layer')
@alg.input(type=alg.FILE_DEST, name='OUTPUT', label='Output File')
def processAlgorithm(instance, parameters, context, feedback, inputs):
    """Saves the attributes of a vector layer to a CSV file."""
    source = instance.parameterAsSource(parameters, 'INPUT', context)
    csv = instance.parameterAsFileOutput(parameters, 'OUTPUT', context)

    fieldnames = [field.name() for field in source.fields()]

    # Compute the number of steps to display within the progress bar and
    # get features from source
    total = 100.0 / source.featureCount() if source.featureCount() else 0
    features = source.getFeatures()

    with open(csv, 'w') as output_file:
      # write header
      line = ','.join(name for name in fieldnames) + '\n'
      output_file.write(line)
      for current, f in enumerate(features):
          # Stop the algorithm if cancel button has been clicked
          if feedback.isCanceled():
              break

          # Add a feature in the sink
          line = ','.join(str(f[name]) for name in fieldnames) + '\n'
          output_file.write(line)

          # Update the progress bar
          feedback.setProgress(int(current * total))

    return {'OUTPUT': csv}

You can run the script and see that the behavior is almost identical but the code is much simpler. The code uses the @alg decorator for defining algorithm name, group, inputs and outputs in a more concise way.

Another important thing to note is that to create a processing algorithm, one must create a class that inherits from QgsProcessingAlgorithm. When you write a class, you refer to the current instance of the class with the keyword self. Here you are just writing a function, and the decorator will create a class for you. So we are using a parameter named instance. When the decorator will create a class, the reference to the instance will be passed on to our function as this parameter.

Back to Top

What next?

We have several step-by-step tutorials and guides on PyQGIS development. You can do those tutorials to practice your skills and build confidence.

Tutorials

Guides

Resources

Data Credits

License

This course material is licensed under a Creative Commons Attribution 4.0 International (CC BY 4.0). You are free to re-use and adapt the material but are required to give appropriate credit to the original author as below:

Customizing QGIS with Python Course by Ujaval Gandhi www.spatialthoughts.com


This course is offered as an instructor-led online class. Visit Spatial Thoughts to know details of upcoming sessions.


© 2020 Spatial Thoughts www.spatialthoughts.com


  1. See https://qgis.org/api/3.4/classQgsProject.html↩︎


If you want to report any issues with this page, please comment below.