Understanding a QGIS Plugin

Right, this might be a bit long but hopefully it will help explain how to put together a QGIS plugin.

First I would install the “Plugin Builder” plugin and create a base using that. The path of the plugin you are creating should be something similar to .qgis\python\plugins\ and should contain the following files:

  • <pluginname>.py
  • ui_<pluginname>.py
  • resources.py
  • __init__.py

The __init__.py file will include the set-up details similar to the following (for the purposes of this post, the plugin is called Raster Killer – it just resizes rasters at the moment using PIL):

"""This script initializes the plugin, making it known to QGIS.
"""
def name():
    return "Raster processes for RS"
def description():
    return "Adds some image processing functionality"
def version():
    return "Version 0.1"
def icon():
    return "icon.png"
def qgisMinimumVersion():
    return "1.5"
def classFactory(iface):
    # load RasterKiller class from file RasterKiller
    from rasterkiller import RasterKiller
    return RasterKiller(iface)

The main set-up will then be coded into the <pluginname>.py file and we will set up a further Python script to undertake the main part of the program. This will be called do<pluginname>.py and will be saved in the same location as all the other files. If using QT Designer to produce the GUI, then I recommend storing the ui_<pluginname>.ui file here as well. The first thing to do is edit the .ui file in QT Designer, save the file and then use makepyqt.pyw to create the resources.py  and ui_<pluginname>.py files. This means that the complete set of files that are in the plugin folder will be:

  • <pluginname>.py
  • do<pluginname>.py
  • ui_<pluginname>.py
  • ui_<pluginname>.ui
  • resources.py
  • __init__.py
  • icon.png
  • Makefile
Next, the core part of the plugin needs to be written in <pluginname>.py. This is generally the same for most plugins and takes the following form:
# Import the PyQt and QGIS libraries
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *

# Initialize Qt resources from file resources.py
import qrc_resources
import dorasterkiller

So far we have initialised the imports. There are two from PyQt – the core modules and those for creating and displaying the GUI. Then the QGIS Python modules need to be imported so that we can make use of the geographical and file information available. The final two imports bring in the resources file (this time called qrc_resources.py) and the do<pluginname>.py file. This has the GUI display class coded inside it, as we’ll see later.

class RasterKiller:
    def __init__(self, iface):
    # Save reference to the QGIS interface
    self.iface = iface

    def initGui(self):
        # Create action that will start plugin configuration
        self.action = QAction(QIcon(":/plugins/rasterkiller/icon.png"), \
            "RasterKiller", self.iface.mainWindow())
        # connect the action to the run method
        QObject.connect(self.action, SIGNAL("triggered()"), self.run)
        # Add toolbar button and menu item
        self.iface.addToolBarIcon(self.action)
        self.iface.addPluginToMenu("&RasterKiller", self.action)

        def unload(self):
        # Remove the plugin menu item and icon
        self.iface.removePluginMenu("&RasterKiller",self.action)
        self.iface.removeToolBarIcon(self.action)</pre>

Now we have created the plugin Class. It needs to be aware of QGIS so saves a reference to the QGIS interface as iface. Then the plugin configuration occurs, adding buttons and menu items to the QGIS interface. A method to remove these if the plugin is removed is also presented at this point in the code.

        # run method that performs all the real work
        def run(self):
            # create and show the dialog
            dlg = dorasterkiller.RkDialog(self.iface)
            dlg.exec_()

The final part of the <pluginname>.py file is to create a method that calls the GUI as coded in the do<pluginname>.py file. The first part calls the GUI passing it the reference to the QGIS interface and the exec_() command then runs the plugin GUI as a modal window (meaning only the plugin can be accessed, QGIS is locked whilst the plugin GUI is active).

So the next thing to do is start to code the main part of the plugin in do<pluginname>.py:

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *
from ui_rasterkiller import Ui_RasterKiller

So the same imports occur in the first 3 lines as we saw in the previous code snippet. Then we import the Ui_ class from the ui_ file.

class RkDialog(QDialog, Ui_RasterKiller):
    def __init__(self, iface):
        QDialog.__init__(self)
        self.iface = iface
        self.setupUi(self)
        QObject.connect(self.btnFolder, SIGNAL("clicked()"), self.findFolder)

The RkDialog Class draws the GUI as saved in the .ui QT Designer file inheriting from QDialog and the Ui_ class that was imported. The GUI is initialised, and the button on the GUI is connected to the findFolder method. From now on the code presented in this post is specific to the example being given. It is this code that would need to substantially change for new plugins.

    def findFolder(self):
    # display file dialog for output shapefile

        folderLoc = QFileDialog.getExistingDirectory(None, 'Which folder?', '.')
        self.lblFolder.setText(folderLoc)

    def accept(self): # Called when "OK" button pressed
        # Import supporting libraries
        try:
            import Image as I
            import glob, os
        except:
            QMessageBox.information(None, "Import Error", "Required: PIL")

The findFolder method opens a standard QFileDialog widget and allows the user to navigate to a given folder, returning the folder path as a QString. This can then be passed to a label on the GUI (lblFolder) so that the path can be displayed. Using this file structure and calling the GUI display in this way allows the components of that GUI to be called using the self.component.function() convention. The key method in this script defines what happens when the OK button is pressed. This might not be particularly elegant, but it does what is required of it. First it makes sure that all the required libraries are available (glob, os and PIL). If not, an error message is presented.

        try:
            newPath = str(self.lblFolder.text())
            resize = float(self.spnFactor.text())

            b = []

            os.chdir(newPath)

Next, data is obtained from the GUI. In this case this is the folder path and the resize factor. The working directory is then changed so that the images can be processed.

            for imfile in glob.glob("*.jpg"):
                file, ext = os.path.splitext(imfile)
                im = I.open(imfile)
                out = im.resize((int(im.size[0]*(resize/100)), \
                        int(im.size[1]*(resize/100))))
                out.save(file + "_small" + ".jpg")

            for wfile in glob.glob("*.jgw"):
                file, ext = os.path.splitext(wfile)
                worldf = open(wfile, 'r')
                for i, line in enumerate(worldf):
                    if i == 0 or i == 3:
                        line = str(float(line)*resize)+"\n"
                        b.append(line)
                    else:
                        b.append(line)
                worldf.close()
                wfout = open(file + "_small" + ".jgw", 'w')
                for i in b:
                    wfout.write(str(i))
                wfout.close()
                b = []

            QMessageBox.information(None, "Complete", "Complete: No issues")
            self.close()
        except:
            QMessageBox.information(None, "Complete", "Failed")

Each image file is processed in turn and saved as a new file. There is no error capture to see whether a folder has already been processed. Next, the individual worldfiles are processed. The sizes of the pixels are changed and a new worldfile saved. Once the processing has been completed, a message box is displayed, and when the OK button is pressed the plugin dialogs close.

I am sure that there are better ways to write these plugins, but as someone with no real knowledge of PyQt, GUI programming in Python or Python programming in QGIS I found working out how best to structure the code quite complex. This post hopefully lays out the basics. The next thing I need to do is better understand PyQT, the QGIS API and Python in general!

Advertisements
Tagged , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: