Use QgsMapLayerComboBox in a Plugin Dialog

Almost every plugin dialog starts with "which layer?" and, very often, "which field?". Writing that by hand means populating a QComboBox from the project, filtering it to the right geometry type, keeping it in step as layers are added and removed, and mapping the selected text back to a layer object. QGIS ships widgets that do all of it, and using them removes about forty lines and several bugs per dialog.

This recipe belongs to Qt Designer for GIS Interfaces. It covers QgsMapLayerComboBox and its filters, linking a QgsFieldComboBox to it, promoting the widgets in Qt Designer so the .ui file carries them, and the details that keep the dialog honest when the project changes underneath it.

Two linked widgets doing the bookkeepingThe layer combo box lists only the layers matching its filter and updates itself when the project changes. Selecting a layer sets it on the field combo box, which then lists only that layer's fields, filtered to the types the operation accepts. Neither needs manual population.The widgets watch the project; you watch neitherthe projectparcels (polygon)zones (polygon)roads (line)dem (raster)QgsMapLayerComboBoxfilter: PolygonLayershows two of the fourlayerChangedQgsFieldComboBoxfilter: Numericrepopulates itselfwhat you no longer writepopulate on openrefresh on layersAddedmap name back to layerhandle the empty case

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A plugin with a dialog, whether built in Qt Designer or in code.
  • The qgis.gui module, which is available in any plugin context.

The layer combo box

Three lines give a filtered, self-maintaining layer picker.

from qgis.gui import QgsMapLayerComboBox
from qgis.core import QgsMapLayerProxyModel

combo = QgsMapLayerComboBox(self)
combo.setFilters(QgsMapLayerProxyModel.PolygonLayer)
combo.setAllowEmptyLayer(True, "— choose a layer —")

layer = combo.currentLayer()

Breakdown: setFilters() takes a flag combination, so PolygonLayer | LineLayer accepts both and VectorLayer accepts any geometry. The widget listens to the project itself, so a layer added while the dialog is open appears immediately without any code on your part. setAllowEmptyLayer(True, …) adds a null entry, which matters because otherwise the first matching layer is preselected and a user who forgets to choose gets an operation on whatever happened to be first — the most common cause of a plugin doing something surprising.

currentLayer() returns the layer object directly, or None when the empty entry is selected. That is the whole reason to use the widget: no name lookup, no ambiguity when two layers share a name.

Filters worth knowing beyond geometry type: HasGeometry excludes attribute-only tables, WritableLayer excludes read-only sources, and RasterLayer and MeshLayer select those types. They can be combined, and setExceptedLayerList() removes specific layers — useful for stopping a dialog offering its own output layer as an input.

QgsFieldComboBox lists the fields of whatever layer it is given.

from qgis.gui import QgsFieldComboBox
from qgis.core import QgsFieldProxyModel

field_combo = QgsFieldComboBox(self)
field_combo.setFilters(QgsFieldProxyModel.Numeric)
field_combo.setAllowEmptyFieldName(True)

combo.layerChanged.connect(field_combo.setLayer)
field_combo.setLayer(combo.currentLayer())

Breakdown: Connecting layerChanged directly to setLayer is the whole linkage — no intermediate slot, because the signal emits a layer and the slot takes one. The explicit call afterwards is easy to omit and matters: signals only fire on change, so the field combo starts empty until the user picks a different layer. QgsFieldProxyModel.Numeric covers integers and doubles; String, Date, DateTime and Binary are the other filters, and AllTypes is the default.

Reading the selection is field_combo.currentField(), which returns the field name as a string, or an empty string when the empty entry is chosen.

Promote the widgets in Qt Designer

A dialog built in Designer can carry these widgets directly, which keeps the layout in the .ui file where it belongs.

Drop an ordinary QComboBox onto the form, right-click it and choose Promote to…, then enter QgsMapLayerComboBox as the promoted class name and qgis.gui as the header file. Designer will show a plain combo box; QGIS substitutes the real widget at load time. The same procedure works for QgsFieldComboBox, QgsFileWidget, QgsColorButton, QgsExtentGroupBox and the rest of the QGIS widget set.

from qgis.PyQt import uic
import os

FORM, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), "dialog.ui"))


class MyDialog(QtWidgets.QDialog, FORM):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.layerCombo.setFilters(QgsMapLayerProxyModel.PolygonLayer)
        self.fieldCombo.setFilters(QgsFieldProxyModel.Numeric)
        self.layerCombo.layerChanged.connect(self.fieldCombo.setLayer)
        self.fieldCombo.setLayer(self.layerCombo.currentLayer())

Breakdown: The filters are set in code rather than in Designer because Designer does not know about the flag enums — the .ui file holds the layout and the promotion, the constructor holds the configuration. Setting them in __init__ immediately after setupUi() keeps them next to the widget they configure. If QGIS's own custom widgets appear in Designer's widget box, the QGIS custom widgets plugin is installed and promotion is unnecessary.

Promotion substitutes the real widget at load timeQt Designer shows a plain combo box because it does not have the QGIS widget available. The ui file records the promoted class name and header. When uic loads the file at runtime it imports that class from qgis.gui and instantiates it in place of the plain widget.Designer shows a placeholder; the runtime shows the real thingin Qt DesignerQComboBox (empty)right-click → Promote to…class + headerdialog.ui<customwidget>QgsMapLayerComboBoxheader: qgis.guirecorded in the fileat runtimeparcels ▾populated, filtered,watching the project

