This workshop is designed to help you take your first steps in building QGIS plugins. We will understand the QGIS plugin architecture and see how you can package your code and distribute it as a plugin.
This workshop requires QGIS LTR version 3.34. Please review QGIS-LTR Installation Guide for step-by-step instructions.
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 workshop. 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.
Replace by space
. Python
is very sensitive about whitespace and this setting will ensure tabs and
spaces are treated properly.We have created a data package containing ready-to-use plugin files for each section. You can download the data package from qgis_plugin_workshop.zip. Once downloaded, unzip the contents to a folder on your computer.
The workshop will teach you how to build a plugin named Basemap Loader that adds a toolbar to QGIS allowing you to pick and easily load your favorite basemaps.
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. 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.
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
__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.main.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')
__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
basemap_loader
. Copy the logo.png
file from
your data package and copy to this folder.{profile folder}/python/plugins
. Copy the entire folder to
this directory.Your data package contains a folder
basemap_loader_minimal
with all the required files created
for this section. You may use them directly to catch up to this point in
the workshop.
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.
main.py
file with the content from
below.main.py
import os
import inspect
from PyQt5.QtWidgets import QAction
from PyQt5.QtGui import QIcon
from qgis.core import QgsRasterLayer, QgsProject, QgsCoordinateReferenceSystem
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')
Your data package contains a folder
basemap_loader_complete
with all the required files created
for this section. You may use them directly to catch up to this point in
the workshop.
We will now add some user-interface widget to the plugin. Instead of a button that loads a single basemap, we can build a toolbar that has a drop-down menu of many options that the user can pick from and load the basemap.
For a comprehensive list of basemap tile-services and their URLs, you can see the Contextily Providers page.
main.py
file with the content from
below.main.py
import os
import inspect
from PyQt5.QtWidgets import QAction, QComboBox, QLabel, QPushButton
from PyQt5.QtGui import QIcon
from qgis.core import QgsRasterLayer, QgsProject, QgsCoordinateReferenceSystem
cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0]
# We create a dictionary of all the basemaps and their URLs to be used
osm = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
cartodb_darkmatter = 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'
cartodb_positron = 'https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'
esri_shaded_relief = 'https://server.arcgisonline.com/ArcGIS/rest/services/' \
'World_Street_Map/MapServer/tile/{z}/{y}/{x}'
BASEMAPS = {
'OpenStreetMap': osm,
'CartoDB DarkMatter': cartodb_darkmatter,
'CartoDB Positron': cartodb_positron,
'Esri World Shaded Relief': esri_shaded_relief
}
class BasemapLoaderPlugin:
def __init__(self, iface):
self.iface = iface
def initGui(self):
# Create a Toolbar
self.basemapToolbar = self.iface.addToolBar('Basemap Selector')
# Create an Action with Logo
icon = os.path.join(os.path.join(cmd_folder, 'logo.png'))
self.action = QAction(QIcon(icon), 'Load Basemap', self.basemapToolbar)
# Create a label
self.label = QLabel('Select a basemap', parent=self.basemapToolbar)
# Create a dropdown menu
self.basemapSelector = QComboBox(parent=self.basemapToolbar)
self.basemapSelector.setFixedWidth(150)
# Populate it with names of all the basemaps
for basemap_name in BASEMAPS.keys():
self.basemapSelector.addItem(basemap_name)
# Add all the widgets to the toolbar
self.basemapToolbar.addWidget(self.label)
self.basemapToolbar.addWidget(self.basemapSelector)
self.basemapToolbar.addAction(self.action)
# Connect the run() method to the action
self.action.triggered.connect(self.run)
def unload(self):
del self.basemapToolbar
def run(self):
selected_basemap = self.basemapSelector.currentText()
basemap_url = BASEMAPS[selected_basemap]
zmin = 0
zmax = 19
crs = 'EPSG:3857'
uri = f'type=xyz&url={basemap_url}&zmax={zmax}&zmin={zmin}$crs={crs}'
rlayer = QgsRasterLayer(uri, selected_basemap, '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')
Your data package contains a folder basemap_loader_ui
with all the required files created for this section. You may use them
directly to catch up to this point in the workshop.
If you enjoyed this workshop, check out our full course PyQGIS Masterclass which covers the entire PyQGIS API in a structured manner.
This workshop 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:
Building Your First QGIS Plugin Workshop by Ujaval Gandhi www.spatialthoughts.com
© 2024 Spatial Thoughts www.spatialthoughts.com
If you want to report any issues with this page, please comment below.