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.
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.WheelorQEvent.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:
- Keep
eventFilter()Fast: Avoid heavy computations, database queries, or synchronous network calls inside the filter. Offload processing to a background thread or defer it usingQTimer.singleShot(0, callback). - Always Call
super().eventFilter(): ReturningFalsewithout delegating to the parent class can break Qt’s internal event routing, causing missing repaints or unresponsive widgets. - Detach on Unload: QGIS plugins must clean up filters during
unload()orclose()to prevent dangling references and segmentation faults. Theremove()method above handles this safely. - Coordinate System Awareness: Screen coordinates (
event.pos()) differ from map coordinates. Always useQgsMapCanvas.getCoordinateTransform().toMapCoordinates()to ensure CRS accuracy, especially in projects with on-the-fly reprojection enabled. - 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.