Typed QWebChannel Bridge
Typed QWebChannel Bridge
Build type-safe communication between QGIS Python and HTML/JS with auto-generated TypeScript types.
Overview
QGIS WebEngine plugins traditionally use QWebChannel with manual JS wiring:
<script src="qrc:///qtwebchannel/qwebchannel.js"></script><script>new QWebChannel(qt.webChannelTransport, channel => { bridge = channel.objects.bridge; bridge.get_layer(data => console.log(data));});</script>@qgis-sdk/bridge improves this:
- Auto-injects
qrc:///qtwebchannel/qwebchannel.jswith fallbacks (./qwebchannel.js, CDN) - Promisifies callback-style to
Promise - Typed via auto-generated
bridge.d.tsfrom Python - Framework adapters for React, Vue, Web Components
- Python → JS via
CustomEvent('qgis-message')
Python Bridge
Define your bridge as a plain Python class with type hints:
from qgis_sdk.ui import WebDialog, web_bridgefrom pathlib import Path
class MapBridge: def get_layer(self) -> dict: return {"name": "buildings", "count": 100}
def get_extent(self) -> dict: return {"xmin": -180, "ymin": -90, "xmax": 180, "ymax": 90}
def log(self, msg: str) -> str: print(f"[JS] {msg}") return "ok"
def process(self, data: str) -> dict: import json return {"status": "ok", "action": json.loads(data).get("action")}
HTML_FILE = Path(__file__).parent.parent / "web" / "map.html"
def show_web_dialog(parent=None): dlg = WebDialog.from_file(HTML_FILE, title="Map View", width=900, height=700, parent=parent) dlg.set_bridge(MapBridge()) return dlg.exec()
# Decorator version (alternative)web_dlg = WebDialog.from_file(HTML_FILE, title="Map View")
@web_bridge(web_dlg)class Bridge: def get_layer(self) -> dict: return {"name": "buildings", "count": 100}Register in QGIS:
from PyQt5.QtWebChannel import QWebChannel
channel = QWebChannel()bridge = MapBridge()channel.registerObject("bridge", bridge) # name must match JSweb_view.page().setWebChannel(channel)Generate TypeScript Types
CLI
# Generate single .d.ts fileqgis-plugin bridge generate --bridge my_plugin.dialogs.web_dialog:Bridge --output web/bridge.d.ts
# Generate full package (bridge.d.ts + bridge.js + react.ts + vue.ts + webcomponents.ts)qgis-plugin bridge generate --bridge my_plugin.dialogs.web_dialog:Bridge --output web/ --package
# Custom namesqgis-plugin bridge generate --bridge my_plugin.dialogs.web_dialog:MapBridge --output web/bridge.d.ts --name MapBridge --object-name bridge --framework reactSupports both notations:
module:Class→my_plugin.dialogs.web_dialog:Bridgemodule.Class→my_plugin.dialogs.web_dialog.Bridge
Python API
from qgis_sdk.bridge import generate_ts_bridge, generate_js_wrapper, generate_packagefrom my_plugin.dialogs.web_dialog import Bridgefrom pathlib import Path
# TS interfacets_code = generate_ts_bridge(Bridge, name="Bridge")Path("web/bridge.d.ts").write_text(ts_code)
# JS wrapper (standalone, no npm)js_code = generate_js_wrapper(Bridge, name="Bridge", object_name="bridge")Path("web/bridge.js").write_text(js_code)
# Full packagegenerate_package(Bridge, output_dir="frontend/src/qgis-bridge", name="Bridge")Generated Output
/** * Auto-generated from Python my_plugin.dialogs.web_dialog.Bridge * Generated by qgis_sdk.bridge.generate_ts_bridge */export interface Bridge { get_layer(callback: (result: Record<string, any>) => void): void; get_layer(): Promise<Record<string, any>>; get_extent(callback: (result: Record<string, any>) => void): void; get_extent(): Promise<Record<string, any>>; log(msg: string, callback: (result: string) => void): void; log(msg: string): Promise<string>; process(data: string, callback: (result: Record<string, any>) => void): void; process(data: string): Promise<Record<string, any>>;}
export interface BridgeState { bridge: Bridge | null; ready: boolean;}Type mapping:
| Python | TypeScript |
|---|---|
str | string |
int, float | number |
bool | boolean |
dict, Dict | Record<string, any> |
list, List[X] | any[] or X[] |
Optional[X] | X | null |
None | void |
| custom class | any |
JavaScript Usage
Install
npm install @qgis-sdk/bridge# pnpm add @qgis-sdk/bridge# bun add @qgis-sdk/bridgeVanilla JS / TypeScript
import { createBridge } from '@qgis-sdk/bridge';import type { Bridge } from './web/bridge.d.ts';
const bridge = await createBridge<Bridge>();const layer = await bridge.get_layer();console.log(layer.name, layer.count);
await bridge.log("hello from JS");
// Legacy callback style still worksbridge.get_layer((result) => { console.log(result);});createBridge auto-injects qwebchannel.js:
- Checks if
QWebChannelalready global - Tries
qrc:///qtwebchannel/qwebchannel.js(Qt built-in, works in QGIS) - Tries
./qwebchannel.js,./web/qwebchannel.js,/qwebchannel.js - Falls back to CDN
jsdelivr.net/unpkg
Options:
import { createBridge } from '@qgis-sdk/bridge';
const bridge = await createBridge<Bridge>({ objectName: 'bridge', // default, must match Python channel.registerObject qwebchannelSources: ['qrc:///qtwebchannel/qwebchannel.js', './qwebchannel.js'], timeout: 10000, // ms});
// Or shorthandconst bridge = await createBridge<Bridge>('bridge', { timeout: 5000 });Python → JS Messages
Python:
import jsondata = {"message": "hello from Python", "center": [51.5, -0.09], "zoom": 13}web_view.page().runJavaScript(f"window.qgisBridge.onMessage({json.dumps(data)})")# orweb_view.page().runJavaScript(f"window.dispatchEvent(new CustomEvent('qgis-message', {{detail: {json.dumps(data)}}}}))")JS:
import { onQgisMessage } from '@qgis-sdk/bridge';
// Via helperconst unsubscribe = onQgisMessage((data) => { console.log('from Python', data);});
// Via CustomEvent (any framework)window.addEventListener('qgis-message', (e: CustomEvent) => { console.log(e.detail);});
// Via global (legacy compatibility)window.updateFromPython = (data) => { console.log(data);};window.qgisBridge.onMessage = (data) => { /* ... */ };Framework Adapters
React
import { useQgisBridge } from '@qgis-sdk/bridge/react';import type { Bridge } from './web/bridge.d.ts';import { useEffect, useState } from 'react';
function App() { const { bridge, ready, error } = useQgisBridge<Bridge>();
const [layer, setLayer] = useState<any>(null);
useEffect(() => { if (ready && bridge) { bridge.get_layer().then(setLayer); } }, [ready]);
if (!ready) return <div>Connecting to QGIS... (auto-injects qrc:///qtwebchannel/qwebchannel.js)</div>; if (error) return <div>Error: {error.message}</div>;
return ( <div> <h2>Layer: {layer?.name} ({layer?.count})</h2> <button onClick={() => bridge?.log("hello from React")}>Send to Python</button> </div> );}For bundlers where React is imported:
import { useState, useEffect } from 'react';import { createReactHook } from '@qgis-sdk/bridge/react';import type { Bridge } from './bridge';
const useQgisBridge = createReactHook<Bridge>({ useState, useEffect });Vite template (scaffolded via qgis-plugin new my_plugin --web --framework react):
{ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "@qgis-sdk/bridge": "^0.1.0" }}Vue
<script setup lang="ts">import { ref, watch } from 'vue';import { useQgisBridge } from '@qgis-sdk/bridge/vue';import type { Bridge } from './web/bridge.d.ts';
const { bridge, ready, error } = useQgisBridge<Bridge>();const layer = ref<any>(null);
watch(ready, async (isReady) => { if (isReady && bridge.value) { layer.value = await bridge.value.get_layer(); }});</script>
<template> <div v-if="!ready">Connecting to QGIS...</div> <div v-else-if="error">Error: {{ error.message }}</div> <div v-else> <p>Layer: {{ layer?.name }} ({{ layer?.count }})</p> <button @click="bridge?.value?.log('hello from Vue')">Send to Python</button> </div></template>For bundlers:
import { ref, onMounted } from 'vue';import { createVueComposable } from '@qgis-sdk/bridge/vue';
const useQgisBridge = createVueComposable<Bridge>({ ref, onMounted });Web Components
HTML:
<qgis-bridge object-name="bridge" id="qgisBridge"></qgis-bridge><script type="module"> import '@qgis-sdk/bridge/webcomponents'; const el = document.getElementById('qgisBridge'); el.addEventListener('qgis-bridge-ready', async (e) => { const layer = await e.detail.bridge.get_layer(); console.log(layer); }); el.addEventListener('qgis-message', (e) => { console.log('from Python', e.detail); });</script>JS Mixin:
import { withQgisBridge } from '@qgis-sdk/bridge/webcomponents';
class MyMap extends withQgisBridge(HTMLElement) { onBridgeReady(bridge) { bridge.get_layer().then(layer => this.render(layer)); } onQgisMessage(data) { console.log('from Python', data); }}customElements.define('my-map', MyMap);Scaffold Integration
qgis-plugin new with --web now always generates web/bridge.d.ts:
qgis-plugin new my_plugin --web --framework react# Creates:# my_plugin/web/map.html (Leaflet + @qgis-sdk/bridge comment)# my_plugin/web/react.html (React CDN + useQgisBridge)# my_plugin/web/bridge.d.ts (typed interface)# my_plugin/web/bridge.ts (usage examples)# my_plugin/frontend/package.json (with @qgis-sdk/bridge)# my_plugin/frontend/src/App.tsx (typed hook)
qgis-plugin new my_plugin --web --framework vueqgis-plugin new my_plugin --web --framework webcomponentsqgis-plugin new my_plugin --web # all 4 examplesProduction build:
cd my_plugin/frontendnpm installnpm run build # -> my_plugin/web/dist/Then in Python:
from qgis_sdk.ui import WebDialogdlg = WebDialog.from_file("web/dist/index.html", title="My App")Offline / QGIS Bundle
Qt provides qwebchannel.js at qrc:///qtwebchannel/qwebchannel.js — no need to bundle. For offline dev outside QGIS:
find /usr -name qwebchannel.js 2>/dev/nullcp /usr/share/qt5/.../qwebchannel.js ./web/@qgis-sdk/bridge auto-tries local ./qwebchannel.js before CDN, so offline works.
API Reference
createBridge<T>(objectName?, options?)
objectName: default'bridge', must matchchannel.registerObjectin Pythonoptions.qwebchannelSources: custom sources arrayoptions.timeout: ms before rejecting (default 10000)- Returns
Promise<T>with promisified methods
loadQWebChannel(sources?)
Loads qwebchannel.js from first available source. Returns Promise<void>.
onQgisMessage(handler)
Register handler for Python → JS messages. Returns unsubscribe function.
QWEBCHANNEL_SOURCES
Default sources tried: qrc:///qtwebchannel/qwebchannel.js, ./qwebchannel.js, ./web/qwebchannel.js, /qwebchannel.js, CDN.
Next Steps
- Web Frameworks Guide — React/Vue/WebComponents in QWebEngine
- Testing Fixtures Guide — test bridge without QGIS
- Plugin Development — full plugin workflow