How to make DBUS calls on Linux from Qt (to make screenshots on Wayland)

DBUS is a relatively new interprocess communication system on Linux. As with any IPC, it's purpose is to allow applications to talk with each other in a simple and standardized way.

It was originally designed in 2006 in RedHat, and became a part of a freedesktop.org project. DBUS was mainly aimed for various components of KDE and Gnome to communicate with each other and with these desktop environments in general.

Problem with taking screenshots programmatically on modern Linux distros

I needed to periodically retrieve screenshots from a Qt application. Basic researched showed, that while on older (X11/Xorg based distros) one can take screenshots simply by doing something like:

QScreen *screen = QGuiApplication::primaryScreen();
QPixmap pixmap = screen->grabWindow(0);

on newer Wayland-based distros (like, Ubuntu 22) all we get is a black image. The system doesn't let us grab the screen easily. This is because Wayland claims to be user privacy-concerned, and letting the app grab the screen at random moments seems like a privacy violation.

Interestingly, Windows 11 still allows random apps take screenshots whenever they please, while modern versions of macOS present a "would you like to allow this app to take screenshots?" UI to the user automatically, without any extra actions needed from the developer.

So, on Wayland we first need to request the system's permission to take a screenshot, which makes the system render a system UI dialog asking the user if he wants to share a screenshot with the app. It looks something like this:

system screenshot prompt

Then the user chooses whether and what exactly he wants to share with the app, clicks "Share" and the app is being sent a DBUS signal with the path to the file with the screenshot. Quite tedious from the developer's perspective (unlike, for example, macOS).

Some DBUS theory

So to ask the system something and get back response we can utilize DBUS IPC system. It basically contains a system process (daemon), whom all the conversationalists are talking to, and which routes messages to their destination apps.

technically, there's a possibility for processes to talk directly to each other. Also, there are actually two kids of DBUS daemons - one system-wide, and one per each user session.

From the Qt standpoint we don't care about the details of the implementation, this is all abstracted. The apps can make calls (invoke methods with params of certain basic types, like int, structs, dicts, strings, etc) to other apps (via a dbus daemon, but this happens transparently), and subscribe to and receive signals (which are almost like Qt signals, and in fact, are implemented in Qt as Qt signals).

There's a few things that we need to know first.

An app, that wants to be talked to, registers a "dbus service" (which looks like a domain name, for example, org.freedesktop.PowerManagement). The service can contain multiple objects, that are addressed by an "object path" (looks like a unix path, for example, /org/freedesktop/PowerManagement/Statistics). These object paths are supposed to mimic a kind of an OOP object tree, with its inheritance, but from developer's standpoint this does not matter - an object name is just a fancy string with slashes. Finally, each "dbus object" can have several interfaces (whose names look like domain names as well, and sometimes - but not necessarily - can coincide with the service name, thus making confusion), and each interface can expose a bunch of methods that others can call, properties that other apps can access and/or modify, and can emit signals (that other apps can subscribe to).

A few experiments

First of all, we can explore what kind of services and objects (with their methods, properties and signals) are registered by other apps in the system. To do that, there is a tool called "DBUS Viewer" that comes with a default installation of Qt.

Next, there's command line utilities, that help us send dbus messages to various objects even without writing code. For example, we can request a list of all registered dbus services like so:

dbus-send --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.ListNames

Similarly, if we add --system, we'll get a list of all "system" dbus services (registered with the system-wide dbus daemon rather than the user-session one).

Note the format of the command:

dbus-send --system --print-reply --dest=SERVICE_NAME OBJECT_PATH INTERFACENAME.METHODNAME [method params, if any]

Let's for example lock the screen (if we're running Gnome):

dbus-send --type=method_call --dest=org.gnome.ScreenSaver /org/gnome/ScreenSaver org.gnome.ScreenSaver.Lock

Also, we can monitor dbus signals (events), emitted by some objects. For example, let's try to receive notifications, whenever network connectivity changes:

dbus-monitor --system "interface='org.freedesktop.NetworkManager'"

Now, try turning off/on wifi or LAN cable and see the printout in terminal.

Qt dev packages comes with a similar qdbus command line tool, which allows exploring what methods are available, calling those methods and so on.

DBUS in Qt

