Add a Toolbar Button to a QGIS Plugin

A toolbar button is the most direct way for users to reach your plugin. In PyQGIS it is a QAction carrying a QIcon, registered with the QGIS interface during initGui(), connected to a callback through its triggered signal, and — critically — removed again during unload(). Getting the lifecycle symmetric is what separates a plugin that disables cleanly from one that leaves ghost buttons behind. This page shows the full pattern: a single icon on the main plugin toolbar, the same action mirrored into a plugin menu, and an optional dedicated QToolBar for plugins that expose several tools.

This recipe is part of Plugin Boilerplate & Structure. If you have not yet scaffolded a plugin, generate one first with Create a QGIS Plugin with Plugin Builder 3 — the code below drops straight into the main class it produces.

Prerequisites

  • QGIS 3.34 LTR (Python 3.12) with the Python console enabled
  • An existing plugin skeleton exposing a classFactory and a main class that receives iface
  • An icon file (PNG or SVG) reachable from your plugin directory, or compiled into a .qrc
  • Familiarity with Qt's signal/slot mechanism — buttons fire triggered, you connect a slot

How the Interface Registers Buttons

The iface object (a QgisInterface) is your single entry point. It offers two relevant placements: a slot on the shared Plugins toolbar via addToolBarIcon, and a plugin menu entry via addPluginToMenu. For multiple related actions you can instead create your own QToolBar with addToolBar. The diagram below shows what each call adds and what must be torn down.

QAction lifecycle across initGui and unload initGui creates a QAction with a QIcon and registers it via addToolBarIcon and addPluginToMenu; unload reverses each call with removeToolBarIcon and removePluginMenu. initGui() unload() QAction(QIcon, text) addToolBarIcon(action) addPluginToMenu(menu, action) triggered.connect(run) removeToolBarIcon(action) removePluginMenu(menu, action)

Recipe 1: A Single Button on the Plugins Toolbar

The simplest case adds one icon to the shared QGIS Plugins toolbar and the same command to a plugin menu. Store the QAction so unload() can find it later.

import os
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction
from qgis.core import Qgis


class BufferTool:
    def __init__(self, iface):
        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)
        self.menu = "&Buffer Tool"
        self.actions = []

    def initGui(self):
        icon = QIcon(os.path.join(self.plugin_dir, "icons", "buffer.svg"))
        action = QAction(icon, "Buffer active layer", self.iface.mainWindow())
        action.setStatusTip("Create a buffer around the active vector layer")
        action.triggered.connect(self.run)

        self.iface.addToolBarIcon(action)
        self.iface.addPluginToMenu(self.menu, action)
        self.actions.append(action)

    def unload(self):
        for action in self.actions:
            self.iface.removeToolBarIcon(action)
            self.iface.removePluginMenu(self.menu, action)
        self.actions.clear()

    def run(self):
        layer = self.iface.activeLayer()
        if layer is None:
            self.iface.messageBar().pushMessage(
                "Buffer Tool", "Select a layer first.",
                level=Qgis.Warning, duration=4,
            )
            return
        self.iface.messageBar().pushMessage(
            "Buffer Tool", f"Buffering {layer.name()}...",
            level=Qgis.Info, duration=3,
        )

Breakdown: QAction(icon, text, parent) builds the clickable command; passing self.iface.mainWindow() as the parent ties its Qt lifetime to the main window. triggered.connect(self.run) fires run() on every click. addToolBarIcon places it on the shared toolbar; addPluginToMenu mirrors it into a named submenu under Plugins. Because the action is kept in self.actions, unload() can reverse both registrations — removeToolBarIcon and removePluginMenu must each be called, or the orphaned widget lingers after the plugin is disabled.

Recipe 2: A Dedicated Custom Toolbar

When a plugin ships several related tools, a dedicated QToolBar groups them and lets users dock or hide the set as a unit. Create it once in initGui() and destroy it in unload().

import os
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction


class GeometryToolkit:
    def __init__(self, iface):
        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)
        self.menu = "&Geometry Toolkit"
        self.actions = []
        self.toolbar = None

    def _make_action(self, icon_name, text, callback):
        icon = QIcon(os.path.join(self.plugin_dir, "icons", icon_name))
        action = QAction(icon, text, self.iface.mainWindow())
        action.triggered.connect(callback)
        self.toolbar.addAction(action)
        self.iface.addPluginToMenu(self.menu, action)
        self.actions.append(action)
        return action

    def initGui(self):
        self.toolbar = self.iface.addToolBar("Geometry Toolkit")
        self.toolbar.setObjectName("GeometryToolkit")  # required for state persistence
        self._make_action("centroid.svg", "Centroids", self.run_centroids)
        self._make_action("simplify.svg", "Simplify", self.run_simplify)

    def unload(self):
        for action in self.actions:
            self.iface.removePluginMenu(self.menu, action)
            self.toolbar.removeAction(action)
        self.actions.clear()
        if self.toolbar is not None:
            del self.toolbar  # removes the toolbar widget from the main window
            self.toolbar = None

    def run_centroids(self):
        self.iface.messageBar().pushMessage("Toolkit", "Centroids tool", duration=2)

    def run_simplify(self):
        self.iface.messageBar().pushMessage("Toolkit", "Simplify tool", duration=2)

