Creating a QGIS Plugin

From CUOSGwiki
Revision as of 17:40, 28 January 2023 by MichaelWray (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

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>