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,unloadyrun(que exige QGIS), la claseSorgodefine los métodosabrir_capa_suelo,abrir_capa_dem,guardar_capa_sorgo,cancelaryaceptar. Cada uno agrupa una tarea concreta, como en cualquier clase bien organizada. -
Atributo compartido entre métodos:
self.dlgguarda 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 asetupUi, se usa en Python comoself.dlg.edit_suelo. El nombre sigue la convención tipo_contenido:edit_para cuadros de texto,button_para botones ylabel_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:
QFileDialogabre las ventanas del sistema para elegir un archivo existente o un archivo nuevo donde guardar. -
Ejecutar herramientas de QGIS desde el código:
processing.runpermite usar cualquier algoritmo de la caja de herramientas. A diferencia del ejercicio 2, aquí no hay una claseQgsProcessingAlgorithm, por lo que las llamadas son más sencillas (sincontextnifeedback). -
Recorrer los elementos hijos de un contenedor:
findChildren(QLineEdit)devuelve todos los cuadros de texto de la ventana, y un cicloforpermite 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.
# -*- 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; recibeiface, la conexión con QGIS.from .sitio_sorgo import Sorgo: importación relativa de la clase principal, que está en el archivositio_sorgo.pyde esta carpeta.return Sorgo(iface): crea el objeto del complemento y lo devuelve a QGIS.- Personalizado:
Sorgoes el Class name ysitio_sorgoel 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.
# 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.descriptionyabout: resumen y explicación larga. Elaboutindica 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.
# -*- 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:
#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 usaQFileDialog(ventanas para abrir y guardar archivos) yQLineEdit(cuadros de texto). El comentario del creador lo describe como la "funcionalidad de los widgets de Qt Designer".from qgis import processing: el móduloprocessingejecuta las herramientas de la caja de herramientas de QGIS conprocessing.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 usaQgsRasterLayer(para crear una capa ráster a partir de un archivo) yQgsProject(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')eninitGui: texto de la opción del menú.self.tr(u'&Sitios sorgo')enunload: debe ser idéntico aself.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
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 soloself, 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.rsty.tif).[0]:getOpenFileNamedevuelve 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 textoedit_suelode la ventana. Ese texto se leerá después enaceptar.abrir_capa_dem: hace lo mismo para el modelo digital de elevación y escribe enedit_dem.guardar_capa_sorgo: usaQFileDialog.getSaveFileName, que permite elegir un archivo nuevo donde guardar (filtroRaster (*.tif)), y escribe la ruta enedit_sorgo.
3.4 · Para qué sirve el método cancelar
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
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 = {}youtputs = {}: diccionarios vacíos, igual que en el ejercicio 2:outputsguarda los resultados intermedios yresultsel 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
# 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 ynivel_aguareciben el valor 1 y las que van denivel_aguaa 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 constanteQgsProcessing.TEMPORARY_OUTPUTdel ejercicio 2.processing.run('native:reclassifybytable', alg_params): ejecuta la herramienta. Observa que ya no se envíancontext,feedbackniis_child_algorithm: esos argumentos solo se necesitan dentro de unQgsProcessingAlgorithm.
Paso 3 · Reclasificar el suelo arcilloso
# 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
# 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 herramientanative:rasterbooleanandmarca 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
#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;addMapLayeragrega 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
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 delifpara 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_suelobutton_dem→abrir_capa_dembutton_sorgo→guardar_capa_sorgobutton_cancelar→cancelarbutton_aceptar→aceptar
self.dlg.show(): muestra la ventana. Ya no aparecenexec_()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 aaceptarycancelar.
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.
# -*- 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(...): leesitio_sorgo_dialog_base.uiy 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 comoself.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.
<?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.windowTitlees 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.QPushButtoncon 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.QPushButtonbutton_aceptarybutton_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étodorun.- Los nombres (
objectName) son importantes: el código Python debe usar exactamente los mismos. Un error de escritura (por ejemploedit_Suelo) produciría unAttributeError.
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.
#/***************************************************************************
# 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_filesymain_dialog: archivos.pyy.uique se copian. Personalizados (sus nombres salen del formulario).extras: otros archivos necesarios, comometadata.txteicon.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,cancelaryaceptar), la conexión de los botones enruny 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.