Difference between revisions of "Creating a QGIS Plugin"
JohnFoster (talk | contribs) |
JohnFoster (talk | contribs) |
||
Line 134: | Line 134: | ||
− | 8. After clicking '''Generate''' Plugin Builder will create the required files and attempt to compile them. If we get a warning indicating that '''pyrcc5 was not found in your path''' click '''OK''' and use step 9 to resolve this issue - if no warnings popped up then we can skip ahead to step 10. Either way, a congratulatory report will be printed to the screen which will provide us with some guidance on what to do next. |
+ | 8. After clicking '''Generate''', Plugin Builder will create the required files and attempt to compile them. If we get a warning indicating that '''pyrcc5 was not found in your path''' click '''OK''' and use step 9 to resolve this issue - if no warnings popped up then we can skip ahead to step 10. Either way, a congratulatory report will be printed to the screen which will provide us with some guidance on what to do next. |
[[File:Jf_plugin_builder_report.png|The results of the plugin template generation.]] |
[[File:Jf_plugin_builder_report.png|The results of the plugin template generation.]] |
Revision as of 21:01, 1 October 2021
Contents
- 1 Disclaimer
- 2 Introduction
- 3 Tutorial
- 4 Conclusion
- 5 Code
Disclaimer
This tutorial is assumes a basic knowledge of GIS environments and Python syntax. Originally authored by Michael Wray in 2019, the current version of this tutorial includes revisions made by John Foster in September, 2021. Please see the page history for the changelog.
The following versions were used:
- Windows 10
- QGIS 3.16.11-Hannover LTR (OSGeo4W Network Installer)
- QT Designer 5.15.2 (OSGeo4W Network Installer)
- Python 3.9.5 (OSGeo4W Network Installer)
- Plugin Builder 3.2.1 (QGIS Plugin)
- Plugin Reloader 0.8.2 (QGIS Plugin)
Introduction
QGIS is a free and open source geographic information system (GIS) that is developed and supported by thousands of users and organizations around the world. It's core features include a suite of vector and raster tools that are compatible with many of the most popular geospatial file formats. Additionally, GDAL, GRASS, and SAGA integration leverages the functionality of these powerful packages to make QGIS one of the most complete GIS environments available. Users who wish to expand upon this already extensive default toolset can search through the official QGIS plugin repository (1508 plugins at the time of writing) to see if any published 3rd-party tools will meet their analysis needs. Written in the Python or C++ languages, QGIS plugins can be kept in a private repository, or contributed to the public one.
Three different types of plugin templates can be created in QGIS:
- Tool button with dialog: A plugin accessed from the plugin menu or tool bar that operates within a dialog box
- Tool button with dock widget: A plugin accessed from the plugin menu or tool bar that operates within a dock widget (i.e. a dockable panel within the QGIS desktop environment)
- Processing provider: A plugin accessed from the QGIS Processing Toolbox
In this tutorial we will work through the process of creating a simple Tool button with dialog plugin called DisplayInfo. While the functionality of the plugin itself is quite limited (it prints a layer's file path, extent, and coordinate reference system), following the steps in this guide will provide a starting point for development of more advanced Python plugins.
Please note that this tutorial does not intend to guide you through the process of writting Python code for your own plugin, nor does it include complete instructions on the use of QT Designer. What it does attempt to accomplish is to demonstrate the various steps necessary to generate a plugin template, create a simple user interface, and point out where to insert the necessary Python code.
An overview of the steps we will be taking:
- Install required software
- Find our QGIS user profile directory
- Use Plugin Builder to generate the plugin template
- Create the user interface in QT Designer
- Edit the plugin template to include our Python script
- Test the functional plugin
Tutorial
Install Required Software
If you are reading this tutorial you are likely already a QGIS user and have some or all of the required software. To ensure we are on the same page, here are the software requirements for this tutorial.
QGIS
QGIS can be installed as a standalone application or through the OSGeo4W Network Installer. This OSGeo4W installer is the recommended installation for regular users and it contains the current latest release as well as the current long term release (LTR). Due to its stability and the availability of documentation the current LTR version of QGIS (QGIS 3.16.11) has been used in this tutorial. It is likely that these instructions will work for other QGIS installations but some steps may need to be modified.
Please refer to the following page for download and installation instructions:
QT Designer
QGIS was developed using the QT toolkit to create its graphical user interfaces. QT Designer is an application that is used to design these interfaces and comes packaged with QGIS installers. Depending on how you installed QGIS you can find QT Designer in the following locations:
- Standalone installation:
C:\Program Files\QGIS 3.16.11\apps\qt5\bin\designer.exe
- OSGeo4W installation:
C:\OSGeo4W\apps\Qt5\bin\designer.exe
Text Editor
A text editor is required to make changes to the plugin's Python source code. Notepad, IDLE, or a full fledged editor such as Microsoft Visual Studio Code will all suffice.
Download and installation instructions for Microsoft Visual Studio Code can be found here:
Plugin Builder and Plugin Reloader
This tutorial requires two QGIS plugins:
- Plugin Builder: Generates our plugin template
- Plugin Reloader: Removes the need to restart QGIS every time a plugin's source code is changed
Installation:
- Open QGIS and navigate to Plugins > Manage and Install Plugins...
- Select the All tab to search the plugin repository
- Search for and install Plugin Builder 3 and Plugin Reloaded
Once installed they should both be immediately accessible through the Plugins menu.
Find Our QGIS User Profile Directory
At several different points in the tutorial we will need to access the directory where QGIS stores its plugins so let's figure out where that is for easy access later on.
QGIS installations will create a default user profile directory and store that user's plugins here: C:\Users\{user name}\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins\
Each plugin will have it's own directory within \plugins\
, and it is here that we will generate the template for our Display Info plugin.
A quick way to access this directory is to open QGIS, and then in the menu bar click Settings > User Profiles > Open Active Profile Folder. Windows Explorer should then open in that directory. From here we can then navigate to the \python\plugins\
subdirectory. Directories for any Python plugins we have installed should be visible here. Keep this window open for later access.
Use Plugin Builder to Generate the Plugin Template
As mentioned, we will use Plugin Builder to generate a template for our plugin. Officialy documentation for the QGIS plugin can be found here: QGIS Plugin Builder.
1. Open QGIS and in the menu bar navigate to Plugins > Plugin Builder > Plugin Builder. The Plugin Builder dialog box should open. It is a "wizard" featuring a number of different screens.
2. The various fields that need to be completed are quite self-explanatory but full details can be found in the official documentation. For this tutorial, make sure that Class name, Plugin name, and Module name are all written as shown in figure 1 - the other fields are less important.
3. Provide a brief description of the plugin and its purpose.
4. It's on this screen where we select the type of the plugin template and some parameters relating to it. Display Info will be a dialog box plugin rather than a dock widget or processing tool so ensure Tool button with dialog
is selected as the Template . For the Text for the menu item field write Output layer info to a text file
. For Menu choose Plugins
.
5. This next screen has a number of check boxes which correspond to optional plugin components which can be generated. Let's leave them all checked so we can see the entire template that Plugin Builder can create.
6. Here we can submit the paths that are required for publication. Since we won't be publishing our plugin we can leave the default values.
7. On this last screen we need to select the output directory for the plugin template files. We can either save them to a temporary directory and then move them later, or we can generate them in the correct place right away. Let's go with the latter option. In Plugin Builder, copy and paste the path to the plugins directory which we determined in earlier. Click Generate when ready.
8. After clicking Generate, Plugin Builder will create the required files and attempt to compile them. If we get a warning indicating that pyrcc5 was not found in your path click OK and use step 9 to resolve this issue - if no warnings popped up then we can skip ahead to step 10. Either way, a congratulatory report will be printed to the screen which will provide us with some guidance on what to do next.
9. If the pyrcc5 compiler was not found in your path warning did pop up then we will need to resolve this issue in order to compile the plugin template. Plugin Builder needs to be able to access the relevant Python bindings from the plugin folder, so in order to do this the path to the QGIS install must be indicated.
To do this, we can create the following Windows Batch File (.bat
) in our code editor: (note: if you installed QGIS to a different path, replace C:\OSGeo4W64\bin\
with the location of your install):
@echo off
call "C:\OSGeo4W64\bin\o4w_env.bat"
call "C:\OSGeo4W64\bin\qt5_env.bat"
call "C:\OSGeo4W64\bin\py3_env.bat"
@echo on
pyrcc5 -o resources.py resources.qrc
Note: if you installed QGIS to a different path, replace C:\OSGeo4W64\bin\
with the location of your install.
Save it to the directory where our new plugin was just created and name it compile.bat
. Run the file by double-clicking it - a new file called resources.qrc
should appear. compile.bat
can then be deleted.
10. After successfully generating the plugin template we should end up with a number of files and folders within our \plugins\display_info\
directory. Many of these are related to those optional components which we left selected in step 5. The two most important files for us in this tutorial are display_info.py
and display_info_dialog_base.ui
- we will edit both in the subsequent steps.
11. Another way to confirm that our plugin template has compiled successfully is to see if it can be opened in QGIS. To do this we need to restart QGIS and then select Plugins > Manage and Install Plugins... from the menu bar. When the Plugins dialog box opens select Installed from the menu on the left in order to see all of our installed plugins. Display Info should appear in the list but we need to click the check-box to activate it. Once activated an icon Display Info should appear on one of the toolbars and it will now be listed under Plugins in the menu bar.
When we open it up we see that it has a blank interface. In the next section we will create the graphical user interface.
Create the User Interface in QT Designer
QT Designer is an application for creating user interfaces based on QT widgets. We will use it to create a UI for our new Display Info plugin.
Note: If this section is a struggle feel free to copy and paste the code for the user interface from the display_info_dialog_base.ui
file found below.
1. Open up QT Designer and select File > Open... and navigate to the display_info plugin folder that we just generated
2. Open the display_info_dialog_base.ui
file
3. Our blank user interface should appear in a small window. It's actually not completely blank - a QT widget called QDialogButtonBox (the OK and Cancel buttons) should already be present. We will now search for and drag other widgets into the GUI.
4. In the Widget Box panel on the left, search for Combo Box
and drag a Combo Box into the UI. Place it anywhere for now.
5. Search again for Line Edit
and Tool Button
and add both of these QT widgets to the GUI
6. We will also need two text labels so search for Label
and add two Labels to the GUI
7. The five QT widgets we just added to the GUI can be edited with the mouse and keyboard. Change the label's text and use the mouse to rearrange and resize all of the widgets so they look roughly as follows:
8. In the Object Inspector panel, select the lineEdit object. The widget to the right of the Select output file: label will become highlighted in the UI.
9. With the lineEdit object selected, go to the Property Editor panel and scroll down to the placeholderText field in the QLineEdit section. Change this field to read Enter a path and filename
. Hit Enter
on the keyboard to assign this value.
10. The functionality that will be assigned to these different widgets will take place in our Python script and they will be referenced through their Class names as seen in the Object Inspector. More complex plugins will require more work in QT Designer but all we need to do here has been completed.
9. Save the diplay_info_dialog_base.ui
file that we have just been editing and close QT Designer.
10. In QGIS, configure the Plugin Reloader plugin to reload display_info and then reload it.
11. Opening Display Info will now show our updated user interface but none of the widgets will actually do anything until we introduce our Python script to the template. That will come next.
Edit the Plugin Template to Include our Python Script
When we generated our plugin using Plugin Builder we had to specify a module name: display_info
. This became the plugin's Python module (display_info.py
) (where the plugin's actual operations take place) and can be found in our plugin's directory. We will now edit that file to include the code that will make it work. Rather than inserting code in specific places we will just replace all of it's code with what can be found below. But first, here are the instructions:
1. Using our code editor open the display_info.py
Python module. As we will see, the plugin module template already contains a number of function and class definitions
2. Before we replace all of the code here are a few notes that will be important should we want to apply this tutorial to the creation of a different plugin (not Display Info):
- If our plugin makes use of any additional Python packages we need to import them at the start of
new_plugin.py
- where the other packages in the template are imported - Define any functions within the class
NewPlugin
- the most appropriate place to insert these is above the definition ofrun()
and belowunload()
. - The plugin's Python module will interact with the GUI through the calling of the QT Widget class names.
- No doubt there are other important considerations but these notes should help as a starting point.
3. The full code to replicate our Display Info example plugin (found below) only has to be copied and pasted into the display_info.py
file - replacing all lines of that template - and saved in its original location (the .../plugins/display_info/display_info.py
).
Test the Functional Plugin
Now that our GUI and Python module have been made functional we can take it for a test drive.
1. In QGIS, reload Display Info using Plugin Reloader.
2. Add a vector or raster layer to the current project if one has not already been added. The one shown below can be downloaded from here: Wards - Open Ottawa
2. Open Display Info from the Plugins menu.
3. Select the layer from the dropdown menu, specify an output path and filename, and then click OK.
4. Provided there are no issues and all the previous steps were followed, a text file containing some basic information about the selected layer should be generated and that file will also open.
Conclusion
In this tutorial we have worked through the steps required to generate a plugin template in QGIS and then assign some basic functionality to it. By using Plugin Builder plugin, QT Designer, and Python it was relatively straightforward to create a working plugin for QGIS. While we did not share Display Info to the to public QGIS repository The opportunties afforded by QGIS' open-source nature make it possible for users to contribute new plug
Code
display_info_dialog_base.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DisplayInfoDialogBase</class>
<widget class="QDialog" name="DisplayInfoDialogBase">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>466</width>
<height>174</height>
</rect>
</property>
<property name="windowTitle">
<string>Display Info</string>
</property>
<widget class="QDialogButtonBox" name="button_box">
<property name="geometry">
<rect>
<x>80</x>
<y>110</y>
<width>341</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QComboBox" name="comboBox">
<property name="geometry">
<rect>
<x>160</x>
<y>30</y>
<width>261</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QLineEdit" name="lineEdit">
<property name="geometry">
<rect>
<x>162</x>
<y>70</y>
<width>231</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Enter a path and filename</string>
</property>
</widget>
<widget class="QToolButton" name="toolButton">
<property name="geometry">
<rect>
<x>402</x>
<y>70</y>
<width>21</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>80</x>
<y>30</y>
<width>71</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>Select a layer:</string>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>70</x>
<y>70</y>
<width>81</width>
<height>21</height>
</rect>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Output filename:</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>DisplayInfoDialogBase</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>DisplayInfoDialogBase</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>
display_info.py
# -*- coding: utf-8 -*-
"""
/***************************************************************************
DisplayInfo
A QGIS plugin
This plugin displays a layers info in a .txt.
Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
-------------------
begin : 2019-11-09
git sha : $Format:%H$
copyright : (C) 2019 by Mike (revised by John Foster in 2021)
email : ***********
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction, QFileDialog
from qgis.core import QgsProject, Qgis
# Initialize Qt resources from file resources.py
from .resources import *
# Import the code for the dialog
from .display_info_dialog import DisplayInfoDialog
import os.path
class DisplayInfo:
"""QGIS Plugin Implementation."""
def __init__(self, iface):
"""Constructor.
:param iface: An interface instance that will be passed to this class
which provides the hook by which you can manipulate the QGIS
application at run time.
:type iface: QgsInterface
"""
# Save reference to the QGIS interface
self.iface = iface
# initialize plugin directory
self.plugin_dir = os.path.dirname(__file__)
# initialize locale
locale = QSettings().value('locale/userLocale')[0:2]
locale_path = os.path.join(
self.plugin_dir,
'i18n',
'DisplayInfo_{}.qm'.format(locale))
if os.path.exists(locale_path):
self.translator = QTranslator()
self.translator.load(locale_path)
QCoreApplication.installTranslator(self.translator)
# Declare instance attributes
self.actions = []
self.menu = self.tr(u'&Display Info')
# Check if plugin was started the first time in current QGIS session
# Must be set in initGui() to survive plugin reloads
self.first_start = None
# noinspection PyMethodMayBeStatic
def tr(self, message):
"""Get the translation for a string using Qt translation API.
We implement this ourselves since we do not inherit QObject.
:param message: String for translation.
:type message: str, QString
:returns: Translated version of message.
:rtype: QString
"""
# noinspection PyTypeChecker,PyArgumentList,PyCallByClass
return QCoreApplication.translate('DisplayInfo', message)
def add_action(
self,
icon_path,
text,
callback,
enabled_flag=True,
add_to_menu=True,
add_to_toolbar=True,
status_tip=None,
whats_this=None,
parent=None):
"""Add a toolbar icon to the toolbar.
:param icon_path: Path to the icon for this action. Can be a resource
path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
:type icon_path: str
:param text: Text that should be shown in menu items for this action.
:type text: str
:param callback: Function to be called when the action is triggered.
:type callback: function
:param enabled_flag: A flag indicating if the action should be enabled
by default. Defaults to True.
:type enabled_flag: bool
:param add_to_menu: Flag indicating whether the action should also
be added to the menu. Defaults to True.
:type add_to_menu: bool
:param add_to_toolbar: Flag indicating whether the action should also
be added to the toolbar. Defaults to True.
:type add_to_toolbar: bool
:param status_tip: Optional text to show in a popup when mouse pointer
hovers over the action.
:type status_tip: str
:param parent: Parent widget for the new action. Defaults None.
:type parent: QWidget
:param whats_this: Optional text to show in the status bar when the
mouse pointer hovers over the action.
:returns: The action that was created. Note that the action is also
added to self.actions list.
:rtype: QAction
"""
icon = QIcon(icon_path)
action = QAction(icon, text, parent)
action.triggered.connect(callback)
action.setEnabled(enabled_flag)
if status_tip is not None:
action.setStatusTip(status_tip)
if whats_this is not None:
action.setWhatsThis(whats_this)
if add_to_toolbar:
# Adds plugin icon to Plugins toolbar
self.iface.addToolBarIcon(action)
if add_to_menu:
self.iface.addPluginToMenu(
self.menu,
action)
self.actions.append(action)
return action
def initGui(self):
"""Create the menu entries and toolbar icons inside the QGIS GUI."""
icon_path = ':/plugins/display_info/icon.png'
self.add_action(
icon_path,
text=self.tr(u'Output layer info to a text file'),
callback=self.run,
parent=self.iface.mainWindow())
# will be set False in run()
self.first_start = True
def unload(self):
"""Removes the plugin menu item and icon from QGIS GUI."""
for action in self.actions:
self.iface.removePluginMenu(
self.tr(u'&Display Info'),
action)
self.iface.removeToolBarIcon(action)
def select_output_file(self):
filename, _filter = QFileDialog.getSaveFileName(
self.dlg, "Select output filename and destination","layer_info", '*.txt')
self.dlg.lineEdit.setText(filename)
def run(self):
"""Run method that performs all the real work"""
# Create the dialog with elements (after translation) and keep reference
# Only create GUI ONCE in callback, so that it will only load when the plugin is started
if self.first_start == True:
self.first_start = False
self.dlg = DisplayInfoDialog()
self.dlg.toolButton.clicked.connect(self.select_output_file)
# Fetch the currently loaded layers
layers = QgsProject.instance().layerTreeRoot().children()
# Clear the contents of the comboBox from previous runs
self.dlg.comboBox.clear()
# Populate the comboBox with names of all the loaded layers
self.dlg.comboBox.addItems([layer.name() for layer in layers])
# Show the dialog
self.dlg.show()
# Run the dialog event loop
result = self.dlg.exec_()
# See if OK was pressed
if result:
filename = self.dlg.lineEdit.text()
with open(filename, 'w') as output_file:
# Get the selected layer
selectedLayerIndex = self.dlg.comboBox.currentIndex()
selectedLayer = layers[selectedLayerIndex].layer()
layer_source = selectedLayer.dataProvider().dataSourceUri() + '\n' #QgsProject.instance().readPath("./") + '\n'
# Get the selected layer's extent
layer_extent = selectedLayer.extent()
xmin = str(layer_extent.xMinimum())
xmax = str(layer_extent.xMaximum())
ymin = str(layer_extent.yMinimum())
ymax = str(layer_extent.yMaximum())
# Get the selected layer's CRS ID and description
layer_CRS_id = selectedLayer.crs().authid()
layer_CRS_description = selectedLayer.crs().description()
# Output the info to the text file
output_file.write("Filepath: " + layer_source)
output_file.write("X-min: " + xmin +"m" + '\n')
output_file.write("X-max: " + xmax +"m"+ '\n')
output_file.write("Y-min: " + ymin +"m" +'\n')
output_file.write("Y-max: " + ymax +"m"+ '\n')
output_file.write("CRS: " + layer_CRS_id +" - " + layer_CRS_description)
output_file.close()
# Open the text file
os.startfile(filename)
# Generate a message after running the plugin
self.iface.messageBar().pushMessage(
"Success", "Output file written at " + filename,
level=Qgis.Success, duration=3)