Create a QGIS Plugin with Plugin Builder 3
Writing a plugin skeleton by hand is error-prone: a single typo in metadata.txt or a missing classFactory keeps your plugin out of the manager. Plugin Builder 3 removes that friction. It is itself a QGIS plugin that runs a wizard, asks a handful of questions, and emits a complete, loadable plugin directory — __init__.py, metadata.txt, a main class wired into the toolbar and menu, a Qt Designer dialog, a resources.qrc, and a build helper. This page walks through running the wizard, compiling resources, deploying the output to your profile, enabling it, and replacing the placeholder logic with your own.
This task sits inside the broader topic of Plugin Boilerplate & Structure. If you would rather understand every file before generating it, read that cluster first; Plugin Builder produces exactly the structure described there, so the two pair well.
Prerequisites
- QGIS 3.34 LTR (bundles Python 3.12) or any 3.x release with a working Python console
- Plugin Builder 3 installed via
Plugins → Manage and Install Plugins, then search for "Plugin Builder" and install - Plugin Reloader (optional but strongly recommended) for fast iteration without restarting QGIS
- A resource compiler on your
PATH—pyrcc5(ships with PyQt5) orpyqgistoolchain access - Write access to your QGIS profile plugins directory
The profile plugins directory is where QGIS scans for installed plugins:
- Windows:
%APPDATA%\QGIS\QGIS3\profiles\default\python\plugins\ - macOS:
~/Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins/ - Linux:
~/.local/share/QGIS/QGIS3/profiles/default/python/plugins/
Step 1: Run the Plugin Builder Wizard
Launch the wizard from Plugins → Plugin Builder → Plugin Builder. The form is divided into pages; the values you enter map directly onto the generated metadata.txt and class names, so choose them deliberately.
The first page collects identity:
- Class name —
PascalCase, e.g.FieldAreaCalculator. This becomes the main plugin class and the value returned byclassFactory. - Plugin name — the human-readable label shown in the plugin manager and menu, e.g.
Field Area Calculator. - Module name —
snake_case, e.g.field_area_calculator. This becomes the output folder name and the Python module, so it must be a valid identifier with no spaces or hyphens. - Description and About — short and long summaries; both land in
metadata.txt.
The second page asks for the plugin template. Choose "Tool button with dialog" for a classic dialog-based tool, "Tool button with dock widget" for a persistent panel, or "Processing provider" if you are building algorithms. For this walkthrough select Tool button with dialog.
Subsequent pages collect the menu location (Vector, Raster, Database, Web, or a custom plugin menu), text strings, and author details (author, email, the tracker/repository/homepage URLs, and the minimum QGIS version). Set Minimum QGIS version to 3.34 to match the LTR baseline. Finally choose an output directory — generate into a scratch folder, not directly into your live plugins directory, so you can compile resources before deploying.
Step 2: Inspect the Generated File Tree
With module name field_area_calculator, Plugin Builder writes a folder like this:
field_area_calculator/
├── __init__.py
├── metadata.txt
├── field_area_calculator.py # main plugin class
├── field_area_calculator_dialog.py # dialog wrapper class
├── field_area_calculator_dialog_base.ui # Qt Designer layout
├── resources.qrc # Qt resource manifest
├── resources.py # compiled (may need regeneration)
├── icon.png
├── Makefile
├── pb_tool.cfg # pb_tool build config
├── plugin_upload.py
├── pylintrc
├── README.html
├── help/
├── i18n/
│ └── af.ts
├── scripts/
└── test/
The files that matter day to day:
__init__.py— contains theclassFactory(iface)function that imports and returns your main class. QGIS calls this during discovery.metadata.txt— every value you typed in the wizard, now in[general]key/value form. This is the single source of truth the plugin manager reads. The companion page on Write metadata.txt for a QGIS Plugin covers each key in depth.field_area_calculator.py— the main class withinitGui(),unload(),run(), and the helperadd_action(). This is where your toolbar button and menu entry are registered.field_area_calculator_dialog.py— a thinQDialogsubclass that loads the.uifile withuic.loadUiType.field_area_calculator_dialog_base.ui— the editable Qt Designer layout. Open it in Qt Designer to add widgets.resources.qrc— listsicon.png(and any other assets) for the Qt resource system.Makefile/pb_tool.cfg— two alternative build front-ends; you use one or the other to compile and deploy.
Step 3: Compile the Resources
The generated main class imports icons through a compiled resource module (from .resources import *). The .qrc manifest is plain XML; it must be turned into importable Python with pyrcc5. Even though Plugin Builder ships a resources.py, regenerate it so it matches your local PyQt5 version and any icon you swapped in.
Run this from inside the plugin folder:
cd field_area_calculator
pyrcc5 -o resources.py resources.qrc
Breakdown: pyrcc5 reads resources.qrc, base64-encodes every referenced <file> (here, icon.png), and writes a Python module that registers them with Qt's virtual resource filesystem under paths like :/plugins/field_area_calculator/icon.png. Because the icon is embedded, the deployed plugin needs no loose PNG at runtime. If pyrcc5 is not found, install it with pip install pyqt5-tools outside QGIS, or use the OSGeo4W shell on Windows where it is already on the PATH.
If you prefer not to manage the command yourself, pb_tool wraps it. From the plugin folder:
pb_tool compile
Breakdown: pb_tool compile reads pb_tool.cfg, finds every .qrc and .ui listed there, and runs the appropriate compiler for each. It is the more portable option in CI because it does not depend on remembering individual pyrcc5 invocations. Install it once with pip install pb_tool.
Step 4: Deploy to the Profile Plugins Directory
QGIS only loads plugins from a profile's python/plugins directory. Move the compiled folder there. The cleanest approach during development is pb_tool deploy, which copies only the files declared in pb_tool.cfg and skips build artifacts:
pb_tool deploy -y
Breakdown: deploy reads the home_plugin path resolved from your profile and copies the source files plus the freshly compiled resources.py into .../python/plugins/field_area_calculator/. The -y flag skips the interactive confirmation, which is convenient when you redeploy frequently. The Makefile equivalent is make deploy, which depends on pyrcc5 and a Unix-like shell.
If you would rather copy by hand, drop the entire field_area_calculator/ folder into the profile path listed in the prerequisites. Make sure the folder name exactly equals the module name — a mismatch causes an import failure.
Step 5: Enable and Test the Plugin
Open Plugins → Manage and Install Plugins → Installed. Your plugin appears in the list; tick its checkbox to enable it. A new toolbar button and a menu entry under the location you chose in the wizard should appear immediately.
Watch the Python console (Plugins → Python Console) while enabling — any traceback during initGui() prints there. Click the button: the generated run() simply shows the empty dialog. To iterate, edit the source in the profile directory, then use Plugin Reloader (its toolbar button or Ctrl+F5) to reload without restarting QGIS.
Step 6: Customize the Generated Code
The scaffold is a starting point, not a finished plugin. The default run() looks like this:
def run(self):
"""Run method that performs all the real work."""
if self.first_start:
self.first_start = False
self.dlg = FieldAreaCalculatorDialog()
self.dlg.show()
result = self.dlg.exec_()
if result:
pass
Replace the pass block with real logic. The example below reads the active vector layer, sums polygon areas in the layer's CRS units, and reports the total — turning the empty wizard output into a working tool:
from qgis.core import Qgis, QgsWkbTypes
def run(self):
"""Show the dialog and total the polygon areas on confirmation."""
if self.first_start:
self.first_start = False
self.dlg = FieldAreaCalculatorDialog()
self.dlg.show()
if not self.dlg.exec_():
return
layer = self.iface.activeLayer()
if layer is None or layer.geometryType() != QgsWkbTypes.PolygonGeometry:
self.iface.messageBar().pushMessage(
"Field Area Calculator",
"Select a polygon layer first.",
level=Qgis.Warning,
duration=4,
)
return
total = sum(feature.geometry().area() for feature in layer.getFeatures())
self.iface.messageBar().pushMessage(
"Field Area Calculator",
f"Total area: {total:,.2f} {layer.crs().mapUnits()} units",
level=Qgis.Success,
duration=6,
)
Breakdown: self.iface.activeLayer() returns the layer highlighted in the Layers panel. The guard rejects anything that is not a polygon layer via geometryType(). feature.geometry().area() returns the planar area in the layer's CRS units, so the result is only meaningful in a projected CRS — for geodesic measurements you would use QgsDistanceArea. The messageBar() calls surface feedback without a blocking dialog. To add inputs to the dialog itself — for example a layer picker — open field_area_calculator_dialog_base.ui in Qt Designer and follow the toolbar and widget patterns in Add a Toolbar Button to a QGIS Plugin.
QGIS Version Compatibility
Plugin Builder 3 targets the QGIS 3.x / PyQt5 line. The generated code is portable across recent releases, but the resource compiler and a few signal names differ by version.
| Component | QGIS 3.28 LTR (Py 3.9) | QGIS 3.34 LTR (Py 3.12) | QGIS 3.40 / 3.44 (Py 3.12) |
|---|---|---|---|
| Resource compiler | pyrcc5 | pyrcc5 | pyrcc5 |
| Dialog exec | exec_() works | exec_() works | prefer exec() |
qgisMinimumVersion | 3.28 | 3.34 | 3.40 |
| Qt | Qt 5 | Qt 5 | Qt 5 (Qt 6 builds emerging) |
Set qgisMinimumVersion no higher than the oldest QGIS you intend to support. The deprecated exec_() still works on 3.34 but emits a warning on the newest builds; exec() is safe on all 3.x releases.
Troubleshooting
Plugin does not appear after deploying. The folder name must equal the module name and contain __init__.py with a classFactory. Confirm you deployed to the active profile's plugins directory — check Settings → User Profiles for which profile is loaded.
ModuleNotFoundError: No module named '...resources'. You skipped compilation, or resources.py did not deploy. Run pyrcc5 -o resources.py resources.qrc (or pb_tool compile) and redeploy.
Icon shows as a broken placeholder. The .qrc path and the actual file disagree, or resources.py is stale. Open resources.qrc, confirm the <file> entry matches icon.png, recompile, and reload.
Wizard rejects the module name. Plugin Builder requires a valid Python identifier. Use lowercase letters, digits, and underscores only — no hyphens or spaces.
Changes do not take effect. QGIS caches imported modules. Use Plugin Reloader rather than re-ticking the checkbox, which does not always re-import edited files.
Conclusion
Plugin Builder 3 gives you a loadable, conventional plugin in minutes, eliminating the most common boilerplate mistakes. The workflow is consistent: run the wizard, compile the .qrc with pyrcc5 or pb_tool, deploy to your profile, enable, and then replace the placeholder run() with real logic. From here, the natural next steps are wiring richer toolbar actions and building out the dialog interface, both of which build directly on the scaffold you just generated.
Frequently Asked Questions
Do I need Plugin Builder installed to ship the plugin? No. Plugin Builder only generates the source. Once created, the plugin is self-contained and your users never need Plugin Builder — they just install your packaged zip.
Should I commit resources.py to version control? It is generated, so many developers gitignore it and compile in CI with pb_tool compile. If your contributors lack pyrcc5, committing it is a pragmatic convenience.
What is the difference between the Makefile and pb_tool? Both compile resources and deploy. The Makefile assumes a Unix shell and make; pb_tool is a cross-platform Python CLI driven by pb_tool.cfg, which makes it the friendlier choice on Windows and in CI.
Can I change the menu location after generation? Yes. The menu is set in initGui() via self.iface.addPluginToMenu (or addPluginToVectorMenu, etc.). Edit that call rather than regenerating the whole plugin.