Skip to content

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

Terminal window
pip install qgis-sdk

Pure 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:

Terminal window
qgis-plugin new my_plugin --web

Generated:

  • ui/main_dialog.ui — Qt Designer Dialog with Buttons Bottom, promoted QgsMapLayerComboBox
  • dialogs/main_dialog.py — loads via uic.loadUiType, WA_DeleteOnClose, QSettings persistence
from qgis.PyQt import QtWidgets, uic
from qgis.PyQt.QtCore import QSettings, Qt
import 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

Terminal window
qgis-plugin new my_plugin --web
# or add to existing
qgis-plugin ui add-web ./my_plugin

Generated:

  • web/map.html — Leaflet + qrc:///qtwebchannel/qwebchannel.js
  • dialogs/web_dialog.py — WebDialog + QWebChannel bridge
from qgis_sdk.ui import WebDialog, web_bridge
from 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

Terminal window
# Scaffold with UI + Web
qgis-plugin new my-plugin --web
qgis-plugin new my-plugin --no-ui
# UI helpers
qgis-plugin ui add-dialog ./my_plugin --name custom_dialog
qgis-plugin ui add-web ./my_plugin
# Development mode
qgis-plugin dev
# Build and package (includes ui/*.ui, web/*.html, icons/*)
qgis-plugin build
qgis-plugin package -o dist/
# Publish to QGIS Plugin Repository
qgis-plugin publish

Packaging

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:

Terminal window
qgis-plugin bridge generate --bridge my_plugin.dialogs.web_dialog:Bridge --output web/bridge.d.ts
qgis-plugin bridge generate --bridge my_plugin.dialogs.web_dialog:Bridge --output web/ --package

Python → 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.js
const 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:

tests/conftest.py
pytest_plugins = ["qgis_sdk.testing"]
# tests/test_plugin.py
def 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().

See Testing Fixtures Guide.

Next Steps