Creating a QGIS Plugin

From CUOSGwiki
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.


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 will provide a starting point for development of more advanced Python plugins.


Here is an overview of the steps we will be taking:

  1. Install required software
  2. Use Plugin Builder to generate the plugin template
  3. Edit the user interface file using QT Designer
  4. Edit the plugin source code template to include our Python script
  5. Test the plugin

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. Microsoft Visual Studio Code is a free and well supported option.


More information and a link to download Microsoft VS Code can be found here:


Required QGIS Plugins

This tutorial requires two QGIS plugins:

  • Plugin Builder: Generates our plugin files and boilerplate code
  • 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.


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):

@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

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.

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.

MW2 1.png Figure 2: Plugin builder input forms.

MW3.png Figure 3: Plugin builder final dialog prompt.

Relocating the .bat

The compile.bat file created 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.

MW7.png Figure 4: compile.bat file copied to plugin folder.

MW5.png Figure 5: Opening the Active User Profile


MW4.png Figure 6: Active user profile folder.

In the new user profile window, open the python > plugins subfolder shown in Figure 6 and copy your entire plugin directory there.

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.

MW8.png Figure 7: Plugin icon in toolbar and in menu.

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.


MW9.png Figure 8: Default boilerplate UI.

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 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 here. Next, click on the Whatever you named your plugin.ui file, as shown in Figure 9.

MW10.png Figure 9: .ui file for designing ui.


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.

MW13.png 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 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.

Full Code

   # -*- 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 
           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'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)

Testing the New Plugin

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.

MW11.png Figure 11: DisplayInfo plugin with UI filled out.

MW12.png Figure 12: Final output from DisplayInfo plugin after running.

References

1: https://gis-ops.com/qgis-simple-plugin/

2: https://gis-ops.com/qgis-3-qt-designer-explained/

3: https://gis-ops.com/qgis-3-plugin-development-reference-guide/