Creating a QGIS Plugin
Contents
Disclaimer
This tutorial was created for Microsoft Windows platforms. It assumes a basic knowledge of GIS Environments and Python syntax.
Introduction
This tutorial contains instructions to aid you in the creation of your own QGIS plugins.
Data
As most of this tutorial is software based, the only data required is a shapefile layer for testing purposes at the end. One can find the shapefile used for testing here.
Be sure to keep all the downloaded files together,in the same project folder. Keep the folder pathway/filenames simple and clear.
• Ex.C:\Users\Larry\QGIS\Projects
Software Installation
Installing QGIS
The latest version of QGIS is good for this tutorial, it can either be the standalone QGIS version or OSGeo4W installer (both come equipped with some form of Qt creator). For QGIS installation refer to here.
Installing VS Code
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 installation.
Installing QGIS Plugins
Once QGIS is installed and open, go to the Plugins > Manage and Install Plugins menu and install: Plugin Reloader and Plugin Builder.
Plugin Builder: A useful plugin that creates all the files and boilerplate code required to get going with a plugin.
Plugin Reloader: A useful plugin that aids in testing and changing plugin code, without having to restart QGIS every time.
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.
Figure 2: Plugin builder input forms.
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.
Figure 3: Plugin builder final dialog prompt.
At the end of the dialogs you may receive a pyrcc5 error, but you can just ignore this.
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.
Figure 4: compile.bat file copied to plugin folder.
Figure 5: Opening the Active User Profile
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.
Now if we click on the plugin (Figure 8), you will see a GUI pops up ,but nothing is on it and it has no functionality. This isbecuase we now need to edit the GUI and add some logic to the plugin.
Create the Plugin UI
Adding the Code
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 : euserver28@gmail.com ***************************************************************************/ /*************************************************************************** * * * 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 shapefile is loaded in and selected (see fig).