4  Parte II - O primeiro plugin

4.1 4. Estrutura mínima do pacote

4.1.1 4.1 metadata.txt

O metadata.txt descreve o plugin ao QGIS e ao repositório oficial.

[general]
name=Quick Point Logger
description=Captures map clicks into a temporary point layer.
version=0.1.0
qgisMinimumVersion=3.28
qgisMaximumVersion=4.99
author=Seu Nome
email=nome@example.org
homepage=https://github.com/user/quick-point-logger#readme
repository=https://github.com/user/quick-point-logger
tracker=https://github.com/user/quick-point-logger/issues
license=MIT
category=Vector
tags=point,capture,coordinates,logging
experimental=True
deprecated=False
hasProcessingProvider=False
supportsQt6=True
icon=icons/capture.svg

4.1.1.1 Campos essenciais

  • name: nome público, estável e distinto;
  • description: uma frase curta;
  • about: explicação detalhada, dependências e limitações;
  • version: versão SemVer;
  • qgisMinimumVersion e qgisMaximumVersion;
  • repository, tracker, homepage e license;
  • supportsQt6=True quando validado para Qt 6.

O repositório oficial exige documentação mínima, links funcionais, licença compatível, descrição em inglês, ausência de binários e um pacote dentro do limite de tamanho aplicável.

4.1.2 4.2 __init__.py

def classFactory(iface):
    """Return the plugin instance used by QGIS."""
    from .plugin import QuickPointLoggerPlugin
    return QuickPointLoggerPlugin(iface)

Mantenha esta função pequena. A importação dentro de classFactory() reduz efeitos colaterais durante a descoberta do plugin.

4.1.3 4.3 Classe principal

from pathlib import Path

from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction


class QuickPointLoggerPlugin:
    MENU = "&Quick Point Logger"

    def __init__(self, iface):
        self.iface = iface
        self.action = None
        self.icon_path = Path(__file__).parent / "icons" / "capture.svg"

    def initGui(self):
        self.action = QAction(
            QIcon(str(self.icon_path)),
            "Quick Point Logger",
            self.iface.mainWindow(),
        )
        self.action.triggered.connect(self.run)
        self.iface.addPluginToVectorMenu(self.MENU, self.action)
        self.iface.addToolBarIcon(self.action)

    def run(self):
        self.iface.messageBar().pushInfo(
            "Quick Point Logger",
            "The plugin is running.",
        )

    def unload(self):
        if self.action is None:
            return
        self.iface.removePluginVectorMenu(self.MENU, self.action)
        self.iface.removeToolBarIcon(self.action)
        self.action.deleteLater()
        self.action = None

Este padrão aparece no GPX Batch Converter: initGui() cria QAction, associa triggered ao método run(), adiciona ao menu Vector e à barra; unload() remove os elementos e fecha a interface.

4.1.4 4.4 Ciclo de vida e limpeza

Um plugin que funciona ao activar, mas deixa sinais ou tarefas depois de desactivar, é instável. No unload():

  • desassocie ou remova acções;
  • cancele tarefas activas;
  • aborte respostas de rede;
  • remova painéis;
  • desactive ferramentas de mapa;
  • chame deleteLater() para widgets Qt;
  • limpe referências Python.

4.2 5. Acções, menus e barra de ferramentas

QAction representa uma operação reutilizável. A mesma acção pode estar no menu e na barra.

self.action = QAction(QIcon(icon_path), "Converter GPX", parent)
self.action.setObjectName("gpxBatchConverterAction")
self.action.setStatusTip("Converter vários ficheiros GPX")
self.action.triggered.connect(self.run)

Escolha o menu por domínio:

  • Vector para operações vectoriais;
  • Raster para raster;
  • Web para serviços Web;
  • Database para bases de dados.

Não crie um menu de topo desnecessário para uma única acção.

4.2.1 5.1 Acções verificáveis

Para ligar e desligar modos:

