Adding a Plugin Page to the QGIS Options Dialog
Put plugin settings where users look for them: QgsOptionsPageWidget and QgsOptionsWidgetFactory, registering and unregistering the factory, why the reference…
TL;DR: subclass QgsOptionsPageWidget for the page and QgsOptionsWidgetFactory for the thing that builds it, register the factory in initGui(), keep a reference to it, and unregister it in unload() — read settings in the widget constructor and write them in apply(). This page is part of the plugin settings and configuration management guide.
Complete Runnable Code
"""A settings page inside the QGIS options dialog."""
from qgis.core import QgsSettings
from qgis.gui import QgsOptionsPageWidget, QgsOptionsWidgetFactory
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QCheckBox, QDoubleSpinBox, QFormLayout, QLineEdit
SECTION = "myplugin"
DEFAULTS = {"buffer_distance": 25.0, "service_url": "https://example.org/api",
"simplify_output": True}
def read(key):
default = DEFAULTS[key]
return QgsSettings().value("%s/%s" % (SECTION, key), default, type=type(default))
def write(key, value) -> None:
QgsSettings().setValue("%s/%s" % (SECTION, key), value)
class MyOptionsPage(QgsOptionsPageWidget):
"""One page in the QGIS options dialog. Built fresh each time it is opened."""
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.distance = QDoubleSpinBox()
self.distance.setRange(0.0, 100000.0)
self.distance.setSuffix(" m")
self.distance.setValue(read("buffer_distance"))
layout.addRow("Default buffer distance", self.distance)
self.url = QLineEdit(read("service_url"))
layout.addRow("Service endpoint", self.url)
self.simplify = QCheckBox()
self.simplify.setChecked(read("simplify_output"))
layout.addRow("Simplify output geometry", self.simplify)
def apply(self) -> None:
"""Called by QGIS when the user accepts the dialog — never on cancel."""
write("buffer_distance", self.distance.value())
write("service_url", self.url.text().strip())
write("simplify_output", self.simplify.isChecked())
class MyOptionsFactory(QgsOptionsWidgetFactory):
"""Builds the page on demand and gives it its entry in the dialog's list."""
def __init__(self):
super().__init__()
self.setTitle("My Plugin")
self.setIcon(QIcon(":/plugins/myplugin/icon.svg"))
def createWidget(self, parent=None) -> QgsOptionsPageWidget:
return MyOptionsPage(parent)
Architecture Breakdown
QgsOptionsWidgetFactory — registered once, kept forever
The factory is the object QGIS holds on to. It supplies the title and icon shown in the dialog’s list, and it is asked for a widget each time the dialog opens. Registering it looks like this, inside the plugin’s initGui():
def initGui(self) -> None:
self.options_factory = MyOptionsFactory() # the reference matters
self.iface.registerOptionsWidgetFactory(self.options_factory)
def unload(self) -> None:
self.iface.unregisterOptionsWidgetFactory(self.options_factory)
self.options_factory = None
Storing the factory on the plugin is not tidiness. Without a Python reference it is garbage collected while QGIS still holds a pointer, and the next attempt to open the options dialog crashes the application.
QgsOptionsPageWidget.apply() — the only place to write
QGIS calls apply() when the user accepts the dialog and does not call it when they cancel. That is the whole contract, and honouring it is what makes Cancel behave the way users expect.
The corollary is that the widget must not write settings as the user types. A valueChanged signal wired straight to QgsSettings produces a page where Cancel changes nothing back, which is worse than having no Cancel at all.
Reading in the constructor
The widget is constructed each time the dialog opens, so its constructor is the natural place to read current values. That also means it always shows what is actually stored rather than what was stored when the plugin loaded — which matters when something else changed a setting in between.
Validating What the User Typed
A settings page is a user-input surface, and free-text fields are where configuration bugs come from. Validate in apply() and refuse to store something the plugin cannot use:
from qgis.PyQt.QtWidgets import QMessageBox
def apply(self) -> None:
url = self.url.text().strip()
if url and not url.startswith(("http://", "https://")):
QMessageBox.warning(self, "My Plugin",
"The service endpoint must start with http:// or https://")
return # leave the stored value untouched
write("service_url", url)
Returning early leaves the previous value in place, which is the right failure mode: a plugin with its last known-good endpoint still works, while one that stored an unusable string does not. Where a value has a natural range, prefer a widget that cannot express an invalid one — a spin box with limits beats a line edit with a validator.
Laying the Page Out So It Reads Well
The options dialog is a shared space, and a page that ignores its conventions stands out in the wrong way. Three habits keep a plugin page looking like part of the application rather than something bolted on.
Group related controls under headings rather than presenting a flat list of twenty rows. A QFormLayout per group inside a QVBoxLayout gives you that with no custom painting, and it turns a wall of fields into three or four ideas a user can skim.
Label the unit rather than putting it in the field. A spin box with a " m" suffix cannot be misread; a field labelled “Buffer distance” whose value is silently in metres will eventually be filled in with kilometres. Where a value has a natural range, set it on the widget, so an impossible number is simply not typeable.
Finally, keep the page to what a user genuinely needs to change. Every control is a question you are asking, and a page that asks fifteen of them will have fourteen answered at random.
Production Best Practices
- Keep a reference to the factory, or opening the options dialog will crash QGIS.
- Unregister in
unload()with the same object, or the page outlives the plugin. - Write only in
apply(), so Cancel means cancel. - Read in the constructor, so the page always reflects what is stored now.
- Use constrained widgets rather than validating free text wherever the value allows it.
- Give the page an icon. The options dialog is a long list, and an unlabelled entry is effectively hidden.
Frequently Asked Questions
Why does opening the options dialog crash QGIS?
Almost certainly because the factory was garbage collected. QGIS keeps a raw pointer to the registered factory; if the only Python reference was a local variable in initGui(), the object is freed as soon as that method returns, and the next dialog open dereferences freed memory.
Assign it to an attribute on the plugin instance, and set that attribute to None only after unregistering.
Can I have more than one page?
Yes — register one factory per page. Each appears as its own entry in the options list. For a plugin with a handful of related settings this is usually worse than one page with grouped sections, because the options dialog belongs to QGIS and filling it with entries from one plugin is not neighbourly.
How do I add my page to a specific position in the list?
QgsOptionsWidgetFactory exposes setKey() and, in recent versions, path helpers that place the page within an existing group. Absolute ordering is not under your control, and it should not be: the list is shared with the application and every other plugin.
Should the page write to the project instead of settings?
Only if the values genuinely belong to the project. The options dialog is where users expect application preferences, so a page that quietly modifies the current project is surprising. When a value is project-scoped, put it in a plugin dialog with the project’s name visible, so it is obvious what the change applies to.
How do I react to a setting changing elsewhere?
There is no change signal on QgsSettings, so the answer is to read values when you use them rather than caching them at load. For a value that is expensive to act on — an endpoint that implies reconnecting — expose a small function in your plugin that applies the current settings, and call it from apply() as well as at start-up.
That keeps one code path for “make the plugin match the settings”, which is much easier to reason about than a set of signals that each update part of the state.
Does the page work when no project is open?
It should. QGIS can be running with an empty project, and the options dialog is still available, so a page that assumes a project — reading layers to fill a combo box, for instance — must handle the empty case. Populate such widgets lazily and tolerate an empty list rather than raising in the constructor.
Related
- Plugin Settings and Configuration Management — the parent guide covering where each value belongs
- Designing Qt Dialogs and Form Widgets — building the widgets a settings page is made of
- Plugin Lifecycle and Resource Management — registering in initGui and undoing it in unload