Breakdown: self.iface.addToolBar(name) returns a QToolBar already attached to the main window. Setting setObjectName is not cosmetic — QGIS uses the object name to save and restore toolbar position between sessions, and omitting it triggers a Qt warning. Each action is added to this toolbar via self.toolbar.addAction() rather than addToolBarIcon. In unload(), del self.toolbar releases the last Python reference so Qt removes the empty toolbar; without it, an empty toolbar remains after the plugin unloads.

Toggling, Enabling, and Checkable Buttons

A button that controls a persistent state — say, a map tool that stays active — should be checkable so its pressed state reflects whether the tool is engaged. You can also enable or disable a button reactively based on the active layer.

def initGui(self):
    icon = QIcon(os.path.join(self.plugin_dir, "icons", "measure.svg"))
    self.action = QAction(icon, "Measure mode", self.iface.mainWindow())
    self.action.setCheckable(True)               # button can stay pressed
    self.action.toggled.connect(self.set_mode)   # fires with True/False
    self.iface.addToolBarIcon(self.action)
    self.actions.append(self.action)

    # React to layer changes: only enable for vector layers
    self.iface.currentLayerChanged.connect(self._update_enabled)
    self._update_enabled(self.iface.activeLayer())

def _update_enabled(self, layer):
    from qgis.core import QgsVectorLayer
    self.action.setEnabled(isinstance(layer, QgsVectorLayer))

def set_mode(self, checked):
    state = "on" if checked else "off"
    self.iface.messageBar().pushMessage("Measure", f"Mode {state}", duration=2)

Breakdown: setCheckable(True) lets the action hold a pressed/unpressed state; the toggled signal then delivers the new boolean to your slot, which is more useful than triggered for stateful tools. Connecting to iface.currentLayerChanged lets the button grey itself out when the active layer is not a vector — disconnect this signal in unload() (self.iface.currentLayerChanged.disconnect(self._update_enabled)) so the slot does not fire after teardown. For a more elaborate stateful interface, pair the button with a panel as shown in Add a Custom Dock Widget in PyQGIS.

QGIS Version Compatibility

The QAction API and the iface toolbar/menu methods have been stable across the entire 3.x line, so this code runs unchanged on every modern QGIS. Only peripheral details vary.

AspectQGIS 3.28 LTR (Py 3.9)QGIS 3.34 LTR (Py 3.12)QGIS 3.40 / 3.44 (Py 3.12)
addToolBarIcon / removeToolBarIconstablestablestable
addPluginToMenu / removePluginMenustablestablestable
iface.currentLayerChangedavailableavailableavailable
Icon formatPNG / SVGPNG / SVGPNG / SVG

Prefer SVG icons: they stay crisp on high-DPI displays, where PNGs at fixed pixel sizes can look soft.

Troubleshooting

Button stays visible after disabling the plugin. unload() is not reversing every registration. For each action call both removeToolBarIcon and removePluginMenu; for a custom toolbar also release it with del self.toolbar.

AttributeError on addToolBarIcon. You called it on the wrong object. The method belongs to iface, not to your plugin class — use self.iface.addToolBarIcon(action).

Qt warning about a missing object name. Set toolbar.setObjectName("...") immediately after addToolBar, otherwise QGIS cannot persist its position and logs a warning.

Clicking the button does nothing. The triggered connection is missing or points at an unbound method. Connect to a bound method (self.run) and confirm no exception is swallowed — check the Python console.

Button does not disable for non-vector layers. Ensure _update_enabled is connected to currentLayerChanged and called once during initGui() with the current layer, since the signal only fires on subsequent changes.

Conclusion

A QGIS toolbar button is a QAction plus disciplined lifecycle management. Build the action with a QIcon, register it through addToolBarIcon and addPluginToMenu (or onto a dedicated QToolBar), wire triggered to your callback, and mirror every registration with a matching removal in unload(). Keep your actions in a list, set object names on custom toolbars, and disconnect any interface signals you subscribed to. That symmetry is the whole game — get it right and your plugin loads and unloads without leaving a trace.

Frequently Asked Questions

What is the difference between addToolBarIcon and a custom QToolBar? addToolBarIcon drops your action onto the shared QGIS Plugins toolbar — fine for one button. A custom QToolBar from iface.addToolBar gives your plugin its own dockable, hideable bar, which suits a suite of related tools.

Do I have to add the action to a menu as well? No, but it is good practice. Toolbars can be hidden, so a menu entry guarantees users can still reach the command.

Why store actions in a list? unload() needs a reference to every action to remove it. A list makes teardown a simple loop and prevents forgotten orphans.

Should I disconnect signals in unload()? Yes, for any signal you connected to iface (such as currentLayerChanged). Signals on the action itself are cleaned up with the action, but interface-level connections outlive it and will fire into a dead slot.