Add a Custom Dock Widget in PyQGIS

A dock widget is the right container for any plugin tool that should stay visible while the user works — a layer inspector, a live query panel, a styling controller. Unlike a modal dialog that interrupts the workflow, a QDockWidget snaps into the QGIS window alongside the Layers and Browser panels, can be moved, floated, or hidden, and persists across the session. This page builds one two ways — by subclassing QDockWidget directly and by loading a Qt Designer .ui with uic.loadUiType — embeds GIS-aware widgets inside it, docks it with addDockWidget, remembers its visibility between sessions, and removes it cleanly in unload().

This task belongs to Qt Designer for GIS Interfaces, which covers the broader .ui workflow. A dock widget is typically opened by a toolbar control, so it pairs naturally with Add a Toolbar Button to a QGIS Plugin.

Prerequisites

  • QGIS 3.34 LTR (Python 3.12) with the Python console available
  • A plugin skeleton with a main class that receives iface and implements initGui() / unload() — generate one with Create a QGIS Plugin with Plugin Builder 3 if needed
  • Basic Qt layout knowledge (QVBoxLayout, signal/slot connections)
  • For the .ui approach, Qt Designer installed and widget promotion understood

Approach A: Subclass QDockWidget Directly

For a panel with a handful of controls, writing the widget in Python avoids a .ui round-trip. Build a QWidget content area, lay it out, and set it as the dock's content. Embedding QgsMapLayerComboBox and QgsFieldComboBox gives users the same layer and field pickers QGIS uses natively.

from qgis.PyQt.QtCore import Qt, pyqtSignal
from qgis.PyQt.QtWidgets import (
    QDockWidget, QWidget, QVBoxLayout, QLabel, QPushButton,
)
from qgis.gui import QgsMapLayerComboBox, QgsFieldComboBox
from qgis.core import QgsMapLayerProxyModel


class LayerInspectorDock(QDockWidget):
    """A dockable panel that reports feature counts for a chosen field."""

    closingPlugin = pyqtSignal()

    def __init__(self, iface, parent=None):
        super().__init__("Layer Inspector", parent)
        self.iface = iface
        self.setObjectName("LayerInspectorDock")  # needed to persist state

        content = QWidget(self)
        layout = QVBoxLayout(content)

        layout.addWidget(QLabel("Layer"))
        self.layer_combo = QgsMapLayerComboBox()
        self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
        layout.addWidget(self.layer_combo)

        layout.addWidget(QLabel("Field"))
        self.field_combo = QgsFieldComboBox()
        layout.addWidget(self.field_combo)

        self.count_button = QPushButton("Count distinct values")
        layout.addWidget(self.count_button)
        self.result = QLabel("--")
        layout.addWidget(self.result)
        layout.addStretch()

        self.setWidget(content)

        # Keep the field combo in sync with the selected layer
        self.layer_combo.layerChanged.connect(self.field_combo.setLayer)
        self.field_combo.setLayer(self.layer_combo.currentLayer())
        self.count_button.clicked.connect(self._count)

    def _count(self):
        layer = self.layer_combo.currentLayer()
        field = self.field_combo.currentField()
        if layer is None or not field:
            self.result.setText("Pick a layer and field")
            return
        idx = layer.fields().indexOf(field)
        distinct = layer.uniqueValues(idx)
        self.result.setText(f"{len(distinct)} distinct values")

    def closeEvent(self, event):
        self.closingPlugin.emit()
        event.accept()

Breakdown: The dock title ("Layer Inspector") appears on the panel's title bar. setObjectName is mandatory for QGIS to save and restore the dock's position. QgsMapLayerComboBox.setFilters(QgsMapLayerProxyModel.VectorLayer) limits the picker to vector layers; its layerChanged signal feeds QgsFieldComboBox.setLayer, so the field list always reflects the chosen layer's schema. layer.uniqueValues(index) returns the set of distinct values for the field. The custom closingPlugin signal lets the host plugin react when the user clicks the dock's close button, which is how we keep the toolbar toggle in sync below.

Approach B: Load a .ui File with uic.loadUiType

