PyQGIS · Material de apoyo para la clase de Programación en Ambientes de SIG (PASIG)

Ejercicio 10: Complemento para identificar sitios para el sorgo

En el ejercicio 2 convertiste un modelo gráfico en una herramienta de procesamiento que identifica las zonas aptas para el sorgo, y en el ejercicio 9 creaste tu primer complemento con Plugin Builder, aunque su ventana todavía no hacía nada. Este ejercicio une ambas ideas: es un complemento llamado "Sitios sorgo" que abre una ventana propia, le pide al usuario sus capas de entrada y el nivel que alcanza el agua en una inundación, ejecuta el mismo análisis del ejercicio 2 y carga el resultado en el proyecto de QGIS.

Igual que en el ejercicio 9, Plugin Builder generó la estructura y el creador personalizó el resto. La diferencia es que ahora el creador sí programó la lógica: los botones de la ventana están conectados con métodos propios. En los bloques de código de esta página las partes se distinguen con franjas de color:

  • Franja amarilla · Personalizado por el creador. Datos que el creador escribió en el formulario de Plugin Builder: nombres de la clase y del módulo, descripción, autoría, texto del menú, etc.
  • Franja azul · Código agregado o modificado después de generar la plantilla. Todo lo que el creador programó o diseñó a mano: importaciones, métodos propios, conexiones de botones y elementos de la ventana.

Nota: las franjas se identificaron comparando los archivos con la plantilla estándar de Plugin Builder (tipo Tool button with dialog), la misma del ejercicio 9. Todo lo que no tiene franja es código de la plantilla que no fue alterado.

Conceptos clave de POO, PyQGIS y PyQt usados en el ejercicio

  • Métodos propios en la clase del complemento: además de initGui, unload y run (que exige QGIS), la clase Sorgo define los métodos abrir_capa_suelo, abrir_capa_dem, guardar_capa_sorgo, cancelar y aceptar. Cada uno agrupa una tarea concreta, como en cualquier clase bien organizada.
  • Atributo compartido entre métodos: self.dlg guarda la ventana. Como es un atributo del objeto (no una variable local), todos los métodos pueden leer y modificar los elementos de la ventana.
  • Elementos de la ventana como atributos: cada elemento diseñado en Qt Designer recibe un objectName (por ejemplo edit_suelo) y, gracias a setupUi, se usa en Python como self.dlg.edit_suelo. El nombre sigue la convención tipo_contenido: edit_ para cuadros de texto, button_ para botones y label_ para etiquetas.
  • Señales y callbacks con métodos propios: self.dlg.button_suelo.clicked.connect(self.abrir_capa_suelo) hace que, al hacer clic en el botón, QGIS ejecute el método indicado. El método se pasa sin paréntesis, como referencia.
  • Diálogos estándar de archivo: QFileDialog abre las ventanas del sistema para elegir un archivo existente o un archivo nuevo donde guardar.
  • Ejecutar herramientas de QGIS desde el código: processing.run permite usar cualquier algoritmo de la caja de herramientas. A diferencia del ejercicio 2, aquí no hay una clase QgsProcessingAlgorithm, por lo que las llamadas son más sencillas (sin context ni feedback).
  • Recorrer los elementos hijos de un contenedor: findChildren(QLineEdit) devuelve todos los cuadros de texto de la ventana, y un ciclo for permite limpiarlos sin nombrarlos uno por uno.

Los archivos del complemento

La carpeta sitio_sorgo tiene la misma organización que la del ejercicio 9. Estos son los archivos que se analizan en esta página:

  • __init__.py: punto de entrada; QGIS lo busca primero.
  • metadata.txt: ficha del complemento (nombre, descripción, versión, autoría...).
  • sitio_sorgo.py: la clase principal. Aquí está casi todo el código nuevo.
  • sitio_sorgo_dialog.py: la clase de la ventana.
  • sitio_sorgo_dialog_base.ui: el diseño de la ventana (XML de Qt Designer), ahora con cuadros de texto, etiquetas y botones propios.
  • pb_tool.cfg: configuración para desplegar el complemento (se revisa al final).

icon.png, Makefile, pylintrc, scripts/, help/, i18n/, test/ y los archivos README los generó Plugin Builder y no se modificaron; se describen en el ejercicio 9.

Análisis y explicación de la estructura y lógica del código

Sección 1 · __init__.py: el punto de entrada

Cumple la misma función que en el ejercicio 9: QGIS busca la función classFactory, la llama una sola vez y espera que devuelva el objeto del complemento.

