Difference between revisions of "Creating a QGIS Plugin"
MichaelWray (talk | contribs) |
MichaelWray (talk | contribs) m |
||
(108 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
== Disclaimer == |
== 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. |
||
− | This tutorial was created for Microsoft Windows platforms. It assumes a basic knowledge of GIS Environments and Python syntax. |
||
+ | |||
+ | '''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 == |
== 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 in a private repository, or contributed to the public one. Three different types of plugins can be created in QGIS: |
||
− | This tutorial contains instructions to aid you in the creation of your own QGIS plugins. |
||
+ | ; Tool button with dialog : A plugin accessed from the plugin menu or tool bar that operates within a dialog box |
||
− | == Data == |
||
+ | ; 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 |
||
− | 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 [https://www12.statcan.gc.ca/census-recensement/2011/geo/bound-limit/bound-limit-2016-eng.cfm here.] |
||
+ | 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. |
||
− | Be sure to keep all the downloaded files together,in the same project folder. Keep the folder pathway/filenames simple and clear. |
||
+ | 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. |
||
− | • Ex.C:\Users\Larry\QGIS\Projects |
||
− | == Software Installation == |
||
+ | '''An overview of the steps we will be taking:''' |
||
− | === 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 [https://qgis.org/en/site/forusers/download.html here.] |
||
+ | # Install required software |
||
− | === Installing VS Code === |
||
+ | # Find our QGIS user profile directory |
||
− | 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.] |
||
+ | # 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 |
||
− | === 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'''. |
||
+ | == Tutorial == |
||
− | '''Plugin Builder:''' A useful plugin that creates all the files and boilerplate code required to get going with a plugin. |
||
+ | === Install Required Software === |
||
− | '''Plugin Reloader:''' A useful plugin that aids in testing and changing plugin code, without having to restart QGIS every time. |
||
+ | 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: |
||
− | [[File:MW1.png]] |
||
− | '''Figure 1:''' QGIS plugins used. |
||
− | == |
+ | ==== 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. |
||
− | 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): |
||
+ | ;Please refer to the following page for download and installation instructions : [https://qgis.org/en/site/forusers/download.html Download QGIS for your platform]. |
||
− | @echo off |
||
+ | |||
− | call "C:\OSGeo4W64\bin\o4w_env.bat" |
||
+ | ==== QT Designer ==== |
||
− | call "C:\OSGeo4W64\bin\qt5_env.bat" |
||
+ | 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: |
||
− | call "C:\OSGeo4W64\bin\py3_env.bat" |
||
+ | |||
+ | ; Standalone installation : <code>C:\Program Files\QGIS 3.16.11\apps\qt5\bin\designer.exe</code> |
||
+ | |||
+ | ; 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: |
||
+ | |||
+ | * '''Plugin Builder''': Generates our plugin template |
||
+ | * '''Plugin Reloader''': Removes the need to restart QGIS every time a plugin's source code is changed |
||
+ | |||
+ | |||
+ | '''Installation:''' |
||
+ | # Open QGIS and navigate to '''Plugins > Manage and Install Plugins...''' |
||
+ | # Select the '''All''' tab to search the plugin repository |
||
+ | # 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: <code>C:\Users\{user name}\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins\</code> |
||
+ | |||
+ | 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 |
|
− | + | pyrcc5 -o resources.py resources.qrc |
|
+ | </syntaxhighlight> |
||
+ | |||
+ | 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. |
||
+ | |||
+ | 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. |
||
+ | |||
+ | 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:Jf_plugin_menu.png|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. |
||
+ | |||
+ | [[File:Jf_ui_blank.png|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 <code>display_info_dialog_base.ui</code> file [[#display_info.py|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 <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. |
||
+ | |||
+ | 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. |
||
+ | |||
+ | 5. Search again for <code>Line Edit</code> and <code>Tool Button</code> and add both of these QT widgets to the GUI |
||
+ | |||
+ | 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: |
||
+ | |||
+ | [[File:Jf_ui_complete.png|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 <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. |
||
+ | |||
+ | 9. Save the <code>diplay_info_dialog_base.ui</code> 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: <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: |
||
+ | |||
+ | |||
+ | 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 |
||
+ | |||
+ | 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 <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. |
||
+ | |||
+ | 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>). |
||
+ | |||
+ | |||
+ | === 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: [https://open.ottawa.ca/datasets/wards/ 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. |
||
+ | |||
+ | [[File:Jf_output_test.png|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 == |
||
+ | |||
+ | * [https://g-sherman.github.io/Qgis-Plugin-Builder/ Plugin Builder Documentation] |
||
+ | * [https://docs.qgis.org/3.16/en/docs/pyqgis_developer_cookbook/plugins/index.html Developing Python Plugins - QGIS Documentation] |
||
+ | * [https://doc.qt.io/qt-5/qtdesigner-manual.html QT Designer Manual] |
||
+ | * [https://gis-ops.com/qgis-3-plugin-tutorial-plugin-development-reference-guide/ Plugin Development Reference Guide] |
||
+ | * [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] |
||
+ | |||
+ | |||
+ | == Code == |
||
+ | |||
+ | === display_info.py === |
||
+ | |||
+ | |||
+ | <syntaxhighlight lang="python3"> |
||
+ | # -*- 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 |
||
− | 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. |
||
+ | 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 |
||
− | == Building the Basics == |
||
+ | hovers over the action. |
||
− | 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'''. |
||
+ | :type status_tip: str |
||
+ | :param parent: Parent widget for the new action. Defaults None. |
||
− | [[File:MW2_1.png|550px]] |
||
+ | :type parent: QWidget |
||
− | '''Figure 2:''' Plugin builder input forms. |
||
+ | :param whats_this: Optional text to show in the status bar when the |
||
− | 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. |
||
+ | mouse pointer hovers over the action. |
||
+ | :returns: The action that was created. Note that the action is also |
||
− | [[File:MW3.png|550px]] |
||
+ | added to self.actions list. |
||
− | '''Figure 3:''' Plugin builder final dialog prompt. |
||
+ | :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 |
||
− | At the end of the dialogs you may receive a pyrcc5 error, but you can just ignore this. |
||
+ | self.dlg.comboBox.clear() |
||
+ | # Populate the comboBox with names of all the loaded layers |
||
− | === Relocating the .bat === |
||
+ | self.dlg.comboBox.addItems([layer.name() for layer in layers]) |
||
− | 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 compiled 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 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'''. |
||
+ | |||
+ | # Show the dialog |
||
+ | self.dlg.show() |
||
+ | # Run the dialog event loop |
||
− | [[File:MW7.png|750px]] |
||
+ | result = self.dlg.exec_() |
||
− | '''Figure 4:''' compile.bat file copied to plugin folder. |
||
+ | # See if OK was pressed |
||
− | [[File:MW5.png|1100px]] |
||
+ | if result: |
||
− | '''Figure 5:''' |
||
+ | 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 |
||
− | [[File:MW4.png|750px]] |
||
+ | layer_CRS_id = selectedLayer.crs().authid() |
||
− | '''Figure 6:''' Active user profile folder. |
||
+ | layer_CRS_description = selectedLayer.crs().description() |
||
+ | # Output the info to the text file |
||
− | == Installing the New Plugin == |
||
+ | 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 |
||
− | == Create the Plugin UI == |
||
+ | self.iface.messageBar().pushMessage( |
||
+ | "Success", "Output file written at " + filename, |
||
+ | level=Qgis.Success, duration=3) |
||
+ | </syntaxhighlight> |
||
− | == Adding the Code == |
||
+ | === display_info_dialog_base.ui === |
||
+ | <syntaxhighlight lang="XML"> |
||
− | == Testing the New Plugin == |
||
+ | <?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> |
||
− | == References == |
Latest revision as of 16:40, 28 January 2023
Contents
- 1 Disclaimer
- 2 Introduction
- 3 Tutorial
- 4 Conclusion
- 5 Resources
- 6 Code
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:
- 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
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:
- Open QGIS and navigate to Plugins > Manage and Install Plugins...
- Select the All tab to search the plugin repository
- 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.
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.
3. Provide a brief description of the plugin and its purpose.
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
.
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.
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.
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.
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.
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.
When we open it up we see that it has a blank interface. In the next section we will create the graphical user interface.
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:
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 ofrun()
and belowunload()
. - 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.
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
- Plugin Builder Documentation
- Developing Python Plugins - QGIS Documentation
- QT Designer Manual
- Plugin Development Reference Guide
- QT Designer Explained
- Set Up a Plugin Repository Explained
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>