Use QGIS Custom Widgets in Qt Designer

Every plugin dialog needs the same handful of controls: pick a layer, pick a field from that layer, choose an output file, choose a colour, choose a coordinate system. Writing them from plain Qt widgets means populating combo boxes from the project, keeping them in sync when layers are added, filtering by geometry type, and validating paths — perhaps two hundred lines that QGIS has already written and that behave exactly like the rest of the application.

This recipe belongs to Qt Designer for GIS Interfaces. It covers the widgets worth knowing, how to place them in a .ui file through promotion, how to use them directly in code, and how to wire the dependent ones together.

The same dialog, built two waysBuilt from plain Qt widgets, a layer combo box must be filled from the project, filtered by geometry type, and refreshed whenever a layer is added or removed, and the field combo must be repopulated whenever the layer changes. Built from QGIS widgets, all of that behaviour is already present and the plugin code only reads the selected values.Two hundred lines you do not have to write or maintainplain QComboBoxfill it from the projectfilter to polygon layersrefresh on layersAdded and removedrepopulate fields on changeand keep it all workingQgsMapLayerComboBoxalready knows the projectsetFilters — one linestays in sync automaticallyfield box follows it by signaland looks like QGIS

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • Qt Designer, from your QGIS installation or your package manager.
  • A plugin dialog to work on — see Load a .ui File at Runtime in PyQGIS.

The widgets worth knowing

  • QgsMapLayerComboBox — every layer in the project, filterable by type and geometry, kept in sync automatically.
  • QgsFieldComboBox — the fields of a layer, filterable to numeric, string or date fields.
  • QgsFieldExpressionWidget — a field picker that also accepts an expression, with the expression builder attached.
  • QgsFileWidget — a path field with a browse button, in file, save, directory or multi-file mode.
  • QgsColorButton — a colour swatch opening QGIS's colour dialog, with the project colour scheme.
  • QgsExtentGroupBox — an extent, from a layer, the canvas, or typed coordinates.
  • QgsProjectionSelectionWidget — a coordinate system picker with recent and project options.
  • QgsSpinBox and QgsDoubleSpinBox — spin boxes that support a clearable "not set" state.
  • QgsCollapsibleGroupBox — a group box that remembers whether the user collapsed it.

Each behaves the way its counterpart in QGIS's own dialogs behaves, which means users already know how to use them.

Promote a widget in Qt Designer

QGIS's widgets are not in Designer's palette unless the custom widget plugin is installed, and the portable way to place them is promotion:

  1. Drop the closest standard widget onto the form — a QComboBox for a layer picker, a QWidget for anything without an obvious base.
  2. Right-click it and choose Promote to….
  3. Set the promoted class name to QgsMapLayerComboBox and the header file to qgis.gui.
  4. Press Add, then Promote.

Breakdown: Promotion records "treat this widget as that class" in the .ui file, and the header value qgis.gui is what tells the Python loader which module to import — it is a Python module path here rather than a C++ header, which is the detail that trips people up. Once promoted, the widget is a plain QComboBox in Designer's preview and the real QGIS widget at run time. The base class matters: promote a QComboBox for the combo widgets and a QWidget for the file, colour, extent and projection widgets, or the loader raises about incompatible types.

Or build them in code

For a dialog assembled in Python, skip Designer entirely:

from qgis.core import QgsMapLayerProxyModel, QgsFieldProxyModel
from qgis.gui import (QgsMapLayerComboBox, QgsFieldComboBox,
                      QgsFileWidget, QgsProjectionSelectionWidget)

self.layer_box = QgsMapLayerComboBox()
self.layer_box.setFilters(QgsMapLayerProxyModel.PolygonLayer)
self.layer_box.setAllowEmptyLayer(True)

self.field_box = QgsFieldComboBox()
self.field_box.setFilters(QgsFieldProxyModel.Numeric)

self.output = QgsFileWidget()
self.output.setStorageMode(QgsFileWidget.SaveFile)
self.output.setFilter("GeoPackage (*.gpkg)")

self.crs_box = QgsProjectionSelectionWidget()
self.crs_box.setOptionVisible(QgsProjectionSelectionWidget.CurrentCrs, True)

Breakdown: setFilters() on the layer box takes proxy-model flags that can be combined — PolygonLayer | LineLayer for a dialog accepting either — and it filters live, so a raster added later never appears. setAllowEmptyLayer(True) adds a blank entry, which is the correct way to express an optional input rather than a separate checkbox. The field box's filters are a different enumeration with the same shape, which is easy to mix up: QgsFieldProxyModel for fields, QgsMapLayerProxyModel for layers. The projection widget's options control which shortcuts appear — project CRS, layer CRS, recently used — and showing the relevant ones saves the user a trip through the full selector.

Wire the dependent ones together

A field picker is only useful when it follows the layer picker.

self.layer_box.layerChanged.connect(self.field_box.setLayer)
self.field_box.setLayer(self.layer_box.currentLayer())