__init__.py
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Sorgo
                                 A QGIS plugin
 Complemento para identificar sitio propicios para el sorgo
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2026-09-18
        copyright            : (C) 2026 by UAEM
        email                : sorgo@uaemex.mx
        git sha              : $Format:%H$
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
 This script initializes the plugin, making it known to QGIS.
"""


# noinspection PyPep8Naming
def classFactory(iface):  # pylint: disable=invalid-name
    """Load Sorgo class from file Sorgo.

    :param iface: A QGIS interface instance.
    :type iface: QgsInterface
    """
    #
    from .sitio_sorgo import Sorgo
    return Sorgo(iface)
  • def classFactory(iface):: función con nombre fijo que QGIS llama al cargar el complemento; recibe iface, la conexión con QGIS.
  • from .sitio_sorgo import Sorgo: importación relativa de la clase principal, que está en el archivo sitio_sorgo.py de esta carpeta.
  • return Sorgo(iface): crea el objeto del complemento y lo devuelve a QGIS.
  • Personalizado: Sorgo es el Class name y sitio_sorgo el Module name escritos en el formulario de Plugin Builder; la descripción y la autoría del encabezado también.

Sección 2 · metadata.txt: la ficha del complemento

Archivo de texto con pares clave=valor que QGIS lee para mostrar el complemento en el Administrador de complementos.

metadata.txt
# This file contains metadata for your plugin.

# This file should be included when you package your plugin.# Mandatory items:

[general]
name=Sitios sorgo
qgisMinimumVersion=3.0
description=Complemento para identificar sitio propicios para el sorgo
version=0.1
author=UAEM
email=sorgo@uaemex.mx

about=Complemento de práctica para familiarizarse con la creación de complementos de QGIS. Este complemento identifica sitios potenciales para el cultivo del sorgo a partir de identificar suelos arcillosos y la altura de la elevación de una inundación.

tracker=https://www.uaemex.mx
repository=https://www.uaemex.mx
# End of mandatory metadata

# Recommended items:

hasProcessingProvider=no
# Uncomment the following line and add your changelog:
# changelog=

# Tags are comma separated with spaces allowed
tags=python

homepage=https://www.uaemex.mx
category=Plugins
icon=icon.png
# experimental flag
experimental=True

# deprecated flag (applies to the whole plugin, not just a single version)
deprecated=False

# Since QGIS 3.8, a comma separated list of plugins to be installed
# (or upgraded) can be specified.
# Check the documentation for more information.
# plugin_dependencies=

Category of the plugin: Raster, Vector, Database or Web
# category=

# If the plugin can run on QGIS Server.
server=False
  • name=Sitios sorgo: nombre visible del complemento. Personalizado.
  • description y about: resumen y explicación larga. El about indica que el complemento identifica sitios potenciales para el sorgo a partir de los suelos arcillosos y la altura de la elevación de una inundación. Personalizados.
  • author, email, tracker, repository, homepage: autoría y direcciones web. Personalizados.
  • tags=python: palabra clave para las búsquedas. Personalizada.
  • qgisMinimumVersion=3.0, version=0.1, hasProcessingProvider=no, icon=icon.png, experimental=True, deprecated=False, server=False: valores de la plantilla, igual que en el ejercicio 9.

Sección 3 · sitio_sorgo.py: la clase principal

Este es el archivo donde el creador escribió su lógica. Primero se muestra completo y después se explica por partes: importaciones, partes de la plantilla que solo cambian de nombre, métodos nuevos y el método run.

sitio_sorgo.py
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Sorgo
                                 A QGIS plugin
 Complemento para identificar sitio propicios para el sorgo
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2026-09-18
        git sha              : $Format:%H$
        copyright            : (C) 2026 by UAEM
        email                : sorgo@uaemex.mx
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 QLocale, QTranslator, QCoreApplication
from qgis.core import QgsSettings
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction

# Import the code for the dialog
from .sitio_sorgo_dialog import SorgoDialog
import os.path

#Importar las bibliotecas para nuestro complemento
from qgis.PyQt.QtWidgets import * #Funcionalidad de los widgets de QtDesigner
from qgis import processing #Para tareas de procesamiento
from qgis.core import * #Para funciones de QGIS

class Sorgo:
    """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 = QgsSettings().value('locale/userLocale', QLocale().name())[0:2]
        locale_path = os.path.join(
            self.plugin_dir,
            'i18n',
            '{}.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'&Sitios sorgo')

        # 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('Sorgo', 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 = os.path.join(os.path.dirname(__file__), 'icon.png')
        self.add_action(
            icon_path,
            text=self.tr(u'Sitios sorgo'),
            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'&Sitios sorgo'),
                action)
            self.iface.removeToolBarIcon(action)


    def abrir_capa_suelo(self):
        #Llamar al cuadro de diálogo abrir
        capa_suelo = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar capa de suelo', '', 'Raster (*.rst *.tif)')[0]
        #Poner la capa de suelo en el edit correspondiente
        self.dlg.edit_suelo.setText(capa_suelo)

    def abrir_capa_dem(self):
        #Llamar al cuadro de diálogo abrir
        capa_dem = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar capa de DEM', '', 'Raster (*.rst *.tif)')[0]
        #Poner la capa DEM en el edit correspondiente
        self.dlg.edit_dem.setText(capa_dem)

    def guardar_capa_sorgo(self):
        #Llamar al cuadro de diálogo guardar
        capa_resultado = QFileDialog.getSaveFileName(self.dlg, 'Guardar capa de resultados', '', 'Raster (*.tif)')[0]
        #Poner la capa de dorgo en el edit correspondiente
        self.dlg.edit_sorgo.setText(capa_resultado)

    def cancelar(self):
        #Limpiar todos los "edit" de nuestra interfaz
        for widget in self.dlg.findChildren(QLineEdit):
            widget.clear() 
        self.dlg.close()

    def aceptar(self):
        #En este método se hace el procesamiento para identificar sitios para sorgo
        suelo = self.dlg.edit_suelo.text()
        dem = self.dlg.edit_dem.text()
        nivel_agua = self.dlg.edit_elevacion.text()
        resultado = self.dlg.edit_sorgo.text()

        results = {}
        outputs = {}

        # Clase zona inundable
        alg_params = {
            'DATA_TYPE': 5,  # Float32
            'INPUT_RASTER': dem,
            'NODATA_FOR_MISSING': False,
            'NO_DATA': -9999,
            'RANGE_BOUNDARIES': 1,  # min <= valor < max
            'RASTER_BAND': 1,
            'TABLE': ['0',nivel_agua,'1',nivel_agua,'999','0'],
            'OUTPUT': "TEMPORARY_OUTPUT"
        }
        outputs['ClaseZonaInundable'] = processing.run('native:reclassifybytable', alg_params)

        # Clase arcilla
        alg_params = {
            'DATA_TYPE': 5,  # Float32
            'INPUT_RASTER': suelo,
            'NODATA_FOR_MISSING': False,
            'NO_DATA': -9999,
            'RANGE_BOUNDARIES': 1,  # min <= valor < max
            'RASTER_BAND': 1,
            'TABLE': ['0','2','0','2','3','1','3','99','0'],
            'OUTPUT': "TEMPORARY_OUTPUT"
        }
        outputs['ClaseArcilla'] = processing.run('native:reclassifybytable', alg_params)

        # Zonas sorgo
        alg_params = {
            'DATA_TYPE': 3,  # Int32
            'INPUT': [outputs['ClaseArcilla']['OUTPUT'],outputs['ClaseZonaInundable']['OUTPUT']],
            'NODATA_AS_FALSE': False,
            'NO_DATA': -9999,
            'REF_LAYER': outputs['ClaseZonaInundable']['OUTPUT'],
            'OUTPUT': resultado
        }
        outputs['ZonasSorgo'] = processing.run('native:rasterbooleanand', alg_params)
        results['ZonasSorgo'] = outputs['ZonasSorgo']['OUTPUT']

        #Deplegar el resultado
        rlayer = QgsRasterLayer(results['ZonasSorgo'], 'Zonas potenciales para sorgo')
        QgsProject.instance().addMapLayer(rlayer)
        
    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 = SorgoDialog()

            #Acciones de nuestro complemento
            #Acción para abrir la capa de suelo
            self.dlg.button_suelo.clicked.connect(self.abrir_capa_suelo)
            #Acción para abrir la capa de DEM
            self.dlg.button_dem.clicked.connect(self.abrir_capa_dem)
            #Accion para guardar el resultado
            self.dlg.button_sorgo.clicked.connect(self.guardar_capa_sorgo)
            #Acción para el botón "Cerrar"
            self.dlg.button_cancelar.clicked.connect(self.cancelar)
            #Acción para el botón "Aceptar"
            self.dlg.button_aceptar.clicked.connect(self.aceptar)

        # show the dialog
        self.dlg.show()
        

