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

python
"""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)
When dialog state is read and written A timeline of a dialog session: geometry and values are restored in showEvent, the user edits them, accept writes the values, and closeEvent writes the geometry whether the dialog was accepted or cancelled. one dialog session showEvent restore geometry and last values user edits nothing stored yet accept write values only on OK closeEvent write geometry even on cancel

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.

What is worth remembering, and what is not A grid of four kinds of dialog state — window geometry, the last output folder, a chosen layer, and a destructive option — showing whether each should persist and why. remember? why window geometry yes costs nothing, users expect it last output folder yes saves the most typing selected layer by name only the layer may be gone a destructive option no must be chosen deliberately each time

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.

What to do when a restored value no longer applies A decision tree for restoring state that has gone stale: fall back to the default when a path is missing, select nothing when a remembered layer is absent, and clamp a value that is now out of range. The stored value no longer makes sense path is gone fall back to the default folder layer is gone select nothing do not guess out of range clamp silently the range changed

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().