Add a Custom Layer Properties Page in PyQGIS

Plugins often need per-layer settings: which field holds the asset id, how often the data should be refreshed, which validation rules apply, whether the layer takes part in a sync. A separate plugin dialog for those settings is easy to build and easy for users to miss. The natural home is the Layer Properties dialog itself — next to Symbology, Labels and Fields — where users already look for everything about a layer. QGIS lets plugins add pages there, and optionally to the Layer Styling panel, through a factory class.

This recipe belongs to Extending QGIS with Custom Classes. It builds a properties page with a QgsMapLayerConfigWidget, a factory that decides where it appears and for which layers, stores settings as layer custom properties so they travel with the project, and registers the factory from a plugin.

A plugin page among the built-in pagesThe Layer Properties dialog lists pages on the left: Information, Source, Symbology, Labels, Fields and others. A plugin's factory adds a page, here Asset Sync, with its own icon. The page's widget edits settings stored as custom properties on the layer, and its apply method writes them when the user presses OK or Apply.Your page next to Symbology and LabelsInformationSourceSymbologyLabelsFieldsAsset SyncAsset Sync settingsid field:asset_idsync every:24 hstored as layer custom properties → saved in the project

Prerequisites

  • QGIS 3.34 LTR or newer, or the QGIS 4 series, with the code in a plugin (the page must be registered each session).
  • Basic Qt widget knowledge. Layouts can be built in code as below or in Qt Designer, as in loading a .ui file at runtime.

Build the page widget

The page is a QgsMapLayerConfigWidget. It receives the layer when created, shows the current settings, and writes them back in apply().

from qgis.gui import QgsMapLayerConfigWidget, QgsFieldComboBox
from qgis.PyQt.QtWidgets import QFormLayout, QSpinBox, QCheckBox

PREFIX = "assetsync/"

class AssetSyncPage(QgsMapLayerConfigWidget):
    def __init__(self, layer, canvas, parent=None):
        super().__init__(layer, canvas, parent)
        self.layer = layer
        self.enabled = QCheckBox("Include this layer in asset sync")
        self.id_field = QgsFieldComboBox()
        self.id_field.setLayer(layer)
        self.interval = QSpinBox()
        self.interval.setRange(1, 720)
        self.interval.setSuffix(" h")

        form = QFormLayout(self)
        form.addRow(self.enabled)
        form.addRow("Asset id field", self.id_field)
        form.addRow("Sync every", self.interval)

        self.enabled.setChecked(layer.customProperty(PREFIX + "enabled", False) in (True, "true"))
        self.id_field.setField(layer.customProperty(PREFIX + "id_field", ""))
        self.interval.setValue(int(layer.customProperty(PREFIX + "interval_h", 24)))

        for w in (self.enabled.toggled, self.id_field.fieldChanged, self.interval.valueChanged):
            w.connect(lambda *_: self.widgetChanged.emit())

    def apply(self):
        self.layer.setCustomProperty(PREFIX + "enabled", self.enabled.isChecked())
        self.layer.setCustomProperty(PREFIX + "id_field", self.id_field.currentField())
        self.layer.setCustomProperty(PREFIX + "interval_h", self.interval.value())

Breakdown: Custom properties are key–value pairs stored on the layer and written into the project file, so settings travel with the project and need no separate storage. A plugin-specific prefix keeps keys from colliding with other plugins. QgsFieldComboBox lists the layer's fields and updates if they change. Emitting widgetChanged when any input changes lets the dialog enable its Apply button and, in the styling panel, apply live. Booleans may come back as strings after a project round trip, hence the tolerant check.

Write the factory

The factory tells QGIS what the page is called, which icon to show, which layers it applies to, and where it may appear.

What the factory decidesThe factory supplies the page title and icon, answers supportsLayer for each layer so the page appears only where it makes sense, declares whether it appears in the Layer Properties dialog, the Layer Styling panel or both, and creates the page widget on demand with createWidget.One factory, many pagestitle · iconshown inpage listsupportsLayer()vector, withan id fieldwhereproperties dialogstyling panelcreateWidget()new pageper layer

