Construyendo un Complemento Python (QGIS3)

Los complementos son una gran manera de extender la funcionalidad de QGIS. Puede escribir complementos usando Python que van desde la adición de un simple botón a conjuntos de herramientas sofisticados. Este tutorial destacará el proceso involucrado en establecer tu ambiente de desarrollo, diseñar el interfaz de usuario para un complemento y escribir código para interactuar con QGIS. Por favor revise el tutorial Iniciándote con la Programación Python (QGIS3) para familiarizarse con l básico.

Nota

If you are building a new plugin, I strongly recommend building a Processing Plugin instead of the GUI plugin described in this tutorial. See Building a Processing Plugin (QGIS3) for details.

Vista general de la tarea

Desarrollaremos un complemento simple llamado Guardar Atributos que permitirá a los usuaios escoger una capa vectorial y escribir sus atributos como un archivo CSV.

Obtener las Herramientas

Creador Qt

Qt es un marco de desarrollo de software que es usado para desarrollar aplicaciones que corren en Windows, Mac, Linex así como varios sistemas operativos móviles. QGIS mismo está escrito usando el marco Qt. Para el desarrollo de complemento, usaremos una aplicación llamada Qt Creator para diseñar el interfaz para nuestro complemento.

Descargue e instale el instalador de Qt Creator de Qt Offline Installers.  Asegúrese que selecciona Qt Creator en la página de descarga. Note que tendrá que crear una cuenta libre de Qt para instala el paquete.

Nota

El instalador OSGeo4w para QGIS en Windows incluye una copia del programa Qt Designer que es una versión liviana de Qt Creator y es perfectamente apropiada para construir complementos. Puede saltarse la descarga de Qt Creator y usar en vez de ello C:\OSGeo4W64\bin\qgis-designer.

../../_images/setup11.png

Vínculos Python para Qt

Ya que estamos desarrollando el complemento en Python, necesitamos instalar los vínculos python para Qt. El método para instalar estos dependerá de la plataforma que está usando. Para construir complementos necesitamos la herramienta de línea de comando pyrcc5.

Windows

Los vínculos python relevantes están incluídos en el instalación QGIS en Windows. Pero para usarlos desde la carpeta de complemento, necesitamos indicar la ruta a la instalación QGIS.

Cree un archivo de lotes Windows (extensión .bat) con el siguiente contenido y guárdelo en su computadora como compile.bat. Copiaremos después este archivo a la carpeta de complemento. Si instaló QGIS en una ruta diferente, reemplace C:\OSGeo4W64\bin\ con su ruta.

@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
../../_images/setup2.png

Mac

Instale el administrador de paquete Homebrew. Instale el paquete PyQt corriendo el siguiente comando:

brew install pyqt
Linux

Dependiendo de su Distribución, encuentre e instale el paquete python-qt5. En Ubuntu y distribuciones basadas en Debian, puede correr el siguiente comando:

sudo apt-get install python-qt5

Nota

Puede encontrar que QGIS ya ha instalado este paquete.

Un Editor de Texto o un IDE Python.

Cualquier tipo de desarrollo de software requiere un buen editor de texto. Si ya tiene un editor de texto favorito o un IDE (Ambiente Integrado de Desarrollo, en inglés), puede usarlo para este tutorial. De otra forma, cada plataforma ofrece una amplia variedad de opciones gratuitas o pagadas para editores de texto. Elija aque que se ajuste a sus necesidades.

Este tutorial usa el editor Notepad++ en Windows.

Windows

Notepad++ es un buen editor gratuito para windows. Descargue e instale el editor Notepad++.

Nota

Si está usando Notepad++, asegúrese de ir a Configuración ‣ Preferencias ‣ Configuración de Tab y active Reemplazar por espacio. Python es muy sensible al espacio en blanco y esta configuración asegurará que las tabulaciones y espacios sean tratadas apropiadamente.

Complemento Plugin Builder

Hay un complemento útil QGIS llamado Plugin Builder que crea todos los archivos necesarios y el código repetitivo para un complemento. Encuentre e instale el complemento Plugin Builder. Vea Uso de complementos para más detalles de como instalar complementos.

Complemento Plugins Reloader