Reacting to the chosen layer

Once a layer is selected, the rest of the dialog usually needs to know about it — a preview count, a range derived from the data, a warning about the CRS.

def on_layer_changed(self, layer):
    if layer is None:
        self.summaryLabel.setText("")
        return

    count = layer.featureCount()
    crs = layer.crs()
    parts = [f"{count:,} feature(s)" if count >= 0 else "feature count unknown"]
    if crs.isGeographic():
        parts.append(f"⚠ {crs.authid()} is geographic — distances are in degrees")
    else:
        parts.append(f"{crs.authid()}, units: {crs.mapUnits()}")
    self.summaryLabel.setText(" · ".join(parts))

Breakdown: featureCount() returns -1 when the provider cannot answer cheaply — a large PostGIS table or a WFS layer — so a bare format string produces "-1 features" and looks like a bug. Warning about a geographic CRS here, rather than after the user has run the operation, is the single most useful thing a buffer dialog can do; the same check appears in setting the project CRS because it causes trouble everywhere.

The signal to connect is layerChanged, which emits the layer object, so this slot needs no lookup. Connecting it alongside the field combo's setLayer means both happen from one user action with no ordering to manage.

Restore and remember a selection

A dialog that reopens with the user's last choice is noticeably better, and the widget makes it two lines.

from qgis.core import QgsProject, QgsSettings


def restore(self):
    settings = QgsSettings()
    layer_id = settings.value("my_plugin/last_layer", "")
    layer = QgsProject.instance().mapLayer(layer_id)
    if layer is not None:
        self.layerCombo.setLayer(layer)
        self.fieldCombo.setField(settings.value("my_plugin/last_field", ""))


def remember(self):
    settings = QgsSettings()
    layer = self.layerCombo.currentLayer()
    settings.setValue("my_plugin/last_layer", layer.id() if layer else "")
    settings.setValue("my_plugin/last_field", self.fieldCombo.currentField())

Breakdown: Storing the layer id rather than the name is what makes this correct when two layers share a name, and mapLayer(id) returning None handles the case where the remembered layer is no longer in the project — which is why the guard is not optional. setLayer() on a layer that the filter excludes silently does nothing, so a remembered raster does not appear in a polygon-filtered combo. Storing under a plugin-prefixed key follows the convention described in storing plugin settings with QgsSettings.

Enable the button only when the choice is valid

The widgets emit signals on every change, which is exactly what a validation gate needs.

def wire_validation(self):
    for signal in (self.layerCombo.layerChanged, self.fieldCombo.fieldChanged):
        signal.connect(self.update_ok_state)
    self.update_ok_state()


def update_ok_state(self, *_):
    layer = self.layerCombo.currentLayer()
    field = self.fieldCombo.currentField()
    ok = layer is not None and bool(field) and layer.featureCount() > 0
    self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(ok)

Breakdown: Accepting *_ lets one slot serve signals with different argument counts, which avoids two nearly identical methods. Checking featureCount() > 0 catches the empty-layer case that otherwise produces a confusing "no results" later — although note it returns -1 for some providers when the count is unknown, so a strict > 0 also disables the button for those and != 0 is safer. Calling the update once at the end sets the initial state, since no signal has fired yet.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.16 LTR3.7QgsMapLayerComboBox, QgsFieldComboBox and the proxy model filters present.
3.22 LTR3.9setAllowEmptyLayer gains the placeholder text argument.
3.28 LTR3.9QgsMapLayerProxyModel filter flags unchanged.
3.34 LTR3.12Baseline for this page.
3.36+3.12Filter enums gain a scoped form; the flat names remain as aliases.

Troubleshooting

  • The combo box is empty. The filter excludes every layer in the project. Check the flags against the actual layer types.
  • The field combo stays empty until the layer changes. setLayer() was not called once after wiring the signal.
  • A remembered layer is not restored. It was stored by name, or the filter excludes it. Store the id and check the filter.
  • The widget shows in Designer but not at runtime. The promotion header is wrong; it must be qgis.gui.
  • The dialog offers its own output layer as an input. Add it to setExceptedLayerList() after creating it.
  • currentLayer() returns None unexpectedly. The empty entry is selected. That is what setAllowEmptyLayer is for.

Conclusion

Use QgsMapLayerComboBox with an explicit filter and an empty entry, link QgsFieldComboBox through layerChanged and call setLayer once afterwards, promote both in Designer with the qgis.gui header, and remember the selection by layer id. The widgets handle project changes on their own, which is most of the code you would otherwise write and all of the bugs.

Frequently Asked Questions

Are there similar widgets for other inputs? Many: QgsFileWidget for paths, QgsExtentGroupBox for extents, QgsProjectionSelectionWidget for a CRS, QgsColorButton, QgsSpinBox, QgsDoubleSpinBox and QgsExpressionLineEdit. All promote the same way.

Can I filter to layers with a particular field? Not through the proxy model. Set setExceptedLayerList() from a check you run yourself, and refresh it when the project changes.

Does the combo box include layers not in the layer tree? It follows the project's layer registry, so a layer added with addMapLayer(layer, False) appears even though it is not in the tree. Exclude it explicitly if that is wrong for your dialog.

How do I make a processing algorithm's dialog do this? It already does — declaring a QgsProcessingParameterFeatureSource produces exactly these widgets automatically. See adding parameters to a processing algorithm.