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.

Four levels, four meaningsInfo carries neutral progress notes and should disappear quickly. Success confirms a completed action and can be brief. Warning marks something the user should know but need not act on. Critical marks a failure and should stay until dismissed so it is not missed.Level sets the colour; duration sets whether it is missedInfo"Loaded 412 features from roads.gpkg"duration 4–5 sneutral, disposableSuccess"Export complete — 12 sheets written"duration 5–8 sconfirms the action finishedWarning"3 features skipped: invalid geometry"duration 10 s or 0worth reading, not blockingCritical"Could not write to /data/output — read only"duration 0stays until dismissedduration 0 means the user must dismiss it

Prerequisites

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

One message, three statesA long task pushes a message carrying a progress bar and no timeout. As the task runs the bar advances. When the task finishes the message is cleared and replaced with a success or failure notice that does time out.Replace the progress message; never leave it behindExporting…duration 0Export complete — 12 sheets writtenduration 6Export failed — see the log for detailsduration 0clearWidgets() before pushing the outcome, or both are visible at once

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. iface is None, 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, or print.

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.