Difference between revisions of "Creating a QGIS Plugin"

From CUOSGwiki
Jump to navigationJump to search
m
 
(35 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
== Disclaimer ==
 
== Disclaimer ==
This tutorial is intended for those running QGIS on the Windows 10 operating system and it 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.
+
This tutorial assumes a basic knowledge of GIS environments and Python syntax. Originally authored by a student 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:'''
 
The following program versions were used:
 
   
 
* Windows 10
 
* Windows 10
* QGIS 3.16.11-Hannover LTR (Long Term Release)
+
* QGIS 3.16.11-Hannover LTR (OSGeo4W Network Installer)
  +
* QT Designer 5.15.2 (OSGeo4W Network Installer)
* Plugin Builder 3.2.1
 
  +
* Python 3.9.5 (OSGeo4W Network Installer)
* Plugin Reloader 0.8.2
 
  +
* Plugin Builder 3.2.1 (QGIS Plugin)
* QT Designer 5.15.2
 
  +
* Plugin Reloader 0.8.2 (QGIS Plugin)
* Python 3.9.5
 
  +
   
 
== Introduction ==
 
== Introduction ==
   
[https://qgis.org/en/site/ QGIS] is a free and open source [https://en.wikipedia.org/wiki/QGIS 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, [https://en.wikipedia.org/wiki/GDAL GDAL], [https://en.wikipedia.org/wiki/GRASS_GIS GRASS], and [https://en.wikipedia.org/wiki/SAGA_GIS 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 [https://plugins.qgis.org/plugins/ 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 private on one's local machine, shared to a private repository, or contributed to QGIS' public one.
+
[https://qgis.org/en/site/ QGIS] is a free and open source [https://en.wikipedia.org/wiki/QGIS 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, [https://en.wikipedia.org/wiki/GDAL GDAL], [https://en.wikipedia.org/wiki/GRASS_GIS GRASS], and [https://en.wikipedia.org/wiki/SAGA_GIS 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 [https://plugins.qgis.org/plugins/ 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 plugins 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 Python plugin for QGIS 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 should allow us to develop more advanced Python plugins as needed.
 
   
  +
In this tutorial we will work through the process of creating a simple '''Tool button with dialog''' plugin called Display Info. While the functionality of the plugin itself is quite limited (it writes a layer's file path, extent, and coordinate reference system to a text file), following the steps in this guide will provide a starting point for development of more advanced Python plugins. Provided we don't run into any serious hurdles it should take about 1-2 hours to complete all the steps.
== Software & Installation ==
 
   
  +
Please note that this is not a guide to 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.
=== QGIS ===
 
QGIS can be installed as a standalone application or through the OSGeo4W Network Installer. Written using QGIS Desktop 3.16.11(the current long term release) installed through OSGeo4W, it's likely that this tutorial will remain relevant to future versions of QGIS but some, or all, of the steps may become outdated in the coming years.
 
   
For QGIS installation please refer to the following page: [https://qgis.org/en/site/forusers/download.html Download QGIS for your platform].
 
   
  +
'''An overview of the steps we will be taking:'''
'''NOTE:''' QT designer is utilized as well, but it comes prepackaged with QGIS already.
 
   
  +
# 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
   
=== QT Designer ===
 
   
  +
== Tutorial ==
QGIS was developed using the QT toolkit to create its graphical user interfaces. QT Designer is an application, packaged within the QGIS installation, that is used to design these interfaces. Depending on how you installed QGIS you can find QT Designer in the following locations:
 
   
  +
=== Install Required Software ===
* Standalone installation:
 
  +
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:
C:\Program Files\QGIS 3.16.11\apps\qt5\bin\designer.exe
 
   
* OSGeo4W installation:
 
C:\OSGeo4W\apps\Qt5\bin\designer.exe
 
   
  +
==== QGIS ====
  +
QGIS can be installed as a standalone application or, on Windows machines, through the OSGeo4W Network Installer. This OSGeo4W installer is the recommended installation for regular Windows 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 : [https://qgis.org/en/site/forusers/download.html Download QGIS for your platform].
=== Text Editor ===
 
Any kind of coding requires a text editor or IDE. If you have a preference then use it, but in this tutorial we will be doing things in VS Code. See here for [https://code.visualstudio.com/download installation.]
 
   
  +
==== 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 : <code>C:\Program Files\QGIS 3.16.11\apps\qt5\bin\designer.exe</code>
=== Plugin Builder & Plugin Reloader ===
 
  +
  +
; OSGeo4W installation : <code>C:\OSGeo4W\apps\Qt5\bin\designer.exe</code>
  +
  +
  +
==== 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 : [https://code.visualstudio.com/docs/ Visual Studio Code - Getting Started]
  +
  +
  +
==== Plugin Builder and Plugin Reloader ====
 
This tutorial requires two QGIS plugins:
 
This tutorial requires two QGIS plugins:
   
* '''Plugin Builder''': Generates our plugin files and boilerplate code
+
* '''Plugin Builder''': Generates our plugin template
* '''Plugin Reloader''': Removes the need to restart QGIS every time the plugin's code is changed
+
* '''Plugin Reloader''': Removes the need to restart QGIS every time a plugin's source code is changed
   
   
  +
'''Installation:'''
To install these plugins:
 
 
# Open QGIS and navigate to '''Plugins > Manage and Install Plugins...'''
 
# Open QGIS and navigate to '''Plugins > Manage and Install Plugins...'''
 
# Select the '''All''' tab to search the plugin repository
 
# Select the '''All''' tab to search the plugin repository
# Search for and install '''Plugin Builder 3''' and '''Plugin Reloaded'''
+
# Search for and install '''Plugin Builder 3''' and '''Plugin Reloader'''
   
   
 
Once installed they should both be immediately accessible through the '''Plugins''' menu.
 
Once installed they should both be immediately accessible through the '''Plugins''' menu.
   
  +
=== Find Our QGIS User Profile Directory ===
== Methods ==
 
=== Python Bindings ===
 
The plugin 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 a 'Windows Batch File'''' (.bat)''' will need to be created with the following text (this can be done with notepad and the ''"save as"'' function):
 
   
  +
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.
@echo off
 
  +
call "C:\OSGeo4W64\bin\o4w_env.bat"
 
  +
QGIS installations will create a default user profile directory and store that user's plugins here: <code>C:\Users\{user name}\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins\</code>
call "C:\OSGeo4W64\bin\qt5_env.bat"
 
  +
call "C:\OSGeo4W64\bin\py3_env.bat"
 
  +
Each plugin will have it's own directory within <code>\plugins\</code>, 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 <code>\python\plugins\</code> subdirectory. Directories for any Python plugins we have installed should be visible here. Keep this window open for later access.
  +
  +
[[File:Jf_profile_folder.png|Additional components.]]
  +
  +
  +
=== Use Plugin Builder to Generate the Plugin Template ===
  +
  +
As mentioned, we will use Plugin Builder to generate a template for our plugin. Official documentation for the QGIS plugin can be found here: [https://g-sherman.github.io/Qgis-Plugin-Builder/ QGIS Plugin Builder].
  +
  +
1. Open QGIS and in the menu bar and 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.
  +
  +
[[File:Jf_plugin_builder_1.png|Plugin name and required information.]]
  +
  +
  +
3. Provide a brief description of the plugin and its purpose.
  +
  +
[[File:Jf_plugin_builder_2.png|About the plugin.]]
  +
  +
  +
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 <code>Tool button with dialog</code> is selected as the '''Template''' . For the '''Text for the menu item''' field write <code>Output layer info to a text file</code>. For '''Menu''' choose <code>Plugins</code>.
  +
  +
[[File:Jf_plugin_builder_3.png|Template specific parameters.]]
  +
  +
  +
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.
  +
  +
[[File:Jf_plugin_builder_4.png|Additional components.]]
  +
  +
  +
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.
  +
  +
[[File:Jf_plugin_builder_5.png|Publication information.]]
  +
  +
  +
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 [[#2) Find Our QGIS User Profile Directory | earlier]]. Click '''Generate''' when ready.
  +
  +
[[File:Jf_plugin_builder_6.png|Generate the plugin template.]]
  +
  +
  +
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.]]
  +
  +
  +
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 (<code>.bat</code>) in our code editor: (note: if you installed QGIS to a different path, replace <code>C:\OSGeo4W64\bin\</code> with the location of your install):
  +
  +
<syntaxhighlight lang="bash">
  +
@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
+
@echo on
pyrcc5 -o resources.py resources.qrc
+
pyrcc5 -o resources.py resources.qrc
  +
</syntaxhighlight>
   
Save this file to your project folder and name it '''compile.bat''', but if you installed QGIS to a different path, replace '''C:\OSGeo4W64\bin\''' with the location of your install.
+
Note: if you installed QGIS to a different path, replace <code>C:\OSGeo4W64\bin\</code> with the location of your install.
   
  +
Save it to the directory where our new plugin was just created and name it <code>compile.bat</code>. Run the file by double-clicking it - a new file called <code>resources.qrc</code> should appear. <code>compile.bat</code> can then be deleted.
=== Building the Basics ===
 
Now that all the installs have been completed, the basic plugin framework can be created. Open 'Plugin Builder' from the toolbar,''' Plugins > Plugin Builder > Plugin Builder''' and go through the dialog forms, as seen in '''Figures 2-3'''. Be sure to fill the form with details pertaining to your plugin and don't forget to save it in a good location. The Class name will be the name of the Python Class/file containing the plugin instructions. At the end of the dialogs you may receive a '''pyrcc5 error''', but you can just ignore this.
 
   
  +
10. After successfully generating the plugin template we should end up with a number of files and folders within our <code>\plugins\display_info\</code> 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 <code>display_info.py</code> and <code>display_info_dialog_base.ui</code> - we will edit both in the subsequent steps.
[[File:MW2_1.png|550px]]
 
'''Figure 2:''' Plugin builder input forms.
 
   
  +
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.
[[File:MW3.png|550px]]
 
'''Figure 3:''' Plugin builder final dialog prompt.
 
   
  +
[[File:Jf_plugin_menu.png|Display Info now appears in the Plugins menu.]]
==== Relocating the .bat ====
 
The compile.bat file created [[#Python Bindings|earlier]] can now be relocated to the plugin folder from a minute ago, as shown below in '''Figure 4'''. After the compile.bat file has been copied to the plugin folder, double click it and allow it to run. Next, copy the plugin directory to the QGIS plugin folder for your user profile. To do this, first locate the user profile folder by: opening QGIS and then select '''Settings > User Profiles > ''default (may vary for you)'' > Open Active Profile Folder'''. A new window will then open and this will be the profile folder. This can be seen below in '''Figures 5 & 6'''.
 
   
[[File:MW7.png|750px]]
 
'''Figure 4:''' compile.bat file copied to plugin folder.
 
   
  +
When we open it up we see that it has a blank interface. In the next section we will create the graphical user interface.
[[File:MW5.png|1100px]]
 
'''Figure 5:''' Opening the Active User Profile
 
   
  +
[[File:Jf_ui_blank.png|The blank user interface of Display Info]]
   
  +
=== Create the User Interface in QT Designer ===
[[File:MW4.png|750px]]
 
'''Figure 6:''' Active user profile folder.
 
   
  +
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.
In the new user profile window, open the '''python > plugins''' subfolder shown in '''Figure 6''' and copy your entire plugin directory there.
 
   
  +
Note: If this section is a struggle feel free to copy and paste the code for the user interface from the <code>display_info_dialog_base.ui</code> file [[#display_info.py|found below]].
=== Installing the New Plugin ===
 
Finally, we can have a first look at the plugin. Close QGIS and relaunch it. Then go to '''Plugins > Manage and Install plugins''' and enable the plugin ('DisplayInfo' in this case). There will now be a new icon on the plugin toolbar and a new menu entry under the '''Plugins''', both can be seen in '''Figure 7'''.
 
   
[[File:MW8.png|1100px]]
 
'''Figure 7:''' Plugin icon in toolbar and in menu.
 
   
  +
1. Open up QT Designer and select '''File > Open...''' and navigate to the '''display_info''' plugin folder that we just generated
Now if we click on the plugin ('''Figure 8'''), you will see a UI pops up ,but nothing is on it and it has no functionality. This is because we now need to edit the UI and add some logic to the plugin.
 
   
  +
2. Open the <code>display_info_dialog_base.ui</code> 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.
[[File:MW9.png]]
 
'''Figure 8:''' Default boilerplate UI.
 
   
  +
4. In the '''Widget Box''' panel on the left, search for <code>Combo Box</code> and drag a '''Combo Box''' into the UI. Place it anywhere for now.
=== Create the Plugin UI ===
 
Next we need to design a UI, first open '''QT Designer''' (or whatever form of QT creator you have from QGIS) which comes prepackaged with QGIS V 3.4.3, by typing in the search bar 'QT' and QT designer should show up. As this is a tutorial on how to create a plugin I'm not an expert on QT designer, I'd recommend going [https://gis-ops.com/qgis-3-qt-designer-explained/ here] for some in depth instructions on how to use it specifically for QGIS (this is the site I used to learn). After opening QT designer, go to '''File > Open File'''. Browse to the plugin subfolder of the user profile folder that you were shown how to locate [[#Relocating the .bat|here]]. Next, click on the ''Whatever you named your plugin''.ui file, as shown in '''Figure 9'''.
 
   
  +
5. Search again for <code>Line Edit</code> and <code>Tool Button</code> and add both of these QT widgets to the GUI
[[File:MW10.png|1200px]]
 
'''Figure 9:''' .ui file for designing ui.
 
   
  +
6. We will also need two text labels so search for <code>Label</code> 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:
After opening the plugins .ui file and designing an appropriate UI for plugin (It's structure and look will vary, depending on what you want the plugin to do. Think: Do you need 1 input or 2, should the user be able to specify output?), save the file as. The completed UI for the ''DisplayInfo'' tool can be seen below in '''Figure 10'''.
 
   
[[File:MW13.png|750px]]
+
[[File:Jf_ui_complete.png|The completed interface of Display Info]]
'''Figure 10:''' Completed UI for the ''DisplayInfo'' plugin.
 
   
=== Adding the Code ===
 
The full code to replicate the ''DisplayInfo'' example plugin is viewable below, and only has to be copied into the '''.py''' file for the applicable plugin folder. For help with the Python required to make your own tool or just to get some new ideas, I highly suggest this [https://gis-ops.com/qgis-3-plugin-development-reference-guide/ site]. Once, you've copied the ''DisplayInfo'' code below into the '''python > plugin''' your good to go. Use the '''Plugin Reloader''' to reload the ''DisplayInfo'' plugin, and run it.
 
   
  +
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.
==== Full Code ====
 
  +
# -*- coding: utf-8 -*-
 
  +
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 <code>Enter a path and filename</code>. Hit <code>Enter</code> 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.
DisplayInfo
 
  +
A QGIS plugin
 
  +
9. Save the <code>diplay_info_dialog_base.ui</code> file that we have just been editing and close QT Designer.
This plugin displays a layers info in a .txt.
 
  +
Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
 
  +
10. In QGIS, configure the Plugin Reloader plugin to reload '''display_info''' and then reload it.
-------------------
 
  +
begin : 2019-11-09
 
  +
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.
git sha : $Format:%H$
 
  +
copyright : (C) 2019 by Mike
 
  +
email : ***********
 
  +
=== Edit the Plugin Template to Include our Python Script ===
***************************************************************************/
 
  +
 
  +
When we generated our plugin using Plugin Builder we had to specify a module name: <code>display_info</code>. This became the plugin's Python module (<code>display_info.py</code>) (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 [[#display_info.py|found below]]. But first, here are the instructions:
/***************************************************************************
 
  +
* *
 
  +
* This program is free software; you can redistribute it and/or modify *
 
  +
1. Using our code editor open the <code>display_info.py</code> Python module. As we will see, the plugin module template already contains a number of function and class definitions
* it under the terms of the GNU General Public License as published by *
 
  +
* the Free Software Foundation; either version 2 of the License, or *
 
  +
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):
* (at your option) any later version. *
 
  +
* If our plugin makes use of any additional Python packages we need to import them at the start of <code>new_plugin.py</code> - where the other packages in the template are imported
* *
 
  +
* Define any functions within the class <code>NewPlugin</code> - the most appropriate place to insert these is above the definition of <code>run()</code> and below <code>unload()</code>.
***************************************************************************/
 
  +
* 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.
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
 
  +
from qgis.PyQt.QtGui import QIcon
 
  +
3. The full code to replicate our Display Info example plugin ([[#display_info.py|found below]]) only has to be copied and pasted into the <code>display_info.py</code> file - replacing all lines of that template - and saved in its original location (the <code>.../plugins/display_info/display_info.py</code>).
from qgis.PyQt.QtWidgets import QAction, QFileDialog
 
  +
from qgis.core import QgsProject, Qgis
 
  +
 
  +
=== Test the Functional Plugin ===
# Initialize Qt resources from file resources.py
 
  +
from .resources import *
 
  +
Now that our GUI and Python module have been made functional we can take it for a test drive.
# Import the code for the dialog
 
  +
from .display_info_dialog import DisplayInfoDialog
 
  +
1. In QGIS, reload Display Info using Plugin Reloader.
import os.path
 
  +
 
  +
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: [https://open.ottawa.ca/datasets/wards/ Wards - Open Ottawa]
 
  +
class DisplayInfo:
 
  +
2. Open Display Info from the '''Plugins''' menu.
"""QGIS Plugin Implementation."""
 
  +
 
  +
3. Select the layer from the dropdown menu, specify an output path and filename, and then click '''OK'''.
def __init__(self, iface):
 
  +
"""Constructor.
 
  +
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.
 
  +
:param iface: An interface instance that will be passed to this class
 
  +
[[File:Jf_output_test.png|Display Info's text file output for a layer of the City of Ottawa's wards.]]
which provides the hook by which you can manipulate the QGIS
 
  +
application at run time.
 
  +
:type iface: QgsInterface
 
  +
== Conclusion ==
"""
 
  +
# Save reference to the QGIS interface
 
  +
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. Please check the resrouces section below for some essential links. A wealth of other resources can also be found online, including guides on how to make the other type of QGIS plugins, such as a processing plugin.
self.iface = iface
 
  +
# initialize plugin directory
 
  +
self.plugin_dir = os.path.dirname(__file__)
 
  +
== Resources ==
# initialize locale
 
  +
locale = QSettings().value('locale/userLocale')[0:2]
 
  +
* [https://g-sherman.github.io/Qgis-Plugin-Builder/ Plugin Builder Documentation]
locale_path = os.path.join(
 
  +
* [https://docs.qgis.org/3.16/en/docs/pyqgis_developer_cookbook/plugins/index.html Developing Python Plugins - QGIS Documentation]
self.plugin_dir,
 
  +
* [https://doc.qt.io/qt-5/qtdesigner-manual.html QT Designer Manual]
'i18n',
 
  +
* [https://gis-ops.com/qgis-3-plugin-tutorial-plugin-development-reference-guide/ Plugin Development Reference Guide]
'DisplayInfo_{}.qm'.format(locale))
 
  +
* [https://gis-ops.com/qgis-3-plugin-tutorial-qt-designer-explained/ QT Designer Explained]
 
  +
* [https://gis-ops.com/qgis-3-plugin-tutorial-set-up-a-plugin-repository-explained/ Set Up a Plugin Repository Explained]
if os.path.exists(locale_path):
 
  +
self.translator = QTranslator()
 
  +
self.translator.load(locale_path)
 
  +
== Code ==
QCoreApplication.installTranslator(self.translator)
 
  +
 
  +
=== display_info.py ===
# Declare instance attributes
 
  +
self.actions = []
 
  +
self.menu = self.tr(u'&Display Info')
 
  +
<syntaxhighlight lang="python3">
 
  +
# -*- coding: utf-8 -*-
# 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
 
  +
DisplayInfo
 
  +
A QGIS plugin
# noinspection PyMethodMayBeStatic
 
  +
This plugin displays a layers info in a .txt.
def tr(self, message):
 
  +
Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
"""Get the translation for a string using Qt translation API.
 
  +
-------------------
 
We implement this ourselves since we do not inherit QObject.
+
begin : 2019-11-09
  +
git sha : $Format:%H$
 
  +
copyright : (C) 2019 by Mike (revised by John Foster in 2021)
:param message: String for translation.
 
:type message: str, QString
+
email : ***********
  +
***************************************************************************/
 
  +
:returns: Translated version of message.
 
  +
/***************************************************************************
:rtype: QString
 
  +
* *
"""
 
  +
* This program is free software; you can redistribute it and/or modify *
# noinspection PyTypeChecker,PyArgumentList,PyCallByClass
 
  +
* it under the terms of the GNU General Public License as published by *
return QCoreApplication.translate('DisplayInfo', message)
 
  +
* the Free Software Foundation; either version 2 of the License, or *
 
  +
* (at your option) any later version. *
 
  +
* *
def add_action(
 
  +
***************************************************************************/
self,
 
  +
"""
  +
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,
 
icon_path,
text,
+
text=self.tr(u'Output layer info to a text file'),
callback,
+
callback=self.run,
enabled_flag=True,
+
parent=self.iface.mainWindow())
  +
add_to_menu=True,
 
add_to_toolbar=True,
+
# will be set False in run()
status_tip=None,
+
self.first_start = True
  +
whats_this=None,
 
  +
parent=None):
 
  +
def unload(self):
"""Add a toolbar icon to the toolbar.
 
  +
"""Removes the plugin menu item and icon from QGIS GUI."""
 
:param icon_path: Path to the icon for this action. Can be a resource
+
for action in self.actions:
  +
self.iface.removePluginMenu(
path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
 
:type icon_path: str
+
self.tr(u'&Display Info'),
  +
action)
 
  +
self.iface.removeToolBarIcon(action)
:param text: Text that should be shown in menu items for this action.
 
  +
:type text: str
 
  +
def select_output_file(self):
 
  +
filename, _filter = QFileDialog.getSaveFileName(
:param callback: Function to be called when the action is triggered.
 
  +
self.dlg, "Select output filename and destination","layer_info", '*.txt')
:type callback: function
 
  +
self.dlg.lineEdit.setText(filename)
 
  +
:param enabled_flag: A flag indicating if the action should be enabled
 
  +
def run(self):
by default. Defaults to True.
 
:type enabled_flag: bool
+
"""Run method that performs all the real work"""
+
  +
# Create the dialog with elements (after translation) and keep reference
:param add_to_menu: Flag indicating whether the action should also
 
be added to the menu. Defaults to True.
+
# Only create GUI ONCE in callback, so that it will only load when the plugin is started
:type add_to_menu: bool
+
if self.first_start == True:
  +
self.first_start = False
 
  +
self.dlg = DisplayInfoDialog()
:param add_to_toolbar: Flag indicating whether the action should also
 
  +
self.dlg.toolButton.clicked.connect(self.select_output_file)
be added to the toolbar. Defaults to True.
 
  +
:type add_to_toolbar: bool
 
  +
# Fetch the currently loaded layers
 
  +
layers = QgsProject.instance().layerTreeRoot().children()
:param status_tip: Optional text to show in a popup when mouse pointer
 
  +
hovers over the action.
 
:type status_tip: str
+
# Clear the contents of the comboBox from previous runs
  +
self.dlg.comboBox.clear()
 
  +
:param parent: Parent widget for the new action. Defaults None.
 
  +
# Populate the comboBox with names of all the loaded layers
:type parent: QWidget
 
  +
self.dlg.comboBox.addItems([layer.name() for layer in layers])
 
  +
:param whats_this: Optional text to show in the status bar when the
 
  +
# Show the dialog
mouse pointer hovers over the action.
 
+
self.dlg.show()
  +
:returns: The action that was created. Note that the action is also
 
added to self.actions list.
+
# Run the dialog event loop
:rtype: QAction
+
result = self.dlg.exec_()
  +
"""
 
  +
# See if OK was pressed
 
icon = QIcon(icon_path)
+
if result:
action = QAction(icon, text, parent)
+
filename = self.dlg.lineEdit.text()
action.triggered.connect(callback)
+
with open(filename, 'w') as output_file:
  +
action.setEnabled(enabled_flag)
 
  +
# Get the selected layer
 
if status_tip is not None:
+
selectedLayerIndex = self.dlg.comboBox.currentIndex()
action.setStatusTip(status_tip)
+
selectedLayer = layers[selectedLayerIndex].layer()
  +
layer_source = selectedLayer.dataProvider().dataSourceUri() + '\n' #QgsProject.instance().readPath("./") + '\n'
 
if whats_this is not None:
+
action.setWhatsThis(whats_this)
+
# Get the selected layer's extent
  +
layer_extent = selectedLayer.extent()
 
if add_to_toolbar:
+
xmin = str(layer_extent.xMinimum())
# Adds plugin icon to Plugins toolbar
+
xmax = str(layer_extent.xMaximum())
self.iface.addToolBarIcon(action)
+
ymin = str(layer_extent.yMinimum())
  +
ymax = str(layer_extent.yMaximum())
 
  +
if add_to_menu:
 
  +
# Get the selected layer's CRS ID and description
self.iface.addPluginToMenu(
 
self.menu,
+
layer_CRS_id = selectedLayer.crs().authid()
action)
+
layer_CRS_description = selectedLayer.crs().description()
  +
 
  +
# Output the info to the text file
self.actions.append(action)
 
  +
output_file.write("Filepath: " + layer_source)
 
return action
+
output_file.write("X-min: " + xmin +"m" + '\n')
  +
output_file.write("X-max: " + xmax +"m"+ '\n')
 
  +
output_file.write("Y-min: " + ymin +"m" +'\n')
def initGui(self):
 
"""Create the menu entries and toolbar icons inside the QGIS GUI."""
+
output_file.write("Y-max: " + ymax +"m"+ '\n')
  +
output_file.write("CRS: " + layer_CRS_id +" - " + layer_CRS_description)
 
icon_path = ':/plugins/display_info/icon.png'
+
output_file.close()
self.add_action(
 
icon_path,
 
text=self.tr(u'Displayy Layer Info as TXT'),
 
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 file ","", '*.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.pushButton.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:
 
selectedLayerIndex = self.dlg.comboBox.currentIndex()
 
selectedLayer = layers[selectedLayerIndex].layer()
 
test1 = selectedLayer.dataProvider().dataSourceUri() + '\n' #QgsProject.instance().readPath("./") + '\n'
 
test2 = selectedLayer.extent() #
 
(xmin, xmax, ymin, ymax) = (test2.xMinimum(), test2.xMaximum(), test2.yMinimum(), test2.yMaximum())
 
xminString = str(xmin)
 
xmaxString = str(xmax)
 
yminString = str(ymin)
 
ymaxString = str(ymax) #Turn into extent function?
 
test3 = selectedLayer.crs().authid()
 
test4 = selectedLayer.crs().description()
 
output_file.write("Filepath: " + test1)
 
output_file.write("X-min: " + xminString +"m" + '\n')
 
output_file.write("X-max: " + xmaxString +"m"+ '\n')
 
output_file.write("Y-min: " + yminString +"m" +'\n')
 
output_file.write("Y-max: " + ymaxString +"m"+ '\n')
 
output_file.write("CRS: " + test3 +" - " + test4)
 
os.startfile(filename)
 
self.iface.messageBar().pushMessage(
 
"Success", "Output file written at " + filename,
 
level=Qgis.Success, duration=3)
 
   
  +
# Open the text file
== Testing the New Plugin ==
 
  +
os.startfile(filename)
Below is a testcase for the plugin, a random layer is selected and the desired output location/name of the .txt file to be created is set, as shown in '''Figures 11-12'''.
 
   
  +
# Generate a message after running the plugin
[[File:MW11.png]]
 
  +
self.iface.messageBar().pushMessage(
'''Figure 11:''' ''DisplayInfo'' plugin with UI filled out.
 
  +
"Success", "Output file written at " + filename,
  +
level=Qgis.Success, duration=3)
  +
</syntaxhighlight>
   
[[File:MW12.png]]
 
'''Figure 12:''' Final output from ''DisplayInfo'' plugin after running.
 
   
  +
=== display_info_dialog_base.ui ===
== References ==
 
1: https://gis-ops.com/qgis-simple-plugin/
 
   
  +
<syntaxhighlight lang="XML">
2: https://gis-ops.com/qgis-3-qt-designer-explained/
 
  +
<?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>
   
  +
</syntaxhighlight>
3: https://gis-ops.com/qgis-3-plugin-development-reference-guide/
 

Latest revision as of 17:40, 28 January 2023

Disclaimer

This tutorial assumes a basic knowledge of GIS environments and Python syntax. Originally authored by a student 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 plugins 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 Display Info. While the functionality of the plugin itself is quite limited (it writes a layer's file path, extent, and coordinate reference system to a text file), following the steps in this guide will provide a starting point for development of more advanced Python plugins. Provided we don't run into any serious hurdles it should take about 1-2 hours to complete all the steps.

Please note that this is not a guide to 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:

  1. Install required software
  2. Find our QGIS user profile directory
  3. Use Plugin Builder to generate the plugin template
  4. Create the user interface in QT Designer
  5. Edit the plugin template to include our Python script
  6. 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:


QGIS

QGIS can be installed as a standalone application or, on Windows machines, through the OSGeo4W Network Installer. This OSGeo4W installer is the recommended installation for regular Windows 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 
Download QGIS for your platform.

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 
Visual Studio Code - Getting Started


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:

  1. Open QGIS and navigate to Plugins > Manage and Install Plugins...
  2. Select the All tab to search the plugin repository
  3. Search for and install Plugin Builder 3 and Plugin Reloader


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.

Additional components.


Use Plugin Builder to Generate the Plugin Template

As mentioned, we will use Plugin Builder to generate a template for our plugin. Official documentation for the QGIS plugin can be found here: QGIS Plugin Builder.

1. Open QGIS and in the menu bar and 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.

Plugin name and required information.


3. Provide a brief description of the plugin and its purpose.

About the plugin.


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.

Template specific parameters.


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.

Additional components.


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.

Publication information.


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 earlier. Click Generate when ready.

Generate the plugin template.


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.

The results of the plugin template generation.


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.

Display Info now appears in the Plugins menu.


When we open it up we see that it has a blank interface. In the next section we will create the graphical user interface.

The blank user interface of Display Info

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:

The completed interface of Display Info


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 of run() and below unload().
  • 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.

Display Info's text file output for a layer of the City of Ottawa's wards.


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. Please check the resrouces section below for some essential links. A wealth of other resources can also be found online, including guides on how to make the other type of QGIS plugins, such as a processing plugin.


Resources


Code

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)


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>