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.
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.guimodule, 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.
Link a field combo box
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.
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 version | Python | Notes |
|---|---|---|
| 3.16 LTR | 3.7 | QgsMapLayerComboBox, QgsFieldComboBox and the proxy model filters present. |
| 3.22 LTR | 3.9 | setAllowEmptyLayer gains the placeholder text argument. |
| 3.28 LTR | 3.9 | QgsMapLayerProxyModel filter flags unchanged. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.36+ | 3.12 | Filter 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 whatsetAllowEmptyLayeris 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.