Remember the Last Used Folder in a Plugin
The difference between a plugin people tolerate and one they like is often this small: the file dialog opens in the folder they were working in yesterday, not in their home directory. It costs about six lines. Skipping it costs every user several seconds and a small irritation on every single run, which is a surprisingly effective way to make an otherwise good tool feel unfinished.
This recipe belongs to Plugin Settings and Localization. It covers storing and restoring a last-used path, keeping a short recent list, choosing sensible fallbacks when the stored path no longer exists, and doing the same for window size and position.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A plugin with a file or folder dialog somewhere.
- The settings conventions from Store Plugin Settings with QgsSettings.
Store and restore the folder
import os
from qgis.PyQt.QtWidgets import QFileDialog
from qgis.core import QgsSettings
PREFIX = "parcel_tools"
def choose_output_file(parent):
settings = QgsSettings()
start_dir = settings.value(f"{PREFIX}/last_folder", "", type=str)
path, _ = QFileDialog.getSaveFileName(
parent,
"Save parcel summary",
os.path.join(start_dir, "parcel_summary.gpkg"),
"GeoPackage (*.gpkg)",
)
if path:
settings.setValue(f"{PREFIX}/last_folder", os.path.dirname(path))
return path
Breakdown: The stored value is the folder, not the file, because a suggested file name should usually be a sensible default rather than whatever the user last called something. Joining the folder with a default file name gives both at once: the dialog opens in the right place with a name already filled in. Writing the setting only when a path came back means a cancelled dialog leaves the previous value alone. Note that an empty start_dir is harmless — QFileDialog falls back to its own default — so no special case is needed for a first run.
Fall back when the folder is gone
Stored paths go stale: network shares unmount, projects get archived, an external drive is not plugged in. A dialog that opens at a non-existent path behaves inconsistently across platforms, so check first.
from qgis.core import QgsProject
def resolve_start_dir():
settings = QgsSettings()
candidates = [
settings.value(f"{PREFIX}/last_folder", "", type=str),
QgsProject.instance().homePath(),
settings.value("UI/lastProjectDir", "", type=str),
os.path.expanduser("~"),
]
for candidate in candidates:
if candidate and os.path.isdir(candidate):
return candidate
return ""
Breakdown: The chain runs from most specific to most general: what this plugin last used, then the folder of the open project, then wherever QGIS itself last opened something, then home. Each is checked with isdir() rather than assumed, which is what makes an unmounted share degrade into a mild inconvenience instead of an error dialog. Reading UI/lastProjectDir — QGIS's own key — is the small touch that makes a plugin feel native, because it lands where the user was working even on a first run. The project home path is covered in Working with QGIS Projects in PyQGIS.
Keep a short recent list
For a plugin used across several projects, one remembered folder is not enough. A recent list of four or five, offered in a combo box, covers the way people actually work.
MAX_RECENT = 5
def remember_folder(folder):
settings = QgsSettings()
recent = settings.value(f"{PREFIX}/recent_folders", [], type=list)
recent = [f for f in recent if f != folder]
recent.insert(0, folder)
settings.setValue(f"{PREFIX}/recent_folders", recent[:MAX_RECENT])
def recent_folders():
settings = QgsSettings()
stored = settings.value(f"{PREFIX}/recent_folders", [], type=list)
return [f for f in stored if os.path.isdir(f)]
Breakdown: Removing the folder before inserting it at the front keeps the list ordered by recency without duplicates, which is the behaviour every "recent files" menu has and users expect without thinking about it. Truncating on write rather than on read keeps the stored value bounded. Filtering on read rather than on write is deliberate: a folder on a share that is temporarily unmounted should disappear from today's list and come back tomorrow, not be permanently forgotten because it was unavailable once.
Let QgsFileWidget do it for you
For a path field inside a dialog, QGIS's own widget already handles the browse button, the storage mode and validation — and it accepts a settings key for the default root.
from qgis.gui import QgsFileWidget
self.output_widget = QgsFileWidget()
self.output_widget.setStorageMode(QgsFileWidget.SaveFile)
self.output_widget.setFilter("GeoPackage (*.gpkg)")
self.output_widget.setDefaultRoot(resolve_start_dir())
self.output_widget.fileChanged.connect(
lambda path: remember_folder(os.path.dirname(path)) if path else None)
Breakdown: QgsFileWidget gives a line edit, a browse button and consistent behaviour with the rest of QGIS for four lines, and its storage modes cover files, existing files, directories and multiple files. setDefaultRoot() is where the resolved start directory goes. Connecting fileChanged means the recent list updates as soon as a path is chosen rather than only when the dialog is accepted, which is usually what you want — the user has expressed the intent by then. Using QGIS's widget also means a translated interface gets a translated browse button without any work on your part.
Remember dialog size too
The same mechanism restores window geometry, which users notice on small screens and multi-monitor setups.
def showEvent(self, event):
super().showEvent(event)
geometry = QgsSettings().value(f"{PREFIX}/dialog_geometry")
if geometry is not None:
self.restoreGeometry(geometry)
def closeEvent(self, event):
QgsSettings().setValue(f"{PREFIX}/dialog_geometry", self.saveGeometry())
super().closeEvent(event)
Breakdown: saveGeometry() returns an opaque byte array holding size, position and screen — pass it straight back to restoreGeometry() and never try to interpret it. Restoring in showEvent rather than the constructor means it also applies when a reused dialog is shown again. One caveat worth knowing: a geometry saved on a second monitor can place the dialog off-screen when that monitor is gone; recent Qt versions handle this, but a defensive if not self.geometry().intersects(screen_rect) check is cheap insurance if your users dock and undock laptops all day.
Remember more than the folder
Once the mechanism is in place, the same few lines cover everything else the user sets identically on every run. The judgement is about which values are safe to restore silently.
Safe to restore without comment: the output folder, the window size, an expanded or collapsed section, the last chosen output format, the state of a checkbox such as "add result to map". These change the convenience of the run, not its meaning, and a wrong guess costs a click.
Restore, but show clearly: the last selected layer or field. Restoring a layer name is helpful when the same project is open and confusing when it is not, so match on the layer still existing in the current project and fall back to no selection rather than to something arbitrary.
Do not restore silently: anything that changes what the operation does to data. A buffer distance, a destination table, an overwrite flag — restoring these means a user who ran a job with unusual settings last month gets them again today without noticing. Show the value in the dialog where it can be read, and never apply it to a run the user did not confirm.
The general shape is that memory should reduce the number of decisions, not make them on the user's behalf. A dialog that opens pre-filled with what you did last time, showing every value plainly, is helpful. One that quietly reuses a destructive setting is a bug waiting for a bad week.
QGIS version compatibility
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | QgsSettings, QgsFileWidget and geometry save and restore all present. |
| 3.28 LTR | 3.9 | Identical. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | Identical; newer releases improve off-screen geometry handling in Qt. |
Avoid QFileDialog static methods with the native dialog disabled unless you have a reason — the native dialog is what users recognise, and it already remembers its own state per application on some platforms.
Troubleshooting
- The dialog still opens in home. The stored value is empty, or it was written after the dialog rather than after a successful selection. Print it once to confirm what is stored.
- A stale path causes an error dialog. No existence check. Use the fallback chain.
- The recent list grows forever. Truncation happens on read rather than write, or
MAX_RECENTis applied to the wrong list. - The list comes back as a string. It contained non-string entries, or
type=listwas omitted on the read. - Restored geometry puts the dialog off-screen. A monitor that no longer exists. Validate against the available screen geometry before restoring.
- Values are lost when QGIS is killed. Settings are flushed on destruction; call
sync()after writing anything you cannot afford to lose.
Conclusion
Store the folder rather than the file, write it only after a successful selection, and resolve the starting directory through a fallback chain that ends somewhere that always exists. A five-item recent list covers people who move between projects, QgsFileWidget removes most of the boilerplate, and the same settings pattern restores dialog geometry. None of it is difficult, and together it is most of what makes a plugin feel considered.
Frequently Asked Questions
Should the last folder be a setting or a project entry? A setting. It belongs to the person and their machine, not to the map — storing a local path in the project file breaks for every colleague who opens it.
Can I use QGIS's own last-used directory instead of my own key? As a fallback, yes, and it is a good first-run default. As the only mechanism, no: other tools overwrite it, so your plugin would open wherever the last unrelated action happened to be.
How do I offer the recent list in the interface?
An editable combo box populated from recent_folders(), with a browse button beside it. The user gets one click for the common case and full freedom otherwise.
Does this work in a Processing algorithm? Processing manages its own parameter defaults and history, so let it. This pattern is for a plugin's own dialogs.
Is it worth remembering other choices too? Yes — the last selected layer, the last output format, the state of a checkbox. Anything the user sets identically every run is a candidate, provided the default remains sensible when it is missing.