3.1 · Para qué sirve cada biblioteca importada

Las cuatro primeras importaciones (QtCore, QgsSettings, QIcon, QAction) y os.path vienen de la plantilla y se explican en el ejercicio 9. El creador agregó tres:

sitio_sorgo.py (fragmento)
#Importar las bibliotecas para nuestro complemento
from qgis.PyQt.QtWidgets import * #Funcionalidad de los widgets de QtDesigner
from qgis import processing #Para tareas de procesamiento
from qgis.core import * #Para funciones de QGIS
  • from qgis.PyQt.QtWidgets import *: importa todos los elementos visuales de Qt. De ellos el código usa QFileDialog (ventanas para abrir y guardar archivos) y QLineEdit (cuadros de texto). El comentario del creador lo describe como la "funcionalidad de los widgets de Qt Designer".
  • from qgis import processing: el módulo processing ejecuta las herramientas de la caja de herramientas de QGIS con processing.run. Es el mismo módulo del ejercicio 2.
  • from qgis.core import *: importa todas las clases de la biblioteca principal de QGIS. El código usa QgsRasterLayer (para crear una capa ráster a partir de un archivo) y QgsProject (el proyecto abierto).
  • Observación: el asterisco (*) es cómodo, pero importa cientos de nombres y no deja claro cuáles se usan realmente. En el ejercicio 11 se importan solo los nombres necesarios.