from qgis.gui import QgsMapLayerConfigWidgetFactory
from qgis.core import QgsVectorLayer
from qgis.PyQt.QtGui import QIcon

class AssetSyncFactory(QgsMapLayerConfigWidgetFactory):
    def __init__(self, icon_path):
        super().__init__("Asset Sync", QIcon(icon_path))

    def supportsLayer(self, layer):
        return isinstance(layer, QgsVectorLayer) and layer.isSpatial()

    def supportLayerPropertiesDialog(self):
        return True

    def supportsStyleDock(self):
        return False

    def createWidget(self, layer, canvas, dockWidget=True, parent=None):
        return AssetSyncPage(layer, canvas, parent)

Breakdown: The constructor sets the title and icon shown in the page list. supportsLayer keeps the page off raster layers and attribute-only tables, where asset sync makes no sense; the narrower the rule, the less clutter for users. Settings pages belong in the Layer Properties dialog, so supportsStyleDock returns False; pages that change appearance — a custom renderer's options, for instance — are better in the styling panel, where changes preview live. createWidget is called each time the dialog opens, so the page always shows the layer's current settings.

Register the factory from the plugin

Registration and unregistration go through iface. Keep the factory on the plugin so it is not garbage-collected.

import os

class AssetSyncPlugin:
    def __init__(self, iface):
        self.iface = iface
        self.factory = None

    def initGui(self):
        icon = os.path.join(os.path.dirname(__file__), "icons", "sync.svg")
        self.factory = AssetSyncFactory(icon)
        self.iface.registerMapLayerConfigWidgetFactory(self.factory)

    def unload(self):
        if self.factory:
            self.iface.unregisterMapLayerConfigWidgetFactory(self.factory)
            self.factory = None

Breakdown: After registration, opening Layer Properties on any supported layer shows the new page. Unregistering in unload removes it, so reloading the plugin during development does not leave duplicate pages — see reloading a plugin without restarting. Dialogs already open keep their pages until closed.

Offer a live page in the styling panel

Settings that change how a layer looks — the parameters of a custom renderer, a highlight colour for synchronised features — are better edited in the Layer Styling panel, where users see the effect immediately. The same widget class can serve both places; the factory says so, and the page applies changes as they are made.

class SyncHighlightFactory(QgsMapLayerConfigWidgetFactory):
    def __init__(self, icon_path):
        super().__init__("Sync highlight", QIcon(icon_path))

    def supportsLayer(self, layer):
        return isinstance(layer, QgsVectorLayer) and layer.customProperty(PREFIX + "enabled", False) in (True, "true")

    def supportLayerPropertiesDialog(self):
        return True

    def supportsStyleDock(self):
        return True

    def createWidget(self, layer, canvas, dockWidget=True, parent=None):
        page = HighlightPage(layer, canvas, parent)
        page.setDockMode(dockWidget)
        return page

Breakdown: supportsStyleDock returning True adds the page to the styling panel's list for supported layers, and createWidget is told through dockWidget which host it is being created for. Calling setDockMode lets the page adapt — a compact layout in the narrow panel, a fuller one in the dialog. In the panel, QGIS calls apply() automatically after widgetChanged when live update is on, so the page's apply should be cheap and should end with layer.triggerRepaint() for visual settings. Restricting supportsLayer to layers that have sync enabled means the page appears only where it is relevant, which keeps the panel's page list short.

Read the settings where they are used

The page only stores settings; the rest of the plugin reads them when it acts. A small helper keeps key names and defaults in one place.

from qgis.core import QgsProject

def sync_settings(layer):
    return {
        "enabled": layer.customProperty(PREFIX + "enabled", False) in (True, "true"),
        "id_field": layer.customProperty(PREFIX + "id_field", "") or None,
        "interval_h": int(layer.customProperty(PREFIX + "interval_h", 24)),
    }

