Drawing Temporary Geometries with QgsRubberBand

Draw transient highlights on the QGIS map canvas with QgsRubberBand: add geometry, set colour and width, update live on mouse move, and reset cleanly without…

TL;DR: Construct QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry), style it with setColor()/setWidth()/setFillColor(), feed it geometry with setToGeometry(geom, layer) or addPoint(), and call reset() (plus scene removal on deactivate) to clear it. The band lives directly on the canvas, not inside any layer.

This page is part of the Custom Map Canvas Overlays and Rendering in PyQGIS guide, which sits inside the broader Plugin Development & UI Integration in QGIS reference. Where that guide covers the full custom-rendering pipeline — subclassing QgsMapCanvasItem, authoring paint(), and managing bounding boxes — this page is narrowly about QgsRubberBand: the ready-made canvas item QGIS ships for transient highlights, so you rarely need to write a paint() method at all for feature outlines and interactive previews.

Complete Runnable Example

The class below highlights a selected feature and, when used as an active map tool, draws a live polygon that grows as the user clicks and previews the next edge under the cursor. It is a drop-in QgsMapTool — construct it with iface.mapCanvas(), activate it via iface.mapCanvas().setMapTool(tool), and it cleans up after itself on deactivate().

python
"""
rubberband_tool.py

A QgsMapTool that draws a live polygon with QgsRubberBand and can also
highlight an existing feature. Transient geometry only — nothing is written
to any layer. Compatible with QGIS 3.28+ (LTR).
"""
from __future__ import annotations

from typing import Optional

from qgis.core import (
    QgsFeature,
    QgsGeometry,
    QgsPointXY,
    QgsVectorLayer,
    QgsWkbTypes,
)
from qgis.gui import QgsMapCanvas, QgsMapTool, QgsRubberBand
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QColor


class PolygonRubberBandTool(QgsMapTool):
    """Interactive polygon-drawing tool backed by a QgsRubberBand.

    Left-click appends a vertex; mouse movement previews the next edge;
    right-click finishes and prints the geometry; Esc/deactivate resets.
    """

    def __init__(self, canvas: QgsMapCanvas) -> None:
        super().__init__(canvas)
        self._canvas = canvas

        # The committed vertices (map coordinates) drawn as a solid band.
        self._band = QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry)
        self._band.setColor(QColor(220, 30, 30))
        self._band.setFillColor(QColor(220, 30, 30, 40))  # 40/255 alpha fill
        self._band.setWidth(2)

        # A separate, dashed band that previews the edge to the cursor.
        self._preview = QgsRubberBand(canvas, QgsWkbTypes.LineGeometry)
        self._preview.setColor(QColor(220, 30, 30, 160))
        self._preview.setLineStyle(Qt.DashLine)
        self._preview.setWidth(1)

        self._vertices: list[QgsPointXY] = []

    def canvasPressEvent(self, event) -> None:
        """Append a vertex on left-click; finish the ring on right-click."""
        map_pt: QgsPointXY = self.toMapCoordinates(event.pos())
        if event.button() == Qt.LeftButton:
            self._vertices.append(map_pt)
            # addPoint() is cheaper than rebuilding: it appends one vertex.
            self._band.addPoint(map_pt, True)  # doUpdate=True -> repaint now
        elif event.button() == Qt.RightButton:
            self._finish()

    def canvasMoveEvent(self, event) -> None:
        """Preview the edge from the last committed vertex to the cursor."""
        if not self._vertices:
            return
        cursor: QgsPointXY = self.toMapCoordinates(event.pos())
        self._preview.reset(QgsWkbTypes.LineGeometry)
        self._preview.addPoint(self._vertices[-1], False)
        self._preview.addPoint(cursor, True)

    def keyPressEvent(self, event) -> None:
        """Escape clears the in-progress geometry without finishing it."""
        if event.key() == Qt.Key_Escape:
            self._clear()

    def highlight_feature(
        self, feature: QgsFeature, layer: QgsVectorLayer
    ) -> None:
        """Snap the main band to an existing feature's geometry.

        Args:
            feature: The feature whose geometry should be highlighted.
            layer:   The owning layer — its CRS drives on-the-fly transform.
        """
        self._clear()
        self._band.setToGeometry(feature.geometry(), layer)

    def _finish(self) -> None:
        """Emit the completed geometry, then reset for the next capture."""
        if len(self._vertices) >= 3:
            geom: QgsGeometry = self._band.asGeometry()
            print(f"Captured polygon: {geom.asWkt(precision=2)}")
        self._clear()

    def _clear(self) -> None:
        """Drop all vertices from both bands (keeps them on the canvas)."""
        self._vertices.clear()
        self._band.reset(QgsWkbTypes.PolygonGeometry)
        self._preview.reset(QgsWkbTypes.LineGeometry)

    def deactivate(self) -> None:
        """Reset geometry and detach both bands from the canvas scene.

        Without scene removal the band objects survive as orphaned graphics
        items and reappear when the tool is next created.
        """
        self._clear()
        for band in (self._band, self._preview):
            self._canvas.scene().removeItem(band)
        super().deactivate()

Rubber Band Lifecycle

A rubber band moves through three states: you create it once (binding it to the canvas and a geometry type), you repeatedly feed it geometry as the interaction unfolds, and you tear it down when the tool deactivates. The diagram traces that path.

