Plugin Development
Plugin Development
The qgis-sdk provides a Python framework for building QGIS plugins with UI dialogs, WebEngine HTML/CSS/JS, and optional Rust acceleration.
Installation
pip install qgis-sdkPure Python Plugin
from qgis_sdk import Plugin, Algorithm, parameter, output
class BufferAdvanced(Algorithm): id = "my_plugin:buffer" name = "Advanced Buffer" group = "Vector geometry"
input_layer = parameter.source("Input layer", required=True) distance = parameter.distance("Buffer distance", default=10.0) output_layer = output.sink("Buffered")
def process(self, context): source = context.get(self.input_layer) dist = context.get(self.distance) sink = context.create_sink(self.output_layer, ...)
for feature in source.features(): buffered = feature.geometry.buffer(dist) out = feature.clone() out.geometry = buffered sink.add_feature(out)
return {self.output_layer: sink}UI Dialogs (Qt Designer .ui)
Scaffold with UI:
qgis-plugin new my_plugin --webGenerated:
ui/main_dialog.ui— Qt Designer Dialog with Buttons Bottom, promotedQgsMapLayerComboBoxdialogs/main_dialog.py— loads viauic.loadUiType,WA_DeleteOnClose,QSettingspersistence
from qgis.PyQt import QtWidgets, uicfrom qgis.PyQt.QtCore import QSettings, Qtimport os
FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), "..", "ui", "main_dialog.ui"))
class MainDialog(QtWidgets.QDialog, FORM_CLASS): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.setAttribute(Qt.WA_DeleteOnClose)Declarative fallback (testable without QGIS)
from qgis_sdk.ui import Dialog, field, layout, Button
dlg = Dialog( title="My Tool", layout=layout.vertical( field.layer("input_layer", label="Input layer"), field.spin("threshold", label="Threshold", default=0.5), layout.buttons(Button.ok(), Button.cancel()) ), persist=True)if dlg.exec() == Dialog.Accepted: print(dlg.get("input_layer"))WebEngine HTML + QWebChannel
qgis-plugin new my_plugin --web# or add to existingqgis-plugin ui add-web ./my_pluginGenerated:
web/map.html— Leaflet +qrc:///qtwebchannel/qwebchannel.jsdialogs/web_dialog.py—WebDialog+QWebChannelbridge
from qgis_sdk.ui import WebDialog, web_bridgefrom pathlib import Path
dlg = WebDialog.from_file(Path("web/map.html"), title="Map View")
@web_bridge(dlg)class Bridge: def get_layer(self): return {"name": "buildings", "count": 100}
dlg.exec()<script src="qrc:///qtwebchannel/qwebchannel.js"></script><script>new QWebChannel(qt.webChannelTransport, function(channel) { bridge = channel.objects.bridge; bridge.get_layer(function(data) { console.log(data); });});</script>- JS → Python:
@pyqtSlot(result=QVariant)+channel.registerObject("bridge", obj) - Python → JS:
web_view.page().runJavaScript("updateFromPython({message: 'hello'})")
Important: from qgis.PyQt.QtWebEngineWidgets import QWebEngineView must be imported before QApplication (QGIS issue #49512).
Rust Acceleration
from qgis_sdk import rust_accelerated
class BufferFast(Algorithm): @rust_accelerated(fallback="process_python") def process(self, context): pass # Replaced by Rust at runtime
def process_python(self, context): # Pure Python fallback ...CLI
# Scaffold with UI + Webqgis-plugin new my-plugin --webqgis-plugin new my-plugin --no-ui
# UI helpersqgis-plugin ui add-dialog ./my_plugin --name custom_dialogqgis-plugin ui add-web ./my_plugin
# Development modeqgis-plugin dev
# Build and package (includes ui/*.ui, web/*.html, icons/*)qgis-plugin buildqgis-plugin package -o dist/
# Publish to QGIS Plugin Repositoryqgis-plugin publishPackaging
qgis-plugin package includes ui/, web/, icons/, dialogs/ automatically:
[tool.hatch.build.targets.wheel]include = [ "my_plugin/ui/*.ui", "my_plugin/web/*.html", "my_plugin/icons/*",]Typed Bridge with @qgis-sdk/bridge
Generate TypeScript types from Python bridge classes:
qgis-plugin bridge generate --bridge my_plugin.dialogs.web_dialog:Bridge --output web/bridge.d.tsqgis-plugin bridge generate --bridge my_plugin.dialogs.web_dialog:Bridge --output web/ --packagePython → TS with Promise API:
import { createBridge } from '@qgis-sdk/bridge';import type { Bridge } from './web/bridge.d.ts';
const bridge = await createBridge<Bridge>(); // auto-injects qrc:///qtwebchannel/qwebchannel.jsconst layer = await bridge.get_layer(); // typed!- React:
import { useQgisBridge } from '@qgis-sdk/bridge/react' - Vue:
import { useQgisBridge } from '@qgis-sdk/bridge/vue' - Web Components:
<qgis-bridge>+import '@qgis-sdk/bridge/webcomponents'
See Typed Bridge Guide and Web Frameworks Guide for full details.
Testing without QGIS
qgis_sdk.testing provides fakes and pytest fixtures:
pytest_plugins = ["qgis_sdk.testing"]
# tests/test_plugin.pydef test_toolbar(fake_iface, fake_action_factory): from my_plugin import MyPlugin MyPlugin.action_factory = staticmethod(fake_action_factory) plugin = MyPlugin(fake_iface) plugin.init_gui() assert len(fake_iface.toolbar_icons) == 1 fake_iface.toolbar_icons[0].trigger()
def test_bridge(fake_bridge): assert fake_bridge.get_layer()["name"] == "test_layer"Fakes: FakeIface, FakeAction, FakeContext, FakeSink, FakeFeature, FakeDialog, FakeWebView, FakeBridge, mock_features(), mock_source().
Next Steps
- Typed Bridge Guide — generate TS types, Promise API, auto-inject
- Web Frameworks Guide — React/Vue/Web Components in QWebEngine
- Testing Fixtures Guide — test without QGIS
- qgis-plugin-sdk and qgis-plugin-ui design docs