self.capture_action.setCheckable(True)
self.capture_action.toggled.connect(self.activate_capture)

Sincronize o estado entre toolbar, menu e painel. Ao mudar programaticamente, bloqueie sinais para evitar ciclos:

self.capture_action.blockSignals(True)
self.capture_action.setChecked(enabled)
self.capture_action.blockSignals(False)

4.2.2 5.2 Ícones

Use SVG simples ou PNG optimizado. O ícone deve permanecer legível em 16, 24 e 32 pixels. Use caminhos relativos ao pacote:

ICON_DIR = Path(__file__).parent / "icons"
QIcon(str(ICON_DIR / "capture.svg"))

GeoClick Capture 1.2.6 usa ícones separados para captura, log, snapping, geocodificação, exportação, undo, eliminação e sessões. A distinção visual reduz erros operacionais.

4.3 6. Interfaces: QDialog e QDockWidget

4.3.1 6.1 Quando usar cada um

Use QDialog para uma tarefa com início e fim, como converter ficheiros. Use QDockWidget para uma ferramenta que acompanha o trabalho no mapa, como um log de captura.

GPX Batch Converter: diálogo com pastas, formatos, opções, progresso e resultados.

GeoClick Capture: painel lateral com sessão, camada, snapping, tabela e exportação.

4.3.2 6.2 Interface programática ou Qt Designer

Interface programática:

  • facilita compatibilidade e revisão de código;
  • não exige compilar .ui;
  • funciona bem para formulários médios.

Qt Designer:

  • acelera layouts complexos;
  • separa desenho visual;
  • exige estratégia para carregar .ui sem ficheiros gerados desnecessários.

O repositório oficial recomenda evitar ficheiros gerados como ui_*.py quando não são necessários. Pode carregar o .ui em tempo de execução.

4.3.3 6.3 Um selector de pasta reutilizável

class FolderSelector(QWidget):
    def __init__(self, title, parent=None):
        super().__init__(parent)
        self.title = title
        self.path_edit = QLineEdit()
        self.browse_button = QPushButton("Browse...")
        self.browse_button.clicked.connect(self.browse)

        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.path_edit, 1)
        layout.addWidget(self.browse_button)

    def browse(self):
        selected = QFileDialog.getExistingDirectory(self, self.title)
        if selected:
            self.path_edit.setText(selected)

Transformar um conjunto repetido de widgets numa classe reduz duplicação e facilita validação.

4.3.4 6.4 Widgets QGIS

Sempre que possível, use widgets que conhecem o projecto. Exemplo:

from qgis.core import Qgis, QgsMapLayerProxyModel
from qgis.gui import QgsMapLayerComboBox

self.layer_combo = QgsMapLayerComboBox()
point_filter = getattr(QgsMapLayerProxyModel, "PointLayer", None)
if point_filter is None:
    point_filter = Qgis.LayerFilter.PointLayer
self.layer_combo.setFilters(point_filter)
self.layer_combo.setAllowEmptyLayer(True)
self.layer_combo.setShowCrs(True)

QgsMapLayerComboBox acompanha camadas adicionadas, removidas e renomeadas. Uma QComboBox manual torna-se rapidamente desactualizada.

4.4 7. Validação de entradas e experiência do utilizador

Valide antes de iniciar trabalho pesado:

  • pasta de entrada existe;
  • pasta de saída pode ser criada;
  • existe pelo menos um ficheiro compatível;
  • pelo menos uma camada ou opção foi seleccionada;
  • executáveis necessários foram encontrados;
  • camada de destino é válida e tem geometria adequada;
  • caminhos não são iguais quando isso causa sobrescrita perigosa.

Mensagens de erro devem indicar acção correctiva:

Fraco: Conversion failed.
Melhor: No .gpx files were found in the selected input folder.

Evite caixas modais para cada aviso dentro de um lote. Use log, tabela de resultados e resumo final.