Breakdown: Two lines: connect the signal so future changes propagate, and call it once so the initial state is correct — omitting the second is why a dialog opens with an empty field list until the user touches the layer box. QgsFieldExpressionWidget uses the same pattern with the same method name, so swapping one for the other is a one-word change. For a dialog with several dependent widgets, connect them all in one place immediately after construction, which keeps the wiring visible rather than scattered through the class.

One signal keeps the dialog consistentThe layer combo box emits a layer changed signal. The field combo box, the field expression widget and the extent group box each connect to it so their contents follow the selected layer. Each connection must also be invoked once at construction so the dialog opens in a consistent state rather than with empty dependent widgets.Connect it, then call it onceQgsMapLayerComboBoxemits layerChangedQgsFieldComboBoxsetLayerQgsFieldExpressionWidgetsetLayerQgsExtentGroupBoxsetOutputExtentFromLayerconnect once, and prime the initial state

Read the values back

layer = self.layer_box.currentLayer()          # a QgsMapLayer or None
field = self.field_box.currentField()          # a field name, or an empty string
path = self.output.filePath()                  # a path, or an empty string
crs = self.crs_box.crs()                       # a QgsCoordinateReferenceSystem
expression = self.expression_widget.currentField()   # name or expression text

if layer is None:
    self.iface.messageBar().pushWarning("Parcel Tools", self.tr("Select a layer"))
    return

Breakdown: Each widget returns a real object rather than a string you have to look up — currentLayer() gives the layer itself, which removes the lookup-by-name step and with it the ambiguity when two layers share a name. Empty values are empty strings and None, never exceptions, so validation is a plain check. QgsFieldExpressionWidget returns either a field name or an expression, and its companion isValidExpression() tells you which — worth checking before passing the value somewhere that expects one or the other.

What promotion records in the .ui fileA standard widget dropped on the form is promoted by recording two values: the QGIS class it should become, and the module to import it from. Designer still shows the plain base widget in its preview, while the loader constructs the real QGIS widget at run time. The base widget must be compatible: a combo box for combo widgets and a plain widget for the rest.Two values, recorded once, resolved at run timein Designera plain QComboBoxthe preview never changeswhat promotion recordsQgsMapLayerComboBoxheader: qgis.guia Python module, not a C++ headerat run timethe real QGIS widgetfilters, syncs, knows the project

QGIS version compatibility

QGIS versionPythonNotes
3.22 LTR3.9All widgets listed are present with these methods.
3.28 LTR3.9Identical.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12Adds further widgets, including point cloud pickers; the filter enumerations gained scoped names alongside the legacy ones.

Promotion in .ui files is stable across all of these, since it records only a class name and a module path.

Troubleshooting

  • TypeError when loading the .ui file. The promoted class does not match the base widget — promote a QComboBox for combo widgets and a QWidget for the rest.
  • The widget appears as a plain combo box at run time. The header was left as a C++ header instead of qgis.gui.
  • The layer list is empty. Filters exclude everything, or the dialog was built before the project loaded. Check setFilters() first.
  • The field list stays empty. setLayer() was connected but never called once at construction.
  • A colour button shows the wrong colour. It was set before the dialog was shown; set values after construction, not during it.
  • The file widget accepts a folder for a file. Storage mode was left at its default. Set it explicitly.

Conclusion

Use the widgets QGIS ships: they know about the project, filter themselves, stay in sync, and look like the rest of the application. Place them in Designer by promoting a compatible base widget with qgis.gui as the header, or construct them directly in Python. Wire the dependent ones with one signal connection plus one priming call, and read values back as real objects rather than names.

Frequently Asked Questions

Why are the QGIS widgets missing from Designer's palette? The custom widgets plugin is not installed or not found. Promotion works without it and is more portable, which is why it is the recommended route.

Can I use these widgets outside a plugin? Yes, in any Qt application that has the QGIS libraries available — they are ordinary widgets. Most need a QGIS application initialised to be useful.

How do I restrict the layer box to one specific layer type and geometry? Combine proxy-model flags with the bitwise or: PolygonLayer | NoGeometry for polygons and attribute-only tables, for example.

Is there a widget for choosing a symbol or a colour ramp? Yes — QgsSymbolButton and QgsColorRampButton, both opening QGIS's own editors, which pairs well with Programmatic Layer Styling in PyQGIS.

Do these widgets handle translation? Their own labels and dialogs follow the QGIS interface language automatically. Your labels around them need tr() as usual.

Should a Processing algorithm use these? No — algorithms declare parameters and the framework builds the interface. These widgets are for a plugin's own dialogs.

What happens to a layer combo box when the user removes the selected layer? It drops the entry and emits layerChanged with the new selection, which may be None. Handle that case in the connected slot rather than assuming the value is always a layer — this is the most common crash in dialogs that stay open while the user works.

Can I preselect a layer when the dialog opens? Yes: setLayer(layer) on the combo box, typically with iface.activeLayer() if it passes your filters. Preselecting the layer the user already has highlighted removes one interaction from every run, and falls back gracefully when the active layer is the wrong type.