Este es otro complemento asistente que permite el desarrollo iterativo de complementos. Usando este complemento, puede cambiar el código del complemento y tenerlo reflejado en QGIS sin tener que reiniciar QGIS cada vez. Encuentre e instale el complemento Plugin Reloader. Vea Uso de complementos para más detalles sobre como instalar complementos.

Nota

Plugin Reloader es un complemento experimental. Asegúrese que ha marcado Mostrar también complementos experimentales en la configuración Administrador de Complementos si no puede encontrarlo.

Procedimiento

  1. Abra QGIS. Vaya a Complementos ‣ Plugin Builder ‣ Plugin Builder.

../../_images/118.png
  1. Verá el diálogo QGIS Plugin Builder con un formulario. Puede llenar el formulario con detalles relacionados a nuestro complemento. El Nombre de clase será el nombre de la Clase Python que contiene la lógica del complemento. Esto también será el nombre de la carpeta que contiene todos los archivos de complementos. Ingrese SaveAttributes como el nombre de la clase. El Nombre de complemento es el nombre bajo el que aparecerá el complemento en el Administrador de Complemento. Ingrese el nombre como Guardar Attributos . Agregue una descripción en el campo Descripción. El Nombre del módulo será el nombre del archivo python principal para el complemento. Ingréselo como save_attributes_processing. Deje los números de versión como están e ingrese su nombre y dirección electrónica en los campos apropiados. Clic en Siguiente.

../../_images/217.png
  1. Ingrese una breve descripción del complemento para el diálogo Acerca y clic en Siguiente.

../../_images/315.png
  1. Seleccione la Botón de herramienta con diálogo del selector Template. El valor Texto para el elemento de menú será como encontrarán los usuarios el complemento en el menú QGIS. Ingréselo como Guardar Atributos como CSV. El campo Menu decidirá donde es agregado el elemento complmento en QGIS. Debido a que el complemento es para datos vectoriales, seleccione Vectorial. Clic en Siguiente.

../../_images/46.png
  1. El Plugin Builder le pedirá el tipo de archivos a generar. Mantenga la selección predeterminada y clic en Siguiente.

../../_images/56.png
  1. Como no pretendemos publicar el complemento, puede dejar los valores Rastreador de errores, Repositorio y Página de inicio predeterminados. Marque la caja Señale el complemento como experimental abajo y clic en Siguiente.

../../_images/66.png
  1. Se le pedirá escoger un directorio para su complemento. Por ahora, guárdelo a un directorio que pueda encontrar fácilmente en su computadora y clic en Generar.

../../_images/76.png
  1. A continuación, presione el botón generar. Verá un diálogo de confirmación una vez que la plantilla de complemento sea creada.

../../_images/86.png

Nota

Puede que vea un aviso que dice que no se encontró pyrcc5 en su ruta. Puede ignorar este mensaje.

  1. Antes que podamos usar este complemento recién creado, necesitamos compilar el archivo resources.qrc creado con Plugin Builder. Este archivo es parte del Qt Resource System que referencia a todos los archivos binarios usados en el complemento. Para este complemento, sólo tendrá el icono del complemento. Compilando este archivo genera el código de la aplicación que puede ser usado en el complemento independiente de en que plataforma se corra el complemento. Siga las instrucciones específicas de la plataforma para este paso.

Windows

Ahora puede copiar el archivo compile.bat (creado durante la sección Python Bindings for Qt al inicio) a la carpeta del complemento. Una vez copiado, doble-clic en el archivo para ejecutarlo. Si la ejecución es exitosa, verá un nuevo archivo llamado resources.py en la carpeta.

../../_images/96.png

Nota

Si este paso falla, puede iniciar cmd.exe y explorar la carpeta del complemento usando el comando cd. Ejecute el archivo de procesamiento por lotes ejecutando compile.bat para ver el error.

Mac y Linux

Necesitará instalar primero pb_tool. Abra una Terminal e instálelo mediante pip.

sudo pip3 install pb_tool

Abra una Terminal y vaya al directorio del complemento y escriba pb_tool compile. Esto ejecutará el comando pyrcc5 que instalamos como parte de la sección Python Bindings for Qt.

pb_tool compile
  1. Los complemento en QGIS son almacenados en una carpeta especial. Debemos copiar nuestro directorio de complemento a esa carpeta antes que podamos usarlo. En QGIS, localice la carpeta de su perfil actual yendo a Configuración ‣ Perfiles de Usuario ‣ Abrir la Carpeta del Perfil Activo.

