Persisting Dialog State with QgsSettings
Make a QGIS plugin dialog remember itself: restore geometry and values in showEvent, write values on accept and geometry on close, validate restored state,…
TL;DR: restore geometry and last-used values in showEvent, write values in accept() and geometry in closeEvent(), and never restore a value without checking that it still makes sense — a remembered folder that has been deleted should quietly fall back rather than producing an error. This page is part of the designing Qt dialogs and form widgets guide.
Complete Runnable Code
"""A dialog that remembers where it was and what the user last chose."""
import os
from qgis.core import QgsProject, QgsSettings
from qgis.PyQt.QtWidgets import QComboBox, QDialog, QDialogButtonBox, QFormLayout, QLineEdit
SECTION = "myplugin/dialog"
class ExportDialog(QDialog):
"""Remembers its size, position, output folder and layer choice."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Export")
layout = QFormLayout(self)
self.folder = QLineEdit()
layout.addRow("Output folder", self.folder)
self.layer_combo = QComboBox()
layout.addRow("Layer", self.layer_combo)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addRow(buttons)
# ---- state ---------------------------------------------------------
def showEvent(self, event) -> None:
"""Populate and restore just before the dialog becomes visible."""
super().showEvent(event)
settings = QgsSettings()
geometry = settings.value("%s/geometry" % SECTION)
if geometry is not None:
self.restoreGeometry(geometry)
folder = settings.value("%s/folder" % SECTION, "", type=str)
self.folder.setText(folder if os.path.isdir(folder) else "")
self.layer_combo.clear()
names = sorted(l.name() for l in QgsProject.instance().mapLayers().values())
self.layer_combo.addItems(names)
remembered = settings.value("%s/layer" % SECTION, "", type=str)
if remembered in names: # only if it is still there
self.layer_combo.setCurrentText(remembered)
def accept(self) -> None:
"""Store the values the user settled on — never on cancel."""
settings = QgsSettings()
settings.setValue("%s/folder" % SECTION, self.folder.text().strip())
settings.setValue("%s/layer" % SECTION, self.layer_combo.currentText())
super().accept()
def closeEvent(self, event) -> None:
"""Store the geometry however the dialog was dismissed."""
QgsSettings().setValue("%s/geometry" % SECTION, self.saveGeometry())
super().closeEvent(event)
Architecture Breakdown
Geometry and values follow different rules
Window geometry should be remembered whatever happens — a user who resizes a dialog and then cancels still wants it that size next time. Values should be remembered only on acceptance, because Cancel means “forget what I just did”.
That is why the two are written in different places. closeEvent fires on both paths; accept() fires only on the one where the user committed.
Restore in showEvent, not in the constructor
Populating combo boxes from the project at construction time bakes in whatever the project contained at that instant. A dialog created once and shown repeatedly then offers a stale list, and a dialog created at plugin load offers the list from before any project was opened.
showEvent runs immediately before each appearance, which makes it the right place for anything read from the project or the settings store.
saveGeometry() handles more than size
It encodes size, position, maximised state and which screen the window was on, in an opaque byte array that restoreGeometry() understands. Storing width and height separately loses the rest and handles multi-monitor setups badly, so use the pair as intended rather than reconstructing them.
Choosing What to Remember
Not everything should be sticky, and remembering the wrong thing is worse than remembering nothing.
The last row is the important one. A checkbox that overwrites existing files, or one that runs a destructive operation, should start unchecked every time. Persisting it means a user who ticked it once in a hurry gets the destructive behaviour by default forever, and the interface gives them no reason to look.
Restoring Values That Have Gone Stale
A restored value is a guess about a world that has moved on, and the guess is often wrong.
The example handles both common cases explicitly: a folder that no longer exists falls back to empty rather than pre-filling a path that will fail, and a remembered layer name is only selected if a layer with that name is still in the project. Remembering the layer by name rather than by identifier is deliberate — identifiers change between projects, and a name at least has a chance of matching a layer the user recognises.
For values with a range, clamping quietly is usually right: a spin box whose limits changed in a new release should not refuse to open because the stored value is now out of bounds.
Making Restoration Feel Deliberate
There is a difference between a dialog that remembers and a dialog that surprises. The distinction is mostly about whether the restored value is visible before it is used.
Restoring a folder into a visible text field is fine, because the user sees it and can change it. Restoring the same folder into a hidden default that only takes effect when they press OK is not, because they never had the chance to notice it was wrong. Anything remembered should be on screen and editable at the moment it matters.
The corollary is that dialogs which remember well tend to have fewer hidden defaults, not more stored values. Where a choice has consequences a user would want to review — an overwrite, a destination, a scope — showing the remembered answer and letting them confirm it is both friendlier and less likely to produce a support conversation.
Production Best Practices
- Namespace the settings keys with the plugin and the dialog, so two dialogs cannot collide.
- Write geometry on close, values on accept.
- Restore in
showEvent, so the dialog reflects the project as it is now. - Validate every restored value before applying it.
- Never persist a destructive option.
- Give the user a way to reset, even if it is only removing the settings section — a dialog that opens off-screen after a monitor change is otherwise unusable.
Frequently Asked Questions
Why does my dialog open off-screen?
Because the stored geometry refers to a monitor that is no longer attached. restoreGeometry() does some sanity checking but cannot cover every case, particularly with mixed scaling factors.
The defensive fix is to check the restored frame against the available screen geometry after restoring and re-centre if it does not intersect. A simpler mitigation is a reset option in your settings page, which costs two lines and rescues the user without an explanation.
Should state be stored per project or globally?
Globally, for almost everything in a dialog. A window size and a last-used folder describe how the person works, not what the project contains. The exception is a value genuinely tied to the data — a chosen field, a project-specific output path — which belongs in the project, as covered in plugin settings and configuration management.
How do I remember a layer reliably?
You mostly cannot, and it is worth accepting that. Layer identifiers are unique but change between projects; names are stable to the user but not unique and can be edited. Storing the name and selecting it only when it still matches is the pragmatic compromise: it works in the common case of reopening the same project, and it fails safely otherwise.
Does QgsSettings handle a QByteArray from saveGeometry()?
Yes — it round-trips binary values, and reading it back gives you something restoreGeometry() accepts. Do not pass a type= argument for this one; the coercion that helps with booleans and numbers gets in the way of an opaque byte array.
How do I handle a dialog opened from several places?
Give each entry point its own settings section if the dialogs genuinely serve different purposes, and share one section if they are the same dialog reached two ways. The failure to avoid is two call sites writing the same keys with different meanings, which produces a dialog that appears to remember the wrong things at random.
Where a dialog is parameterised — the same widget exporting different layer types, say — including the parameter in the key is usually clearer than trying to make one remembered value serve both.
When should I not persist anything?
For a dialog that appears once during a workflow and whose values are derived from context rather than preference. Persisting there adds a settings key nobody benefits from and one more thing to migrate later. Persistence earns its place where the user would otherwise retype the same value.
Should the dialog write settings as the user edits?
No. That defeats Cancel, and it makes every keystroke a settings write. Collect the state in the widgets, which is what they are for, and commit it once in accept().
Related
- Designing Qt Dialogs and Form Widgets — the parent guide covering dialog construction
- Plugin Settings and Configuration Management — namespacing, typed reads and where each value belongs
- Adding a Plugin Page to the QGIS Options Dialog — where a reset control belongs