When the panel grows or a designer maintains the layout, build it in Qt Designer from the Dock Widget template and load it at runtime. Promote the layer and field pickers in Designer (QgsMapLayerComboBox → header qgsmaplayercombobox.h; QgsFieldComboBoxqgsfieldcombobox.h) so the same widgets appear without manual instantiation.

import os
from qgis.PyQt import uic
from qgis.PyQt.QtCore import pyqtSignal
from qgis.core import QgsMapLayerProxyModel

FORM_CLASS, _ = uic.loadUiType(
    os.path.join(os.path.dirname(__file__), "ui", "inspector_dock_base.ui")
)


class LayerInspectorDock(FORM_CLASS):
    """Dock widget whose layout comes from a Qt Designer .ui file."""

    closingPlugin = pyqtSignal()

    def __init__(self, iface, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.iface = iface

        # Widgets below are defined and promoted in the .ui file
        self.mLayerCombo.setFilters(QgsMapLayerProxyModel.VectorLayer)
        self.mLayerCombo.layerChanged.connect(self.mFieldCombo.setLayer)
        self.mFieldCombo.setLayer(self.mLayerCombo.currentLayer())

    def closeEvent(self, event):
        self.closingPlugin.emit()
        event.accept()

Breakdown: uic.loadUiType() parses the .ui XML once at import time and returns a base class — for a Dock Widget template that base is already a QDockWidget, so the subclass is a dock. setupUi(self) instantiates every widget defined in Designer and attaches it as an attribute named after its objectName (mLayerCombo, mFieldCombo). Runtime loading sidesteps pyuic5 version mismatches, which is why it is preferred over static compilation; the trade-off is that the .ui file must ship inside the plugin. Notice there is no setObjectName call here — set it on the dock in Designer's property editor instead.

Docking, Toggling, and Removing on Unload

The main plugin class owns the dock's lifecycle. Add it with addDockWidget, expose a checkable toolbar action to show or hide it, and remove it in unload(). The dock's visibilityChanged signal keeps the toolbar toggle accurate when the user closes the panel manually.

from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction


class InspectorPlugin:
    def __init__(self, iface):
        self.iface = iface
        self.dock = None
        self.action = None

    def initGui(self):
        self.action = QAction(QIcon(), "Layer Inspector", self.iface.mainWindow())
        self.action.setCheckable(True)
        self.action.toggled.connect(self.toggle_dock)
        self.iface.addToolBarIcon(self.action)

    def toggle_dock(self, checked):
        if checked and self.dock is None:
            self.dock = LayerInspectorDock(self.iface, self.iface.mainWindow())
            self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dock)
            self.dock.closingPlugin.connect(lambda: self.action.setChecked(False))
            self.dock.visibilityChanged.connect(self.action.setChecked)
        elif self.dock is not None:
            self.dock.setVisible(checked)

    def unload(self):
        if self.dock is not None:
            self.iface.removeDockWidget(self.dock)
            self.dock.deleteLater()
            self.dock = None
        if self.action is not None:
            self.iface.removeToolBarIcon(self.action)
            self.action = None

Breakdown: addDockWidget(Qt.RightDockWidgetArea, dock) snaps the panel into the right-hand dock area, beside Layers and Browser; users can drag it anywhere afterward. The dock is created lazily on first activation so a disabled feature costs nothing. Connecting visibilityChanged to action.setChecked keeps the toolbar button's pressed state in lockstep with the panel — including when the user clicks the panel's close X, which also fires our closingPlugin signal. In unload(), removeDockWidget detaches the panel from the main window and deleteLater() schedules its Qt-side destruction; skipping either leaves a stranded panel after the plugin is disabled.

Persisting Visibility Across Sessions

QGIS restores a dock's position automatically once setObjectName is set, but not whether your plugin should recreate it on the next launch. Use QSettings to remember the user's preference and reopen the dock during initGui() if it was open last time.

from qgis.PyQt.QtCore import QSettings

SETTINGS_KEY = "InspectorPlugin/dockVisible"