../../_images/106.png
  1. En la carpeta del perfil, copie la carpeta del complemento a la subcarpeta python ‣ complementos

../../_images/119.png
  1. Ahora estamos listos para darle una primera mirada al complemento nuevo que creamos. Cierre QGIS e inícielo de nuevo. Vaya a Complementos ‣ Administrar e Instalar complementos y active el complemento Guardar Atributos en la pestaña Instalado.

../../_images/126.png
  1. You will notice that there is a new icon in the plugin toolbar and a new menu entry under Vector ‣ Save Attributes ‣ Save Attributes as CSV`. Select it to launch the plugin dialog.
../../_images/136.png
  1. You will notice a new blank dialog named Save Attributes. Close this dialog.
../../_images/146.png
  1. We will now design our dialog box and add some user interface elements to it. Open the Qt Creator program and go to File ‣ Open File or Project.
../../_images/156.png
  1. Browse to the plugin directory and select the save_attributes_dialog_base.ui file. Click Open.
../../_images/16.gif

Nota

Windows hides the AppData folder so you may not see it in the file selector dialog. You can enter AppData in the File name prompt from its parent directory to open it.

  1. You will see the blank dialog from the plugin. You can drag-and-drop elements from the left-hand panel on the dialog. We will add a Combo Box type of Input Widgets. Drag it to the plugin dialog.
../../_images/176.png
  1. Resize the combo box and adjust its size. Now drag a Label type Display Widget on the dialog.
../../_images/185.png
  1. Click on the label text and enter Select a layer.
../../_images/195.png
  1. Save this file by going to File ‣ Save save_attributes_dialog_base.ui. Note the name of the combo box object is comboBox. To interact with this object using python code, we will have to refer to it by this name.
../../_images/204.png
  1. Let’s reload our plugin so we can see the changes in the dialog window. Go to Plugin ‣ Plugin Reloader ‣ Choose a plugin to be reloaded. Select SaveAttributes in the Configure Plugin reloader dialog.
../../_images/218.png
  1. Click the Reload plugin button to load the latest version of the plugin. Click the Save Attributes as CSV button to open the newly designed dialog box.
../../_images/224.png
  1. Let’s add some logic to the plugin that will populate the combo box with the layers loaded in QGIS. Go to the plugin directory and load the file save_attributes.py in a text editor. First, insert at the top of the file with the other imports:

    from qgis.core import QgsProject
    

    Then scroll down to the end and find the run(self) method. This method will be called when you click the toolbar button or select the plugin menu item. Add the following code at the beginning of the method. This code gets the layers loaded in QGIS and adds it to the comboBox object from the plugin dialog.

    # 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])
    
../../_images/23a.png ../../_images/23b.png
  1. Back in the main QGIS window, reload the plugin by clicking on the Reload plugin button. To test this new functionality, we must load some layers in QGIS. After you have loaded some layers, launch the plugin by going to Vector ‣ Save Attributes ‣ Save Attributes as CSV. You will see that our combo box is now populated with the layer names that are loaded in QGIS.
../../_images/243.png
  1. Let’s add the remaining user interface elements. Switch back to Qt Creator and load the save_attributes_dialog_base.ui file. Add a Label Display Widget and change the text to Select output file. Add a LineEdit type Input Widget that will show the output file path that the user has chosen. Next, add a Push Button type Button and change the button label to .... Note the object names of the widgets that we will have to use to interact with them. Save the file.
../../_images/253.png
  1. We will now add python code to open a file browser when the user clicks the ... push button and show the select path in the line edit widget. Open the save_attributes.py file in a text editor. Add QFileDialog to QtWidgets list of imports at the top of the file.
../../_images/263.png
  1. Add a new method called select_output_file with the following code. This code will open a file browser and populate the line edit widget with the path of the file that the user chose. Note, how getSaveFileName returns a tuple with the filename and the filter used.
def select_output_file(self):
  filename, _filter = QFileDialog.getSaveFileName(
    self.dlg, "Select   output file ","", '*.csv')
  self.dlg.lineEdit.setText(filename)
../../_images/273.png
  1. Now we need to add code so that when the ... button is clicked, select_output_file method is called. Scroll down to the run method and add the following line in the block where the dialog is initialized. This code will connect the select_output_file method to the clicked signal of the push button widget.
self.dlg.pushButton.clicked.connect(self.select_output_file)
../../_images/283.png
  1. Back in QGIS, reload the plugin and run it. If all went fine, you will be able to click the ... button and select an output text file from your disk.
../../_images/292.png
  1. When you click OK on the plugin dialog, nothing happens. That is because we have not added the logic to pull attribute information from the layer and write it to the text file. We now have all the pieces in place to do just that. Find the place in the run method where it says pass. Replace it with the code below. The explanation for this code can be found in Iniciándote con la Programación Python (QGIS3).

    filename = self.dlg.lineEdit.text()
    with open(filename, 'w') as output_file:
      selectedLayerIndex = self.dlg.comboBox.currentIndex()
      selectedLayer = layers[selectedLayerIndex].layer()
      fieldnames = [field.name() for field in selectedLayer.fields()]
      # write header
      line = ','.join(name for name in fieldnames) + '\n'
      output_file.write(line)
      # wirte feature attributes
      for f in selectedLayer.getFeatures():
        line = ','.join(str(f[name]) for name in fieldnames) + '\n'
        output_file.write(line)
    
    ../../_images/302.png
  2. We have one last thing to add. When the operation finishes successfully, we should indicate the same to the user. The preferred way to give notifications to the user in QGIS is via the self.iface.messageBar().pushMessage() method. Add Qgis to qgis.core list of imports at the top of the file and add the code below at the end of the run method.

self.iface.messageBar().pushMessage(
  "Success", "Output file written at " + filename,
  level=Qgis.Success, duration=3)
../../_images/31a.png ../../_images/31b.png
  1. Now our plugin is ready. Reload the plugin and try it out. You will find that the output text file you chose will have the attributes from the vector layer.
../../_images/323.png
  1. You can zip the plugin directory and share it with your users. They can unzip the contents to their plugin directory and try out your plugin. If this was a real plugin, you would upload it to the QGIS Plugin Repository so that all QGIS users will be able to find and download your plugin.

Nota

This plugin is for demonstration purpose only. Do not publish this plugin or upload it to the QGIS plugin repository.

Below is the full save_attributes.py file as a reference.

# -*- coding: utf-8 -*-
"""
/***************************************************************************
 SaveAttributes
                                 A QGIS plugin
 This plugin saves the attributes of the selected vector layer as a CSV file.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2019-03-28
        git sha              : $Format:%H$
        copyright            : (C) 2019 by Ujaval Gandhi
        email                : ujaval@spatialthoughts.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 PyQt5.QtCore import QSettings, QTranslator, qVersion, QCoreApplication
from PyQt5.QtGui import QIcon
from PyQt5.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 .save_attributes_dialog import SaveAttributesDialog
import os.path


class SaveAttributes:
    """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',
            'SaveAttributes_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&Save Attributes')

        # 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('SaveAttributes', 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.addPluginToVectorMenu(
                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/save_attributes/icon.png'
        self.add_action(
            icon_path,
            text=self.tr(u'Save Attributes as CSV'),
            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.removePluginVectorMenu(
                self.tr(u'&Save Attributes'),
                action)
            self.iface.removeToolBarIcon(action)

    def select_output_file(self):
      filename, _filter = QFileDialog.getSaveFileName(
        self.dlg, "Select   output file ","", '*.csv')
      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 = SaveAttributesDialog()
            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 and lineEdit from previous runs
        self.dlg.comboBox.clear()
        self.dlg.lineEdit.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()
            fieldnames = [field.name() for field in selectedLayer.fields()]
            # write header
            line = ','.join(name for name in fieldnames) + '\n'
            output_file.write(line)
            # wirte feature attributes
            for f in selectedLayer.getFeatures():
              line = ','.join(str(f[name]) for name in fieldnames) + '\n'
              output_file.write(line)
          self.iface.messageBar().pushMessage(
            "Success", "Output file written at " + filename,
            level=Qgis.Success, duration=3)
If you liked tutorials on this site and do check out spatialthoughts.com for more free resources.
comments powered by Disqus

This work is licensed under a Creative Commons Attribution 4.0 International License