QgsRubberBand lifecycle Three-stage lifecycle: construct the band bound to the canvas and a geometry type, then repeatedly set geometry with setToGeometry or addPoint during interaction, then reset and remove the band from the scene on deactivate. Create QgsRubberBand(canvas, PolygonGeometry) Update (each event) setToGeometry(geom, layer) or addPoint(pt) on move -> canvas repaint repeat Tear down reset() + scene().removeItem()

Architecture Breakdown

The QgsRubberBand contract

QgsRubberBand is a QgsMapCanvasItem subclass that QGIS provides specifically for drawing transient geometry. You never subclass it or write a paint() method; you configure and feed it. The constructor takes the canvas and a geometry type — pass one of QgsWkbTypes.PointGeometry, QgsWkbTypes.LineGeometry, or QgsWkbTypes.PolygonGeometry. The type is not cosmetic: it decides whether the band renders as vertex markers, an open polyline, or a closed filled ring, and it must match whatever geometry you later hand it.

There are two ways to give the band its shape. setToGeometry(geometry, layer) replaces the band’s contents with an existing QgsGeometry in one call — pass the owning QgsVectorLayer so the band can apply the on-the-fly transform from the layer CRS to the canvas CRS. This is the right choice for highlighting a feature you already have. addPoint(point, doUpdate=True) appends a single vertex in canvas map coordinates and is what you want during interactive capture, because it avoids rebuilding the whole geometry on every mouse click. Set doUpdate=False when adding several points in a batch and trigger a single repaint on the last one.

Styling is a handful of setters: setColor() sets both the stroke and, by default, a derived fill; call setFillColor() separately when you want a distinct, semi-transparent fill (an alpha around 40/255 keeps the basemap legible underneath). setWidth() takes an integer pen width in pixels, setLineStyle() accepts a Qt.PenStyle such as Qt.DashLine, and setIconType()/setIconSize() control vertex markers for point bands. Read back the current shape at any time with asGeometry(), which returns a QgsGeometry you can persist to a real layer once the user commits.

CRS: the band draws in canvas map coordinates

A rubber band stores its vertices in the canvas CRS and converts to screen pixels at paint time, exactly like any other canvas item. When you use addPoint(), always feed it map coordinates — inside a QgsMapTool that means calling self.toMapCoordinates(event.pos()), never raw pixel positions. When you use setToGeometry(geom, layer), the band uses the supplied layer’s CRS to reproject the geometry into the canvas CRS for you; omit or mismatch that layer and a feature stored in, say, UTM will land far off a WGS 84 basemap. If a geometry originates from a source whose CRS differs from both the layer and the canvas, transform it first — see Coordinate Transformations and CRS Handling in PyQGIS for building and validating a QgsCoordinateTransform, and Troubleshooting CRS Mismatches in PyQGIS Scripts when a highlight lands in the wrong place. Because the band works in map space, it automatically follows pan and zoom with no extra code.

Cleanup: avoid orphaned canvas items

reset() clears a band’s vertices but leaves the band object attached to the canvas scene, ready to be reused — that is what you want between successive captures. It optionally takes a geometry type (reset(QgsWkbTypes.LineGeometry)) so you can switch what the band draws without constructing a new one. What reset() does not do is detach the band from the scene, so a band that only ever gets reset() still consumes a graphics item for the life of the canvas. When a tool is finished with a band for good — typically in deactivate() or the plugin’s unload() — remove it explicitly with canvas.scene().removeItem(band). Skipping this leaves invisible, empty bands accumulating on the canvas across tool activations and plugin reloads, the same orphaned-item leak that dogs any manual canvas overlay. This mirrors the broader ownership discipline covered in Plugin Lifecycle and Resource Management: whatever you attach to the canvas, you are responsible for detaching.

Using It from a QgsMapTool

Rubber bands are almost always driven by an active map tool, because the tool is what receives the mouse events that reshape the band. Register the tool on the canvas and QGIS routes press, move, and key events to it:

python
"""Register the polygon rubber-band tool as the canvas's active tool."""
from qgis.utils import iface

from rubberband_tool import PolygonRubberBandTool

canvas = iface.mapCanvas()
tool = PolygonRubberBandTool(canvas)
canvas.setMapTool(tool)  # QGIS calls tool.activate() and starts routing events

# Later, restoring the default pan tool triggers tool.deactivate(),
# which resets the bands and removes them from the scene:
# canvas.unsetMapTool(tool)

Because the tool owns its bands, activation and deactivation form a clean pair: the constructor creates the bands, deactivate() tears them down. If you need still finer control over which events reshape the band — for example filtering out clicks over docked widgets — see Implementing Custom Event Filters for QGIS Map Canvas Interactions.

Production Best Practices

  • Match the geometry type to the data. Construct the band with the QgsWkbTypes value that matches what you will draw; a PolygonGeometry band silently closes rings, a LineGeometry band leaves them open.
  • Use setToGeometry() for existing features, addPoint() for capture. Rebuilding geometry on every click with setToGeometry() is wasteful; append vertices instead and batch repaints with doUpdate=False.
  • Always pass the layer to setToGeometry() so the band reprojects from the layer CRS to the canvas CRS; otherwise highlights drift off the basemap.
  • Give fills an alpha. A setFillColor() value around 40/255 alpha keeps underlying features and labels readable through a polygon highlight.
  • reset() to reuse, scene().removeItem() to dispose. Reset between captures; remove from the scene on deactivate()/unload() to avoid orphaned canvas items.
  • Keep separate bands for committed and preview geometry. A solid band for confirmed vertices plus a dashed band for the edge-to-cursor preview reads far more clearly than mutating a single band.
  • Feed map coordinates, not pixels. Convert cursor positions with toMapCoordinates() before calling addPoint().