3.2 · Lo que de la plantilla solo cambia de nombre

El constructor __init__, el método tr, add_action, initGui y unload funcionan exactamente igual que en el ejercicio 9. Lo único que difiere son los textos escritos en el formulario (franjas amarillas):

  • class Sorgo:: nombre de la clase (Class name).
  • self.menu = self.tr(u'&Sitios sorgo'): nombre del menú del complemento; el & marca la letra del atajo de teclado.
  • QCoreApplication.translate('Sorgo', message): el contexto de traducción es el nombre de la clase.
  • text=self.tr(u'Sitios sorgo') en initGui: texto de la opción del menú.
  • self.tr(u'&Sitios sorgo') en unload: debe ser idéntico a self.menu; si no coincide, la opción del menú no se elimina al desactivar el complemento.

3.3 · Métodos para elegir las capas: abrir_capa_suelo, abrir_capa_dem y guardar_capa_sorgo

sitio_sorgo.py (fragmento)
    def abrir_capa_suelo(self):
        #Llamar al cuadro de diálogo abrir
        capa_suelo = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar capa de suelo', '', 'Raster (*.rst *.tif)')[0]
        #Poner la capa de suelo en el edit correspondiente
        self.dlg.edit_suelo.setText(capa_suelo)

    def abrir_capa_dem(self):
        #Llamar al cuadro de diálogo abrir
        capa_dem = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar capa de DEM', '', 'Raster (*.rst *.tif)')[0]
        #Poner la capa DEM en el edit correspondiente
        self.dlg.edit_dem.setText(capa_dem)

    def guardar_capa_sorgo(self):
        #Llamar al cuadro de diálogo guardar
        capa_resultado = QFileDialog.getSaveFileName(self.dlg, 'Guardar capa de resultados', '', 'Raster (*.tif)')[0]
        #Poner la capa de dorgo en el edit correspondiente
        self.dlg.edit_sorgo.setText(capa_resultado)
  • def abrir_capa_suelo(self):: método de la clase; recibe solo self, porque QGIS lo llamará cuando se haga clic en el botón y no necesita más datos.
  • QFileDialog.getOpenFileName(self.dlg, 'Seleccionar capa de suelo', '', 'Raster (*.rst *.tif)'): abre la ventana del sistema para elegir un archivo existente. Sus argumentos son: la ventana padre (self.dlg), el título, la carpeta inicial ('' = la predeterminada) y el filtro de tipos de archivo (solo .rst y .tif).
  • [0]: getOpenFileName devuelve una pareja (tupla) con la ruta elegida y el filtro usado; el índice [0] se queda solo con la ruta. Si el usuario cancela, la ruta es una cadena vacía.
  • self.dlg.edit_suelo.setText(capa_suelo): escribe la ruta en el cuadro de texto edit_suelo de la ventana. Ese texto se leerá después en aceptar.
  • abrir_capa_dem: hace lo mismo para el modelo digital de elevación y escribe en edit_dem.
  • guardar_capa_sorgo: usa QFileDialog.getSaveFileName, que permite elegir un archivo nuevo donde guardar (filtro Raster (*.tif)), y escribe la ruta en edit_sorgo.

3.4 · Para qué sirve el método cancelar