def initGui(self):
    self.action = QAction(QIcon(), "Layer Inspector", self.iface.mainWindow())
    self.action.setCheckable(True)
    self.action.toggled.connect(self.toggle_dock)
    self.iface.addToolBarIcon(self.action)

    # Restore last session's choice
    if QSettings().value(SETTINGS_KEY, False, type=bool):
        self.action.setChecked(True)  # triggers toggle_dock -> creates the dock


def toggle_dock(self, checked):
    QSettings().setValue(SETTINGS_KEY, checked)
    # ... creation / visibility logic from the previous section ...

Breakdown: QSettings writes to the platform-native store (registry on Windows, plist on macOS, .conf on Linux) under the QGIS organization, so the value survives restarts. Reading it with type=bool guarantees a real boolean rather than the string "false", which is truthy and a common bug. Setting the checkable action to checked during initGui() re-runs toggle_dock, recreating the panel exactly as the user left it.

QGIS Version Compatibility

The dock widget APIs are stable across the 3.x line; the only moving parts are enum spellings, which affect both approaches identically.

APIQGIS 3.28 LTR (Py 3.9)QGIS 3.34 LTR (Py 3.12)QGIS 3.40 / 3.44 (Py 3.12)
iface.addDockWidget / removeDockWidgetstablestablestable
QgsMapLayerComboBox / QgsFieldComboBoxavailableavailableavailable
Filter enumQgsMapLayerProxyModel.VectorLayersameQgis.LayerFilter.VectorLayer also available
Dock area enumQt.RightDockWidgetAreasamesame

On QGIS 3.34 the classic QgsMapLayerProxyModel.VectorLayer filter remains the documented form. Newer releases add Qgis.LayerFilter aliases, but the old enum still works, so the code above is portable.

Troubleshooting

AttributeError: 'NoneType' object has no attribute 'setLayer'. The promoted widget name in your code does not match the objectName in the .ui file. Open the .ui in Qt Designer and confirm mLayerCombo / mFieldCombo exactly match your attribute names.

Dock position is not remembered between sessions. You did not set an object name. Call setObjectName (subclass approach) or set it in Designer (.ui approach) — QGIS keys saved layout state on it.

The toolbar toggle and the panel get out of sync. Connect the dock's visibilityChanged signal to action.setChecked, and emit a closingPlugin signal from closeEvent so the close button also updates the toggle.

Field combo is empty. QgsFieldComboBox needs a layer. Call setLayer once after creation and connect it to the layer combo's layerChanged signal so it updates on every change.

Panel remains after disabling the plugin. unload() must call removeDockWidget and deleteLater() on the dock; removing only the toolbar icon leaves the panel behind.

Conclusion

A custom dock widget turns a one-shot dialog into a persistent, native-feeling part of the QGIS workspace. Whether you subclass QDockWidget in Python or load a Qt Designer .ui with uic.loadUiType, the essentials are the same: embed GIS-aware widgets like QgsMapLayerComboBox and QgsFieldComboBox, dock it with addDockWidget, keep a checkable toolbar action in sync via visibilityChanged, persist the user's choice with QSettings, and tear it all down in unload() with removeDockWidget and deleteLater(). Get that lifecycle right and the panel behaves exactly like a built-in QGIS dock.

Frequently Asked Questions

Should I subclass QDockWidget or load a .ui file? Subclass for small, code-driven panels; use a .ui when the layout is complex or maintained by a designer. Both dock identically — the choice is about how you author the layout.

Why does the panel not reappear after a QGIS restart? QGIS restores dock position once you set an object name, but your plugin must decide whether to recreate the widget. Persist that intent with QSettings and recreate the dock in initGui().

Can I dock the panel on the left or bottom? Yes. Pass a different area to addDockWidgetQt.LeftDockWidgetArea, Qt.BottomDockWidgetArea, or Qt.TopDockWidgetArea. The user can still move it afterward.

How do I open the dock from a toolbar button? Use a checkable QAction whose toggled signal shows or hides the dock, as shown above. The toolbar mechanics are covered in Add a Toolbar Button to a QGIS Plugin.