Qt Designer for GIS Interfaces
Building professional geospatial tools requires more than functional code; it demands an intuitive, responsive user interface. Qt Designer gives you a visual workflow that bridges complex spatial operations and end-user accessibility, and within the PyQGIS ecosystem its .ui files keep interface layout cleanly separated from your business logic. This guide sits inside the broader QGIS Plugin Development track: once you have a working skeleton from Plugin Boilerplate & Structure, Qt Designer is how you build the dialog its run() method opens. Everything here targets QGIS 3.34 LTR on Python 3, with API differences flagged where they matter, and walks a production-ready path for designing, loading, and integrating Qt Designer assets into a plugin.
Prerequisites
Before designing spatial interfaces, developers must establish a stable development environment. QGIS ships with PyQt bindings and the built-in uic module, but standalone Qt Designer must be installed separately via your OS package manager or the Qt Online Installer. Familiarity with Python, object-oriented programming, and the foundational concepts of QGIS Plugin Development is essential. Ensure your IDE recognizes QGIS Python paths, and verify that the qgis.PyQt.uic module is accessible. A working knowledge of Qt's layout system and signal-slot architecture will significantly reduce debugging time during interface integration.
Step-by-Step Workflow
The visual design process follows a predictable pipeline that aligns with standard PyQGIS architecture:
- Initialize the Layout Template: Open Qt Designer and select a template matching your plugin architecture. For standalone dialogs,
Dialog with Buttons Bottomis standard. For persistent tools,WidgetorDock Widgettemplates align better with QGIS workspace paradigms. - Place Standard Controls: Drag Qt widgets (
QComboBox,QSpinBox,QTableWidget,QLineEdit) onto the canvas. Assign descriptiveobjectNameproperties to every interactive element. These names become direct Python attributes during runtime. - Apply Layout Managers: Never rely on absolute positioning. Apply
QVBoxLayout,QHBoxLayout, orQGridLayoutto top-level containers. Set size policies toExpandingorMinimumExpandingto ensure responsive scaling across different DPI settings and QGIS window states. - Promote GIS-Specific Widgets: Select a standard widget, right-click, and choose
Promote to.... Enter the QGIS class name (e.g.,QgsMapLayerComboBox) and header file (e.g.,qgsmaplayercombobox.h). This instructs Qt Designer to generate placeholder code that QGIS will resolve at runtime through its Python bindings. - Export the
.uiFile: Save the design asmy_plugin_dialog.uiin your plugin'sui/directory. This XML-based format decouples visual structure from Python execution. - Load Dynamically (Recommended): Modern PyQGIS workflows bypass static compilation by loading
.uifiles dynamically at runtime usinguic.loadUiType(). This simplifies iteration, avoidspyuic5version mismatches, and reduces boilerplate.
Code Breakdown: Dynamic UI Integration
Once the interface is prepared, integration with PyQGIS requires careful initialization. The following pattern demonstrates runtime loading, which pairs seamlessly with the structural conventions outlined in Plugin Boilerplate & Structure.
import os
from qgis.PyQt import uic
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import QDialog, QMessageBox
from qgis.core import QgsMapLayerProxyModel
# Dynamically parse the .ui XML file at runtime
FORM_CLASS, _ = uic.loadUiType(os.path.join(
os.path.dirname(__file__), 'ui', 'my_plugin_dialog.ui'))
class MyPluginDialog(QDialog, FORM_CLASS):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
# Ensure automatic memory cleanup on close
self.setAttribute(Qt.WA_DeleteOnClose)
self._configure_gis_widgets()
self._connect_signals()
def _configure_gis_widgets(self):
# Filter layers to only show vector types
self.layer_combo.setFilters(
QgsMapLayerProxyModel.PointLayer
| QgsMapLayerProxyModel.PolygonLayer
| QgsMapLayerProxyModel.LineLayer
)
self.layer_combo.setAllowEmptyLayer(True)
def _connect_signals(self):
self.run_button.clicked.connect(self._execute_analysis)
self.cancel_button.clicked.connect(self.reject)
self.clear_button.clicked.connect(self._reset_inputs)
def _execute_analysis(self):
selected_layer = self.layer_combo.currentLayer()
if not selected_layer:
QMessageBox.warning(self, "Missing Input", "Please select a valid vector layer.")
return
# GIS processing logic goes here
QMessageBox.information(self, "Success", "Analysis triggered successfully.")
self.accept()
def _reset_inputs(self):
self.layer_combo.setLayer(None)
if hasattr(self, "threshold_spin"):
self.threshold_spin.setValue(0)
The uic.loadUiType() function parses the XML interface and returns a dynamic class that inherits from the base Qt widget. Calling self.setupUi(self) injects all defined controls into the dialog. Promoting widgets within Qt Designer requires specifying the exact header file and class name. When the dialog initializes, QGIS automatically resolves these headers through its Python bindings, eliminating manual import statements.
Signal Handling & Map Interaction
Qt's signal-slot architecture drives interactive behavior. Map tools require careful state management to avoid blocking the main thread. When connecting UI controls to spatial operations, always validate inputs before triggering heavy geoprocessing. The QgsTaskManager framework should handle long-running operations, keeping the interface responsive — and for algorithms you expect to reuse, route the work through a processing provider plugin rather than embedding it in a slot, which gives you batch execution and Model Builder support for free.
Not every interface is a modal dialog. A tool the user keeps open while working — a layer inspector or live query panel — belongs in a dockable panel instead; add a custom dock widget in PyQGIS applies the same .ui loading and widget-promotion patterns to a QDockWidget.
To prevent memory leaks or dangling references, avoid manual signal disconnection unless absolutely necessary. Qt's parent-child hierarchy and Qt.WA_DeleteOnClose handle cleanup automatically. If you must disconnect, wrap the call in a try/except block to prevent RuntimeError when the slot is already disconnected.
Internationalization
Production plugins must support multilingual workflows. Qt Designer stores translatable strings in the .ui file using standard tr() wrappers. After generating a .ts translation file with pylupdate5, translators populate localized strings, which are compiled into .qm binary files using lrelease. Loading these at runtime requires initializing QTranslator before the plugin UI instantiates — see the boilerplate __init__ pattern in Plugin Boilerplate & Structure for the standard loading sequence.
Common Errors & Fixes
Visual interface development introduces specific failure modes. Understanding these prevents deployment bottlenecks.
1. ImportError: No module named 'qgis.PyQt.uic'Cause: Running the script outside the QGIS Python environment or using a mismatched PyQt version.
Fix: Always execute PyQGIS code within the QGIS Python console or a virtual environment configured with qgis.core and qgis.PyQt paths. Verify sys.executable points to the QGIS Python interpreter.
2. Widget Promotion Fails at RuntimeCause: Qt Designer cannot locate the promoted header, or the header path is incorrect for the current QGIS version.
Fix: Use the exact class name and header as documented in the QGIS API reference. For example, qgsmaplayercombobox.h resolves correctly in QGIS 3.x. If promotion fails, instantiate the widget programmatically after setupUi() and replace the placeholder using layout management.
3. UI Layout Breaks on High-DPI DisplaysCause: Hardcoded pixel dimensions or missing layout containers.
Fix: Apply QVBoxLayout or QGridLayout to top-level containers. Set size policies to Expanding or MinimumExpanding. Test interfaces with QT_SCALE_FACTOR=2 to simulate high-DPI environments.
4. Memory Leaks from Unclosed DialogsCause: Creating new dialog instances without proper parent assignment or garbage collection.
Fix: Pass iface.mainWindow() as the parent during initialization, or use self.setAttribute(Qt.WA_DeleteOnClose). For modal dialogs, call exec() instead of show() to block execution until closure.
5. pyuic5 Compilation ErrorsCause: Malformed XML in the .ui file or unsupported custom widgets.
Fix: Validate the .ui file by reopening it in Qt Designer. Remove unsupported third-party widgets before compilation. Alternatively, switch to runtime uic.loadUiType() to bypass compilation entirely.
Packaging Considerations
Once the interface is stable, asset distribution requires careful planning. The .ui files must be included in the plugin's directory structure and referenced correctly in the initialization script — the same ui/ folder that Plugin Boilerplate & Structure reserves for exactly this purpose. When preparing releases, ensure all UI resources are bundled alongside Python modules. For plugins distributed through publishing to the QGIS plugin repository, the zip structure must include the ui/ folder with every .ui file intact so that uic.loadUiType() resolves correctly on end-user machines. Because dynamic loading depends on the raw XML shipping with the package, add a check for it to your build — the headless harness in testing and CI for plugins can assert that each dialog constructs without a running desktop.
Two ways to turn a .ui file into a class
Qt Designer produces XML. Getting from that XML to a usable Python class can happen at build time or at run time, and the choice affects packaging, debugging and how quickly a design change appears.
The runtime form is the better default during development, because a change in Designer is visible after a plugin reload with no build step in between. Build the path from __file__ rather than hard-coding it, or the dialog will load on your machine and fail on everyone else's:
import os
from qgis.PyQt import uic
FORM_CLASS, _ = uic.loadUiType(
os.path.join(os.path.dirname(__file__), "dialog.ui")
)
Breakdown: loadUiType() returns a tuple of the generated form class and the base widget class, which is why the second value is discarded. Resolving the path from os.path.dirname(__file__) makes it correct wherever the plugin is installed — an absolute path from your development tree is the single most common reason a dialog fails for other users. Doing this at module level means the parsing cost is paid once at import rather than on every dialog construction.
Naming is the contract between Designer and Python
Everything you place in Designer becomes an attribute on the dialog, named by its object name. That makes the object name a genuine API rather than a label, and renaming a widget in Designer silently breaks every line of Python that referenced it.
The practical discipline is short: give every widget you will touch from code a deliberate, prefixed name — btn_run, cmb_layer, spn_distance — and leave the rest at their defaults. A dialog where only the interactive widgets are named tells the next reader exactly which ones the code cares about.
Signal connections are the other half of that contract. Qt supports automatic connection by naming convention, where a method called on_btn_run_clicked is wired up implicitly — and it is worth avoiding. The connection is invisible, a typo produces silence rather than an error, and renaming either side breaks it without warning. Explicit self.btn_run.clicked.connect(self.run) is one more line and fails loudly when the widget name is wrong.
Key Takeaways
- Design in the
.uifile, behave in Python. Keep every layout, size policy, andobjectNamein Qt Designer, and put all logic — validation, geoprocessing, signal handling — in the dialog class. That separation is what lets the interface and the code evolve independently. - Prefer
uic.loadUiType()overpyuic5. Runtime loading parses the XML on the user's machine, sidesteppingpyuic5version mismatches between your build box and their install, and removes a compile step from every iteration. - Promote QGIS widgets, don't rebuild them. Right-click a plain widget and promote it to
QgsMapLayerComboBox,QgsFieldComboBox, or similar; QGIS resolves the header through its Python bindings at runtime, so you inherit layer filtering and CRS awareness for free. - Name every interactive widget.
setupUi(self)only binds controls that carry anobjectName; an unnamed widget exists in the layout but is unreachable from Python. - Keep the main thread free. Validate in the slot, then hand heavy work to
QgsTaskor a Processing algorithm so the dialog — and the whole QGIS window — never freezes. - Let Qt own cleanup. Pass
iface.mainWindow()as the parent and setQt.WA_DeleteOnClose; avoid manual signal disconnection unless you truly need it.
With the dialog in place, the natural next steps are exposing its logic as a reusable processing provider plugin, covering it with automated tests and CI, and shipping it through the QGIS plugin repository.
Frequently Asked Questions
Should I use uic.loadUiType() or compile the .ui file with pyuic5?
Dynamic loading with uic.loadUiType() is recommended for QGIS plugins because it parses the .ui file at runtime, avoiding pyuic5 version mismatches between your build machine and end-user installs. Static compilation is only worth it when you need to ship without the raw .ui or want a marginal startup gain. For QGIS 3.34 LTR, runtime loading is the simplest reliable path.
How do I embed a QGIS widget like QgsMapLayerComboBox in Qt Designer?
Drop a plain QComboBox onto the canvas, right-click it, and choose Promote to..., then enter the class name QgsMapLayerComboBox and header qgsmaplayercombobox.h. Qt Designer stores a placeholder that QGIS resolves through its Python bindings at runtime, so no manual import is needed. After setupUi() you can call methods like setFilters() and currentLayer() directly.
Why are my widget objectName values not becoming Python attributes?setupUi(self) only binds widgets that have an objectName assigned in Qt Designer. If you skipped naming a control, it exists in the layout but is unreachable from Python. Give every interactive widget a descriptive objectName such as layer_combo or run_button before saving the .ui file.
How do I keep the interface responsive during heavy geoprocessing?
Validate inputs in the slot, then hand long-running work to QgsTask or the Processing Framework rather than running it inline on the main thread. Connecting a button's clicked signal directly to a blocking function freezes the dialog and the whole QGIS window. Push status updates back to the UI from the task's completion signal.
How should I handle dialog cleanup to avoid memory leaks?
Pass iface.mainWindow() as the parent and set self.setAttribute(Qt.WA_DeleteOnClose) so Qt's parent-child hierarchy disposes of the dialog automatically. Avoid manual signal disconnection unless necessary; if you must disconnect, wrap it in try/except to swallow the RuntimeError raised when a slot is already gone. For modal dialogs, use exec() instead of show().
Should I compile the .ui file or load it at run time?
Load it at run time during development, because a change in Designer then appears after a plugin reload with no build step. Compiling ahead of time is worth it only when you want to ship without the .ui file or need the generated source in version control.
Why does my dialog fail to open on someone else's machine?
Almost always an absolute path to the .ui file left over from development. Build the path from os.path.dirname(__file__) so it resolves wherever the plugin is installed.
Should I use Qt's automatic signal connections?
No. Connections made by naming convention are invisible in the source, fail silently on a typo and break when either side is renamed. An explicit connect() call is one extra line and fails loudly when something is wrong.
Can I use the QGIS-specific widgets in Designer?
Yes. QGIS ships a Designer plugin providing widgets such as the layer combo box and the file picker, and using them saves reimplementing behaviour users already know. They import from qgis.gui, so a dialog using them cannot be tested headless.
Should the dialog contain any logic? As little as possible. A dialog that collects values and emits them keeps the logic testable without a display, which is the difference between a suite that runs in CI and one that does not.
How do I make a dialog remember its size?
Save the geometry in closeEvent and restore it when the dialog is constructed, using QgsSettings keyed under your plugin's name. Qt provides saveGeometry() and restoreGeometry() for exactly this, and the stored value is opaque, so no parsing is needed.
Should dialogs be modal? Rarely. A modal dialog blocks the whole application, which prevents the user consulting the map they are trying to describe. A modeless dialog or a dock widget is almost always the better choice for GIS work.
Related Guides
- Up: QGIS Plugin Development — the parent guide covering the full plugin journey.
- QGIS Plugin Boilerplate & Structure — build the skeleton whose
run()method opens the dialog you design here. - Add a Custom Dock Widget in PyQGIS — apply the same
.uiand promotion patterns to a persistentQDockWidget. - Processing Provider Plugins for QGIS — move heavy geoprocessing out of your slots and into reusable algorithms.
- Testing & CI for QGIS Plugins — assert that each dialog constructs headlessly.
- Publishing to the QGIS Plugin Repository — bundle the
ui/folder correctly for end-user installs.