sitio_sorgo.py (fragmento)
    def cancelar(self):
        #Limpiar todos los "edit" de nuestra interfaz
        for widget in self.dlg.findChildren(QLineEdit):
            widget.clear() 
        self.dlg.close()
  • self.dlg.findChildren(QLineEdit): busca dentro de la ventana todos los elementos que sean cuadros de texto (QLineEdit) y devuelve una lista.
  • for widget in ...: widget.clear(): recorre la lista y vacía cada cuadro, de modo que la próxima vez que se abra la ventana esté limpia.
  • self.dlg.close(): cierra la ventana.

3.5 · Para qué sirve el método aceptar: el procesamiento

Este método replica el modelo del ejercicio 2, pero toma sus datos de la ventana y no de parámetros de una herramienta. Se analiza en cinco pasos.

Paso 1 · Leer los datos de la ventana

sitio_sorgo.py (fragmento)
    def aceptar(self):
        #En este método se hace el procesamiento para identificar sitios para sorgo
        suelo = self.dlg.edit_suelo.text()
        dem = self.dlg.edit_dem.text()
        nivel_agua = self.dlg.edit_elevacion.text()
        resultado = self.dlg.edit_sorgo.text()

        results = {}
        outputs = {}
  • suelo = self.dlg.edit_suelo.text(): text() devuelve el contenido del cuadro de texto como cadena. Se leen así la capa de suelo, el DEM, la elevación de la inundación (edit_elevacion) y la ruta de salida (edit_sorgo).
  • results = {} y outputs = {}: diccionarios vacíos, igual que en el ejercicio 2: outputs guarda los resultados intermedios y results el resultado final.
  • Observación: el código no valida los datos. Si algún cuadro está vacío o el nivel de agua no es un número, la herramienta de QGIS fallará.

Paso 2 · Reclasificar la zona inundable

sitio_sorgo.py (fragmento)
        # Clase zona inundable
        alg_params = {
            'DATA_TYPE': 5,  # Float32
            'INPUT_RASTER': dem,
            'NODATA_FOR_MISSING': False,
            'NO_DATA': -9999,
            'RANGE_BOUNDARIES': 1,  # min <= valor < max
            'RASTER_BAND': 1,
            'TABLE': ['0',nivel_agua,'1',nivel_agua,'999','0'],
            'OUTPUT': "TEMPORARY_OUTPUT"
        }
        outputs['ClaseZonaInundable'] = processing.run('native:reclassifybytable', alg_params)
  • 'INPUT_RASTER': dem: el ráster de entrada es la ruta del DEM que el usuario eligió.
  • 'TABLE': ['0',nivel_agua,'1',nivel_agua,'999','0']: tabla de reclasificación en grupos de tres valores (mínimo, máximo, valor nuevo). Las elevaciones entre 0 y nivel_agua reciben el valor 1 y las que van de nivel_agua a 999 reciben 0. En el ejercicio 2 el límite era fijo ('11'); aquí es el valor que escribe el usuario, y esa es la principal mejora de pasar de una herramienta a un complemento con ventana.
  • 'OUTPUT': "TEMPORARY_OUTPUT": el resultado es una capa temporal, porque solo sirve como paso intermedio. Es el mismo texto que representa la constante QgsProcessing.TEMPORARY_OUTPUT del ejercicio 2.
  • processing.run('native:reclassifybytable', alg_params): ejecuta la herramienta. Observa que ya no se envían context, feedback ni is_child_algorithm: esos argumentos solo se necesitan dentro de un QgsProcessingAlgorithm.

Paso 3 · Reclasificar el suelo arcilloso

sitio_sorgo.py (fragmento)
        # Clase arcilla
        alg_params = {
            'DATA_TYPE': 5,  # Float32
            'INPUT_RASTER': suelo,
            'NODATA_FOR_MISSING': False,
            'NO_DATA': -9999,
            'RANGE_BOUNDARIES': 1,  # min <= valor < max
            'RASTER_BAND': 1,
            'TABLE': ['0','2','0','2','3','1','3','99','0'],
            'OUTPUT': "TEMPORARY_OUTPUT"
        }
        outputs['ClaseArcilla'] = processing.run('native:reclassifybytable', alg_params)
  • 'INPUT_RASTER': suelo: ahora la entrada es la capa de suelo.
  • 'TABLE': ['0','2','0','2','3','1','3','99','0']: igual que en el ejercicio 2: el rango de 2 a 3 (la clase de suelo arcilloso) recibe 1 y todo lo demás recibe 0. Esta tabla sí es fija porque el criterio del suelo no se pide al usuario.
  • outputs['ClaseArcilla'] = processing.run(...): guarda el resultado intermedio bajo su propia llave.