Making DBUS calls is pretty straightforward in Qt. In fact, dbus nicely fits the Qt object model with its idea of Signals/Slots.

Let's do something similar we've done above from a Qt application. Add "dbus" module in .pro file:

QT += dbus

and let's make a very simple app that makes a call to a dbus interface:

    QDBusConnection bus = QDBusConnection::sessionBus();

    QDBusInterface *i = new QDBusInterface("org.gnome.ScreenSaver", "/org/gnome/ScreenSaver", "org.gnome.ScreenSaver", bus, NULL);
    QDBusReply<QDBusObjectPath> repl = i->call("Lock");

Now, whenever we run this code, screen locks.

XDG (freedesktop) Portals

Aforementioned freedesktop.org organization created a series of DBUS interfaces for apps to request system's permission to do certain things. This mechanism was mainly introduced for sandboxed applications (for apps that are distributed as Flatpaks), since those aren't normally allowed to access arbitrary files on the user's filesystem, or have access to the Camera or other devices - these things must be done "through portals" - i.e., by having the system first explicitly ask user's permission to allow such action for our app.

However, some things now need to be requested through these interfaces for regular non-sandboxed apps as well, like taking screenshots (or screen recording) on Wayland.

How do these portals work?

There is a dbus service called "org.freedesktop.portal.Desktop" with an object called "/org/freedesktop/portal/desktop", and it exports a bunch of interfaces to request user permission to perform certain action. For example, we can make a call to an interface "org.freedesktop.portal.ScreenCast", which will eventually present to the user this dialog:

permission to record screen

and if the user clicks "Share" button - our app will receive a signal with a handler that we can feed to Linux screen capturing APIs (Pipewire). It actually is a bit more complicated than this - there are several dbus calls involved.

Let's see how we can take a screenshot, for example.

Taking a screenshot

The process, as described in the official documentation, is as the following.

We call a Screenshot method of the org.freedesktop.portal.Screenshot interface (that is exported by the /org/freedesktop/portal/desktop object of the org.freedesktop.portal.Desktop service of the user session). To this call we can pass some arguments, as described in the official doc:

Screenshot (IN  s     parent_window,
                     IN  a{sv} options,
                     OUT o     handle);

Here we need to pass a string (marked as "s" dbus type signature) - parent window's wayland identifier (so the system will make sure that the "Ask permission" system dialog is rendered on top of our window), it can be an empty string for simplicity. Also we need to pass a dictionary (see this a{sv} signature? it basically means "array of string-variant" pairs, which is a fancy dbus name of a regular dictionary with QStrings as keys and QVariants as values, i.e. QVariantMap). You can read about dbus type signatures for example here.

What can be inside this options dict? for example, we can specify, whether we want the permission dialog to allow the user to pick what kind of screenshot he wants to share with the requesting app (of a desktop, of just a window, etc):

    QDBusConnection bus = QDBusConnection::sessionBus();

    QDBusInterface *i = new QDBusInterface("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Screenshot", bus, NULL);

    QVariantMap options;
    options["interactive"] = false;

    QDBusReply<QDBusObjectPath> repl = i->call("Screenshot", "", options);

We got a reply, but it is not a screenshot yet. Our "Screenshot" call just asks the system to present a "Would you like to share a screenshot with the app?" dialog, and returns immediately. So repl just contains a path to another (temporary) dbus object. We can (and should) use this object to subscribe to a signal called "Response". This signal will be called if the user clicks the "Allow" (or "Share") button in this system dialog. Then this signal will let us know the file url of the image file, that we can access from our app:

    if(repl.isValid()) {
        bus.connect("", repl.value().path(), "org.freedesktop.portal.Request", "Response", this, SLOT(HandleResponse(uint, QVariantMap)));
    } else {
        qDebug() << "Something is wrong: " << repl.error();
    }

This "HandleResponse" slot of our object will be invoked with two arguments: the first one indicates whether the user clicked "Allow" or "Deny", while the second argument should have a single key "uri" with the path to the image file:

// this is a slot somewhere in our class
void HandleResponse(uint responseCode, QVariantMap results) {
        if(responseCode == 0) {
            qDebug() << "User allowed us to take a screenshot! We can get it from " << results["uri"];
        } else {
            qDebug() << "Unable to take a screenshot";
        }
}

Now we can read the image file into QImage and do something with it.

← Back to Articles