for layer in QgsProject.instance().mapLayers().values():
    if isinstance(layer, QgsVectorLayer):
        cfg = sync_settings(layer)
        if cfg["enabled"] and cfg["id_field"]:
            print(f"{layer.name()}: sync on {cfg['id_field']} every {cfg['interval_h']} h")

Breakdown: Reading through one function means a renamed key or a changed default is a single edit, and every caller handles missing or stringly-typed values the same way. Layers without settings fall back to defaults — here "not enabled" — so the plugin never acts on a layer the user has not configured. The same settings could drive a layer tree indicator showing which layers are synchronised.

Validate before applying

A properties page should not accept settings that will fail later: a sync enabled without an id field, an interval outside what the server allows. Validating in the widget and signalling the problem keeps bad settings out.

Validate in the pageAs the user edits, the page checks the combination of settings: sync enabled requires an id field. When invalid, it shows a short message in the page and refuses to write in apply, keeping the previous settings. When valid, apply writes all values at once.Refuse settings that cannot workuser editsenabled, field,intervalvalid?enabled needsan id fieldapply allwrite propertiesshow messagekeep old values

from qgis.PyQt.QtWidgets import QLabel

class ValidatedAssetSyncPage(AssetSyncPage):
    def __init__(self, layer, canvas, parent=None):
        super().__init__(layer, canvas, parent)
        self.message = QLabel()
        self.message.setStyleSheet("color: #b91c1c")
        self.layout().addRow(self.message)
        self.enabled.toggled.connect(self._validate)
        self.id_field.fieldChanged.connect(self._validate)
        self._validate()

    def _validate(self, *_):
        problem = ""
        if self.enabled.isChecked() and not self.id_field.currentField():
            problem = "Choose an asset id field to enable sync."
        self.message.setText(problem)
        return not problem

    def apply(self):
        if self._validate():
            super().apply()

Breakdown: Validating on every change gives immediate feedback in the page itself, which is less disruptive than a message box. apply checks again and writes nothing if the combination is invalid, so the layer keeps its last good settings. For broader validation helpers, see validating plugin dialog input. Return ValidatedAssetSyncPage from the factory to use it.

QGIS version compatibility

QgsMapLayerConfigWidgetFactory, QgsMapLayerConfigWidget and iface.registerMapLayerConfigWidgetFactory exist throughout QGIS 3.x and in QGIS 4. supportLayerPropertiesDialog was added in 3.0 and supportsStyleDock has always been available. Custom property values written as Python booleans may read back as strings in older releases; the tolerant checks above handle both.

Troubleshooting

  • The page does not appear. The factory was not registered before the dialog opened, supportsLayer returned False, or supportLayerPropertiesDialog is False.
  • Duplicate pages after reloading. The factory was not unregistered in unload().
  • Settings are lost when the project reopens. They were stored on the plugin instead of as layer custom properties, or the project was not saved.
  • Apply does nothing. widgetChanged is never emitted, so the dialog does not know the page changed.

Conclusion

Build a QgsMapLayerConfigWidget that reads and writes layer custom properties, emit widgetChanged on edits, write a factory with a title, icon and a narrow supportsLayer rule, register it through iface and unregister on unload, read settings through one helper, and validate before applying.

Frequently Asked Questions

Can the page appear in the styling panel too? Yes — return True from supportsStyleDock, and apply changes live when the panel is in live-update mode.

Can I add a page for raster layers? Yes; accept QgsRasterLayer in supportsLayer.

Where else can plugins store per-layer data? Custom properties are the standard place. For project-wide settings, use project entries; for user preferences, QgsSettings.

Is there a page for project properties too? Yes: QgsOptionsWidgetFactory adds pages to the Options dialog, as in adding a plugin options page, and project property pages are available on recent releases.