Paso 4 · Combinar ambas capas con un "Y" booleano

sitio_sorgo.py (fragmento)
        # Zonas sorgo
        alg_params = {
            'DATA_TYPE': 3,  # Int32
            'INPUT': [outputs['ClaseArcilla']['OUTPUT'],outputs['ClaseZonaInundable']['OUTPUT']],
            'NODATA_AS_FALSE': False,
            'NO_DATA': -9999,
            'REF_LAYER': outputs['ClaseZonaInundable']['OUTPUT'],
            'OUTPUT': resultado
        }
        outputs['ZonasSorgo'] = processing.run('native:rasterbooleanand', alg_params)
        results['ZonasSorgo'] = outputs['ZonasSorgo']['OUTPUT']
  • 'INPUT': [outputs['ClaseArcilla']['OUTPUT'], outputs['ClaseZonaInundable']['OUTPUT']]: lista con las dos capas reclasificadas. La herramienta native:rasterbooleanand marca con 1 solo las celdas donde ambas capas valen 1.
  • 'REF_LAYER': capa que define la extensión y la resolución del resultado.
  • 'OUTPUT': resultado: a diferencia de los pasos anteriores, esta salida no es temporal: es la ruta que el usuario eligió con el botón de guardar.
  • results['ZonasSorgo'] = outputs['ZonasSorgo']['OUTPUT']: guarda la ruta del archivo final.
  • Como se explicó en el ejercicio 2, conviene revisar el significado real del valor 1 en la capa de zona inundable: aquí marca las elevaciones por debajo del nivel de agua indicado, que junto con el suelo arcilloso describen los sitios que busca el complemento.

Paso 5 · Mostrar el resultado en QGIS

sitio_sorgo.py (fragmento)
        #Deplegar el resultado
        rlayer = QgsRasterLayer(results['ZonasSorgo'], 'Zonas potenciales para sorgo')
        QgsProject.instance().addMapLayer(rlayer)
  • rlayer = QgsRasterLayer(results['ZonasSorgo'], 'Zonas potenciales para sorgo'): crea un objeto de capa ráster a partir del archivo recién guardado, con el nombre que se verá en el panel de capas.
  • QgsProject.instance().addMapLayer(rlayer): QgsProject.instance() es el proyecto abierto en QGIS; addMapLayer agrega la capa al proyecto para que aparezca en el mapa. Hay que pasar un objeto de capa, no solo la ruta del archivo.

3.6 · Para qué sirve el método run

sitio_sorgo.py (fragmento)
    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 = SorgoDialog()

            #Acciones de nuestro complemento
            #Acción para abrir la capa de suelo
            self.dlg.button_suelo.clicked.connect(self.abrir_capa_suelo)
            #Acción para abrir la capa de DEM
            self.dlg.button_dem.clicked.connect(self.abrir_capa_dem)
            #Accion para guardar el resultado
            self.dlg.button_sorgo.clicked.connect(self.guardar_capa_sorgo)
            #Acción para el botón "Cerrar"
            self.dlg.button_cancelar.clicked.connect(self.cancelar)
            #Acción para el botón "Aceptar"
            self.dlg.button_aceptar.clicked.connect(self.aceptar)

        # show the dialog
        self.dlg.show()
  • if self.first_start == True:: igual que en el ejercicio 9, la ventana se crea una sola vez (self.dlg = SorgoDialog()).
  • Conexión de los botones: las cinco líneas self.dlg.button_...clicked.connect(self.método) enlazan cada botón con su método. Se hacen dentro del if para que ocurran una sola vez; si se repitieran en cada apertura, un clic ejecutaría el mismo método varias veces.
    • button_suelo → abrir_capa_suelo
    • button_dem → abrir_capa_dem
    • button_sorgo → guardar_capa_sorgo
    • button_cancelar → cancelar
    • button_aceptar → aceptar
  • self.dlg.show(): muestra la ventana. Ya no aparecen exec_() ni la revisión del resultado de la plantilla, porque la ventana no usa los botones OK/Cancel estándar: sus botones propios llaman directamente a aceptar y cancelar.

Sección 4 · sitio_sorgo_dialog.py: la clase de la ventana

Es el mismo archivo que en el ejercicio 9 (carga el .ui con uic.loadUiType y hereda de QDialog y FORM_CLASS); solo cambian los nombres de la clase y del módulo, escritos en el formulario.

