Creating a QGIS Plugin

From CUOSGwiki
Revision as of 14:41, 29 September 2021 by JohnFoster (talk | contribs)
Jump to navigationJump to search

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.


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 QGIS' public one.

Three different types of plugin templates can be created in QGIS:

  • Tool button with dialog: A plugin accessed from the plugin menu or tool bar that operates within a dialog box
  • Tool button with dock widget: A plugin accessed from the plugin menu or tool bar that operates within a dock widget (i.e. a dockable pane within the QGIS desktop environment)
  • Processing provider: A plugin accessed from the QGIS Processing Toolbox

In this tutorial we will work through the process of creating a simple Tool button with dialog plugin called DisplayInfo. While the functionality of the plugin itself is quite limited (it prints a layer's file path, extent, and coordinate reference system), following the steps in this guide will provide a starting point for development of more advanced Python plugins.

Please note that this tutorial does not intend to guide you through the process of writting Python code for your own plugin, nor does it include complete instructions on the use of QT Designer. What it does attempt to accomplish is to demonstrate the various steps necessary to generate a plugin template, create a simple user interface, and point out where to insert the necessary Python code.

Here is 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


Step 1) Install Required Software

If you are reading this tutorial you are likely already a QGIS user and have some or all of the required software. To ensure we are on the same page, here are the software requirements for this tutorial.


QGIS

QGIS can be installed as a standalone application or through the OSGeo4W Network Installer. This OSGeo4W installer is the recommended installation for regular users and it contains the current latest release as well as the current long term release (LTR). Due to its stability and the availability of documentation the current LTR version of QGIS (QGIS 3.16.11) has been used in this tutorial. It is likely that these instructions will work for other QGIS installations but some steps may need to be modified.


Please refer to the following page for download and installation instructions:


QT Designer

QGIS was developed using the QT toolkit to create its graphical user interfaces. QT Designer is an application that is used to design these interfaces and comes packaged with QGIS installers. Depending on how you installed QGIS you can find QT Designer in the following locations:


  • Standalone installation:
   C:\Program Files\QGIS 3.16.11\apps\qt5\bin\designer.exe
  • OSGeo4W installation:
   C:\OSGeo4W\apps\Qt5\bin\designer.exe


Text Editor

A text editor is required to make changes to the plugin's Python source code. Notepad, IDLE, or a full fledged editor such as Microsoft Visual Studio Code will all suffice.

Download and installation instructions for Microsoft Visual Studio Code can be found here:


QGIS Plugins

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 Reloaded


Once installed they should both be immediately accessible through the Plugins menu.


Step 2) 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.


Step 4) Use Plugin Builder to Generate the Plugin Template

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

1. Open QGIS and in the menu bar navigate to Plugins > Plugin Builder > Plugin Builder. The Plugin Builder dialog box should open. It is a "wizard" featuring a number of different screens.

2. The various fields that need to be completed are quite self-explanatory but full details can be found in the official documentation. For this tutorial, make sure that Class name, Plugin name, and Module name are all written as shown in figure 1 - the other fields are less important.

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 what Plugin Builder creates.

Additional components.


6. Here we can submit the paths that are required for publication. Since we won't be publishing our plugin we 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 in earlier. Click Generate when ready but take note of the next step.

Generate the plugin template.


8. After clicking Generate Plugin Builder will create the required files and attempt to compile them. A warning might pop up after clicking Generate indicating that pyrcc5 was not found in your path. If that happens just click OK and be sure to perform the next step. If the generation went smoothly and no there was no warning then we can skip the next step.

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 a warning popped up saying that the pyrcc5 compiler was not found in your path then we will need to perform this extra step. To compile the new 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 a Windows Batch File (.bat) will need to be created with the following text (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

This file can be written in Microsoft Visual Studio code or whichever text editory you prefer. 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 be created.

10. After successfully generating the plugin files 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 loaded into 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 user interface. In the next section we will use QT Designer to create the user interface.

The blank user interface of Display Info


Step 5) 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.

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 UI.

4. In the Widget Box pane 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 UI

6. We will also need two text labels so search for Label and add two Labels to the UI

7. The five QT widgets we just added to the UI 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 pane, 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 pane 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.


Step 6) 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 you want to apply this tutorial to the creation of your own plugin (not Display Info):

  • If your plugin makes use of any additional Python packages be sure to import them at the start of your_plugin.py - where the other packages in the template are imported
  • Define any functions within the class YourPlugin - 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 UI through the calling of the QT Widget class names

3. The full code to replicate our Display Info example plugin is viewable at the bottom of this tutorial, and only has to be copied and pasted into the display_info.py file - replacing all lines of the template. Alternatively, the Display Info Python module can be downloaded from here: display_info.py. Simply overwrite the existing display_info.py file with the new one.

Test the Functional Plugin

Now that our GUI and Python module have been made functional we can test Display Info.

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. 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. While we did not share Display Info to the to public QGIS repository The opportunties afforded by QGIS' open-source nature make it possible for users to contribute new plug


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)