Show Messages with QgsMessageBar in PyQGIS
The message bar is the strip that appears at the top of the map canvas, and it is the right place for almost everything a plugin needs to tell a user. It does not steal focus, it does not have to be dismissed, it stacks, and it can carry a button. A modal dialog does none of that, and a plugin that uses one for every notification is a plugin people stop using.
This recipe belongs to Qt Designer for GIS Interfaces. It covers the levels and durations, adding a button or a whole widget, showing progress, logging alongside, and the cases where a dialog really is the right answer.
Prerequisites
- QGIS 3.34 LTR or newer with a GUI.
- Access to
iface, as covered in using iface to control the QGIS interface.
The basic call
from qgis.core import Qgis
bar = iface.messageBar()
bar.pushMessage(
"Export complete",
"12 sheets written to /data/output",
level=Qgis.MessageLevel.Success,
duration=6,
)
Breakdown: The two strings are a title and a body; the title is rendered bold and should be short enough to read at a glance, with the detail in the body. duration is in seconds and 0 means the message stays until the user closes it — reserve that for critical messages, because a bar that accumulates undismissed notices covers the map. The convenience methods pushInfo, pushSuccess, pushWarning and pushCritical take just the two strings and pick sensible defaults, and they are what most code should use.
On QGIS 3.28 and earlier the level constants are Qgis.Success and friends without the scoped enum; both spellings work on current releases.
Adding a button
The message bar's real advantage over a status message is that it can offer an action.
from qgis.PyQt.QtWidgets import QPushButton
widget = bar.createMessage("Export complete", "12 sheets written")
button = QPushButton("Open folder")
button.pressed.connect(lambda: open_output_folder())
widget.layout().addWidget(button)
bar.pushWidget(widget, Qgis.MessageLevel.Success, duration=10)
Breakdown: createMessage builds the bar item without showing it, so you can add widgets before pushing. Any Qt widget can go into that layout — a button, a combo box, a small progress bar — which is how the built-in "layer added, zoom to it?" style prompts work. Keeping the button's action short matters: the bar disappears after its duration, taking the button with it, so an action nobody performs in ten seconds is better placed in a dialog or a panel.
Note the closure: a lambda connected here captures whatever it references, and if it references self on a plugin that is later unloaded, the widget outlives the plugin. Connecting to a bound method of an object you keep is safer than capturing arbitrary state.
Progress in the bar
from qgis.PyQt.QtWidgets import QProgressBar
from qgis.PyQt.QtCore import Qt
class Exporter:
def start(self, total):
self.widget = iface.messageBar().createMessage("Exporting…")
self.progress = QProgressBar()
self.progress.setMaximum(total)
self.progress.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.widget.layout().addWidget(self.progress)
iface.messageBar().pushWidget(self.widget, Qgis.MessageLevel.Info)
def advance(self, value):
self.progress.setValue(value)
def finish(self, ok, detail):
iface.messageBar().clearWidgets()
if ok:
iface.messageBar().pushSuccess("Export complete", detail)
else:
iface.messageBar().pushCritical("Export failed", detail)
Breakdown: Pushing with no duration argument means the message stays, which is what a progress indicator needs. clearWidgets() removes everything currently in the bar before the outcome is pushed — without it the progress message sits underneath the result until it is dismissed, which looks like the task is still running. Keeping the widget and the progress bar on the instance is required for the same reason every canvas object is: a local goes out of scope and is collected while Qt still refers to it.
This only works if the work yields to the event loop. A tight Python loop blocks repainting, so the bar freezes at zero — which is precisely the problem QgsTask exists to solve, as described in running a background task with QgsTask.
Log alongside, do not choose
A message bar notice is transient by design. Anything worth investigating later belongs in the log as well.
from qgis.core import QgsMessageLog
def report(title, detail, level=Qgis.MessageLevel.Info):
iface.messageBar().pushMessage(title, detail, level=level, duration=6)
QgsMessageLog.logMessage(f"{title}: {detail}", "MyPlugin", level=level)
Breakdown: One helper doing both means a user sees the notice now and a support request can be answered from the log later. QgsMessageLog takes a tag — use your plugin's name consistently, because the log panel groups by it and a plugin that logs under several tags is much harder to follow. The log is also the only channel that works headless, so a helper like this is what lets the same code path serve an interactive plugin and a batch script, with iface guarded as described in the iface recipe.
Writing messages people can act on
The mechanics are easy; the wording is where most plugins fall down. Three rules cover it.
Name what happened, not what the code did. "Export failed" tells a user something; "QgsVectorFileWriter returned error code 3" tells them nothing they can use. Keep the technical detail for the log, where it belongs and where it will still be available tomorrow.
Say what to do next when there is something to do. "Could not write to /data/output — the folder is read-only" is a message a user can act on. "Could not write output" is a message they can only report. The difference is one clause and it removes a support round trip.
Count things. "3 features skipped: invalid geometry" is far more useful than "some features were skipped", because a user can tell at a glance whether three out of four thousand matters. Where a count is zero, consider not showing the message at all — a success notice for an operation that did nothing is noise.
skipped = [f.id() for f in features if not f.geometry().isGeosValid()]
if skipped:
report(
f"{len(skipped)} features skipped",
"Their geometries are invalid — run Fix Geometries and try again.",
Qgis.MessageLevel.Warning,
)
Breakdown: Building the message from the actual count rather than from a fixed string is what keeps it honest across runs, and the second sentence names the remedy — in this case fixing invalid geometries, which is a real action the user can take without asking anyone. Guarding the whole thing on a non-empty list means a clean run says nothing, which is the correct amount to say.
When a dialog is right instead
Three cases genuinely need to block. A destructive action needs confirmation before it happens — deleting features, overwriting a file, committing a change that cannot be undone. A choice the code cannot proceed without needs an answer. And a long-form result — a validation report with forty lines — needs somewhere scrollable, which the bar is not.
Everything else is a message bar item. The test is simple: if the user could reasonably keep working without reading it, it does not deserve a dialog.
QGIS version compatibility
QgsMessageBar and the pushMessage/pushWidget API have been present since QGIS 2.x and are unchanged in 3.x. The Qgis.MessageLevel enum moved into the scoped namespace in 3.30, with the older Qgis.Info spellings retained. pushSuccess, pushWarning, pushCritical and pushInfo were added in 3.4; on 3.0 use pushMessage with an explicit level.
Troubleshooting
- The message never appears.
ifaceisNone, or the code is running before the interface exists. - The progress bar never moves. The work is blocking the event loop; move it to a task.
- Two messages are stacked. The earlier one had no duration and was not cleared.
- The button does nothing. The connection captured an object that was collected — connect to a bound method of something you keep.
- The message is unreadable at a glance. The title is carrying the whole sentence; put the detail in the body.
- Nothing is visible in a headless run. Expected. Use
QgsMessageLog, orprint.
Conclusion
Use the bar for everything the user does not have to answer, pick the level to match the meaning and the duration to match the importance, clear a progress message before pushing its outcome, and log everything you show. Reserve dialogs for confirmations and questions. That is most of what makes a plugin feel considerate rather than intrusive.
Frequently Asked Questions
Can I show a message from a background thread? Not directly — Qt widgets must be touched from the main thread. Emit a signal from the task and push the message in the slot, which runs on the main thread.
How do I dismiss a specific message?
Keep the widget returned by createMessage and pass it to popWidget(widget). clearWidgets() removes all of them.
Is there a message bar in a dialog?
Yes — a QgsMessageBar can be added to any dialog's layout, which is the tidy way to report validation errors in a form, as covered in validating plugin dialog input.
Does the bar support rich text? The body accepts basic HTML, including links, which is a good way to point at a log file or a documentation page without a dialog.