sitio_sorgo_dialog.py
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 SorgoDialog
                                 A QGIS plugin
 Complemento para identificar sitio propicios para el sorgo
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2026-09-18
        git sha              : $Format:%H$
        copyright            : (C) 2026 by UAEM
        email                : sorgo@uaemex.mx
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os

from qgis.PyQt import uic
from qgis.PyQt import QtWidgets

# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer
FORM_CLASS, _ = uic.loadUiType(
    os.path.join(
        os.path.dirname(__file__),
        'sitio_sorgo_dialog_base.ui'),
    from_imports=True,
    import_from='sitio_sorgo')


class SorgoDialog(QtWidgets.QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(SorgoDialog, self).__init__(parent)
        # Set up the user interface from Designer through FORM_CLASS.
        # After self.setupUi() you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)
  • FORM_CLASS, _ = uic.loadUiType(...): lee sitio_sorgo_dialog_base.ui y devuelve una clase que sabe construir los elementos del diseño.
  • class SorgoDialog(QtWidgets.QDialog, FORM_CLASS):: herencia múltiple; la clase es una ventana de diálogo que incluye el diseño del .ui.
  • self.setupUi(self): construye los elementos visuales y los convierte en atributos (self.edit_suelo, self.button_aceptar...), que la clase principal usa como self.dlg.edit_suelo.

Sección 5 · sitio_sorgo_dialog_base.ui: el diseño de la ventana

En el ejercicio 9 la ventana solo tenía los botones OK/Cancel y una casilla. Aquí el creador eliminó esos botones y diseñó en Qt Designer toda la ventana (franja azul): 4 etiquetas, 4 cuadros de texto y 5 botones.

sitio_sorgo_dialog_base.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>SorgoDialogBase</class>
 <widget class="QDialog" name="SorgoDialogBase">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>521</width>
    <height>293</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Sitios sorgo</string>
  </property>
  <widget class="QLineEdit" name="edit_suelo">
   <property name="geometry">
    <rect>
     <x>140</x>
     <y>40</y>
     <width>321</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QPushButton" name="button_suelo">
   <property name="geometry">
    <rect>
     <x>470</x>
     <y>40</y>
     <width>31</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>...</string>
   </property>
  </widget>
  <widget class="QLabel" name="label_suelo">
   <property name="geometry">
    <rect>
     <x>20</x>
     <y>40</y>
     <width>111</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Capa ráster de suelo</string>
   </property>
  </widget>
  <widget class="QLabel" name="label_dem">
   <property name="geometry">
    <rect>
     <x>20</x>
     <y>70</y>
     <width>111</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Capa ráster del DEM</string>
   </property>
  </widget>
  <widget class="QLineEdit" name="edit_dem">
   <property name="geometry">
    <rect>
     <x>140</x>
     <y>70</y>
     <width>321</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QPushButton" name="button_dem">
   <property name="geometry">
    <rect>
     <x>470</x>
     <y>70</y>
     <width>31</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>...</string>
   </property>
  </widget>
  <widget class="QLabel" name="label_elevacion">
   <property name="geometry">
    <rect>
     <x>20</x>
     <y>100</y>
     <width>151</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Elevación de la inudación (m)</string>
   </property>
  </widget>
  <widget class="QLineEdit" name="edit_elevacion">
   <property name="geometry">
    <rect>
     <x>180</x>
     <y>100</y>
     <width>113</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QLabel" name="label_sorgo">
   <property name="geometry">
    <rect>
     <x>20</x>
     <y>160</y>
     <width>111</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Capa ráster de sorgo</string>
   </property>
  </widget>
  <widget class="QLineEdit" name="edit_sorgo">
   <property name="geometry">
    <rect>
     <x>140</x>
     <y>160</y>
     <width>321</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QPushButton" name="button_sorgo">
   <property name="geometry">
    <rect>
     <x>470</x>
     <y>160</y>
     <width>31</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>...</string>
   </property>
  </widget>
  <widget class="QPushButton" name="button_aceptar">
   <property name="geometry">
    <rect>
     <x>160</x>
     <y>230</y>
     <width>75</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>Aceptar</string>
   </property>
  </widget>
  <widget class="QPushButton" name="button_cancelar">
   <property name="geometry">
    <rect>
     <x>280</x>
     <y>230</y>
     <width>75</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>Cancelar</string>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>
  • <widget class="QDialog" name="SorgoDialogBase">: la ventana principal, de 521 × 293 píxeles. windowTitle es el título: personalizado ("Sitios sorgo").
  • QLabel (label_suelo, label_dem, label_elevacion, label_sorgo): etiquetas con el texto que orienta al usuario. No se usan desde Python.
  • QLineEdit (edit_suelo, edit_dem, edit_elevacion, edit_sorgo): cuadros de texto. Python lee lo escrito en ellos con .text() y escribe en ellos con .setText(). El de la elevación (edit_elevacion) es el que recibe el nivel de la inundación que el usuario teclea.
  • QPushButton con texto ... (button_suelo, button_dem, button_sorgo): botones que abren los diálogos de archivo; cada uno está a la derecha de su cuadro de texto.
  • QPushButton button_aceptar y button_cancelar: botones que ejecutan el análisis y cierran la ventana.
  • <connections/>: vacío. A diferencia del ejercicio 9, ninguna señal se conectó en el diseño; todas se conectaron con Python en el método run.
  • Los nombres (objectName) son importantes: el código Python debe usar exactamente los mismos. Un error de escritura (por ejemplo edit_Suelo) produciría un AttributeError.

Sección 6 · pb_tool.cfg: configuración para desplegar

Lo usa la herramienta pb_tool para copiar el complemento a la carpeta de complementos de QGIS con pb_tool deploy. Su estructura es la del ejercicio 9.

pb_tool.cfg
#/***************************************************************************
# Sorgo
#
# Configuration file for plugin builder tool (pb_tool)
# Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
#                            -------------------
#       begin               : 2026-09-18
#       copyright           : (C) 2026 by UAEM
#       email               : sorgo@uaemex.mx
# ***************************************************************************/
#
#/***************************************************************************
# *                                                                         *
# *   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.                                   *
# *                                                                         *
# ***************************************************************************/
# 
# 
# You can install pb_tool using:
#  pip install http://geoapt.net/files/pb_tool.zip
# 
# Consider doing your development (and install of pb_tool) in a virtualenv.
# 
# For details on setting up and using pb_tool, see:
#  http://g-sherman.github.io/plugin_build_tool/
#
# Issues and pull requests here:
# https://github.com/g-sherman/plugin_build_tool:
# 
# Sane defaults for your plugin generated by the Plugin Builder are
# already set below.
# 
# As you add Python source files and UI files to your plugin, add
# them to the appropriate [files] section below.

[plugin]
# Name of the plugin. This is the name of the directory that will
# be created in .qgis2/python/plugins
name: sitio_sorgo

# Full path to where you want your plugin directory copied. If empty,
# the QGIS default path will be used. Don't include the plugin name in
# the path.
plugin_path: C:/Users/rico_/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins

[files]
# Python  files that should be deployed with the plugin
python_files: __init__.py sitio_sorgo.py sitio_sorgo_dialog.py

# The main dialog file that is loaded (not compiled)
main_dialog: sitio_sorgo_dialog_base.ui

# Other ui files for dialogs you create (these will be compiled)
compiled_ui_files: 

# Other files required for the plugin
extras: metadata.txt icon.png

# Other directories to be deployed with the plugin.
# These must be subdirectories under the plugin directory
extra_dirs:

# ISO code(s) for any locales (translations), separated by spaces.
# Corresponding .ts files must exist in the i18n directory
locales:

# Resource files
resource_files:

[help]
# the built help directory that should be deployed with the plugin
dir: help/build/html
# the name of the directory to target in the deployed plugin 
target: help
  • name: sitio_sorgo: nombre de la carpeta que se crea al desplegar. Personalizado (módulo del formulario).
  • plugin_path: carpeta de complementos del perfil de QGIS del usuario; es el destino del despliegue.
  • python_files y main_dialog: archivos .py y .ui que se copian. Personalizados (sus nombres salen del formulario).
  • extras: otros archivos necesarios, como metadata.txt e icon.png.

Resumen: lo que personalizó el creador

  • Franja amarilla (formulario de Plugin Builder): el nombre de la clase (Sorgo) y del módulo (sitio_sorgo), el nombre del complemento ("Sitios sorgo"), la descripción, la autoría (UAEM), el correo, las direcciones web y la etiqueta.
  • Franja azul (programado a mano): las importaciones adicionales, los cinco métodos propios (abrir_capa_suelo, abrir_capa_dem, guardar_capa_sorgo, cancelar y aceptar), la conexión de los botones en run y el diseño completo de la ventana en el archivo .ui.
  • Diferencia con el ejercicio 9: el complemento ya hace un trabajo real: el mismo análisis del ejercicio 2, pero con capas y nivel de inundación elegidos por el usuario en una ventana.
Pon a prueba lo analizado
Volver a PASIG II