Implementing Custom Event Filters for QGIS Map Canvas Interactions

To intercept raw input before QGIS processes it, subclass QObject, override the eventFilter() method, and attach the filter to the active QgsMapCanvas via…

To intercept raw input before QGIS processes it, subclass QObject, override the eventFilter() method, and attach the filter to the active QgsMapCanvas via installEventFilter(). This pattern gives you first access to low-level Qt events (mouse, keyboard, wheel, focus), allowing you to consume, modify, or log interactions without overriding built-in map tools or modifying QGIS core code.

How Qt Event Filtering Works in QGIS

QGIS inherits its input model from Qt’s event propagation system. When a user clicks, scrolls, or presses a key, Qt generates an event object (QMouseEvent, QWheelEvent, etc.) and dispatches it through the widget hierarchy. By default, QgsMapCanvas consumes these events to drive panning, zooming, feature selection, and rubber-banding.

Event filters operate at a higher priority than standard signal/slot connections. While Signal and Slot Event Handling in QGIS is ideal for reacting to completed state changes (e.g., canvasClicked, scaleChanged, or selectionChanged), event filters intercept the raw trigger before the canvas translates it into map actions. This distinction is critical when you need to:

  • Block specific mouse buttons or keyboard shortcuts without disabling the entire tool
  • Clamp zoom ranges or restrict panning boundaries
  • Capture exact screen-to-map coordinate transitions during drag operations
  • Override default context menus, snapping behavior, or selection modifiers

The filter receives two arguments: watched (the emitting object) and event (the event instance). Returning True consumes the event, halting further propagation. Returning False passes it down the chain to QgsMapCanvas and other registered handlers.

flowchart TD
    EV["Qt input event: mouse, key, wheel"] --> EF["eventFilter(obj, event)"]
    EF --> D{"Handle and consume?"}
    D -->|"return True"| STOP["Event consumed — propagation halts"]
    D -->|"return False"| PASS["Passed to QgsMapCanvas default handling"]

Production-Ready Implementation

Below is a complete, type-hinted pattern for attaching a custom filter to the QGIS map canvas. It logs left-click coordinates in map units, blocks right-click context menus, restricts wheel zooming when locked, and safely detaches during plugin cleanup.

python
from qgis.PyQt.QtCore import QObject, QEvent, Qt
from qgis.PyQt.QtGui import QMouseEvent, QWheelEvent
from qgis.gui import QgsMapCanvas
from qgis.utils import iface

class MapCanvasEventFilter(QObject):
    """Custom event filter for intercepting and modifying QGIS map canvas input."""

    def __init__(self, canvas: QgsMapCanvas):
        super().__init__()
        self.canvas = canvas
        self.zoom_locked = False
        self._installed = False

    def install(self) -> None:
        """Attach the filter to the canvas event loop."""
        if not self._installed:
            self.canvas.installEventFilter(self)
            self._installed = True

    def remove(self) -> None:
        """Safely detach the filter to prevent memory leaks."""
        if self._installed:
            self.canvas.removeEventFilter(self)
            self._installed = False

    def eventFilter(self, obj: QObject, event: QEvent) -> bool:
        # Guard against events from other widgets
        if obj is not self.canvas:
            return super().eventFilter(obj, event)

        # Handle mouse button presses
        if event.type() == QEvent.MouseButtonPress:
            mouse_event: QMouseEvent = event
            if mouse_event.button() == Qt.LeftButton:
                # Transform screen coordinates to active map CRS
                transform = self.canvas.getCoordinateTransform()
                map_coords = transform.toMapCoordinates(mouse_event.pos())
                print(f"Left click at map coordinates: {map_coords.x():.4f}, {map_coords.y():.4f}")
            elif mouse_event.button() == Qt.RightButton:
                # Consume right-click to suppress default context menu
                return True

        # Handle wheel zooming
        elif event.type() == QEvent.Wheel:
            if self.zoom_locked:
                return True  # Block zoom when locked

        # Always pass unhandled events to the default chain
        return super().eventFilter(obj, event)

# Usage in a QGIS plugin or console script:
# canvas = iface.mapCanvas()
# canvas_filter = MapCanvasEventFilter(canvas)
# canvas_filter.install()
# ...
# canvas_filter.remove()  # Call during plugin unload

Core Use Cases & Patterns

Event filters excel in scenarios where higher-level APIs fall short. Common implementations include:

  • Custom Navigation Constraints: Clamp zoom levels by intercepting QEvent.Wheel or QEvent.KeyRelease (Ctrl+/Ctrl-) and comparing the resulting scale against project-defined thresholds.
  • Tool-Specific Overrides: Temporarily suppress snapping, grid alignment, or rubber-banding when a custom digitizing tool is active, then restore default behavior on tool deactivation.
  • Input Telemetry & Debugging: Log modifier key states (Qt.ShiftModifier, Qt.AltModifier) alongside mouse events to diagnose unexpected tool behavior in complex workflows.
  • Accessibility & Kiosk Modes: Disable right-click menus, keyboard shortcuts, and multi-touch gestures to lock the interface for public-facing or training deployments.

Performance & Plugin Lifecycle Best Practices

Event filters execute synchronously on the main GUI thread. Poorly optimized logic will introduce input lag or freeze the canvas during heavy interactions. Follow these guidelines to maintain responsiveness:

  1. Keep eventFilter() Fast: Avoid heavy computations, database queries, or synchronous network calls inside the filter. Offload processing to a background thread or defer it using QTimer.singleShot(0, callback).
  2. Always Call super().eventFilter(): Returning False without delegating to the parent class can break Qt’s internal event routing, causing missing repaints or unresponsive widgets.
  3. Detach on Unload: QGIS plugins must clean up filters during unload() or close() to prevent dangling references and segmentation faults. The remove() method above handles this safely.
  4. Coordinate System Awareness: Screen coordinates (event.pos()) differ from map coordinates. Always use QgsMapCanvas.getCoordinateTransform().toMapCoordinates() to ensure CRS accuracy, especially in projects with on-the-fly reprojection enabled.
  5. Avoid Over-Filtering: Register filters only on the specific canvas instance you need. Applying filters globally to iface.mainWindow() or parent widgets will intercept unrelated UI events and degrade performance.

For deeper architectural context on how QGIS routes input through its core components, review the PyQGIS Core Architecture & Data Handling documentation. Understanding the event loop’s position relative to the rendering pipeline and provider layers will help you design filters that complement, rather than conflict with, QGIS’s native toolchain.

When to Choose Filters Over Signals

Use event filters when you need pre-processing control or input suppression. Use signals when you need post-processing reactions or state synchronization. Mixing both patterns is common: a filter can modify raw input, then emit a custom signal that downstream components consume via standard slot connections. This hybrid approach maintains clean separation of concerns while preserving full control over the interaction lifecycle.