Forschen

DBus Dienste mit Qt6 nutzen

In diesem Artikel wollen wir etwas Licht in das Thema “Qt + DBus” bringen, bei dem die Dokumentation relativ dünn ist bzw. an diversen Stellen zusammengesucht werden muss.

Als Beispiel nutzen wir die org.freedesktop.Notifications-API, um auf einem Linux-Desktop eine einfache Desktop-Notification von QML aus zu senden. Es ist bewusst einfach gehalten - die Notification-API kann natürlich deutlich mehr als nur Benachrichtigungen mit Titel und Nachrichtentext zu senden.

Vorbereitung

Zunächst erstellen wir ein minimales QML-Projekt. Dazu kann der Qt Creator hilfreich sein. Folgende Dateien sind notwendig:

CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)

project(notifytest VERSION 0.1 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 6.5 REQUIRED COMPONENTS Quick)

qt_standard_project_setup(REQUIRES 6.5)

qt_add_executable(appnotifytest
    main.cpp
)

qt_add_qml_module(appnotifytest
    URI notifytest
    VERSION 1.0
    QML_FILES
        Main.qml
)

# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
set_target_properties(appnotifytest PROPERTIES
#    MACOSX_BUNDLE_GUI_IDENTIFIER com.example.appnotifytest
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

target_link_libraries(appnotifytest
    PRIVATE Qt6::Quick Qt6::DBus
)

include(GNUInstallDirs)
install(TARGETS appnotifytest
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);
    engine.loadFromModule("notifytest", "Main");

    return app.exec();
}

Main.qml

import QtQuick

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Notification test")
}

Das obige Beispiel entspricht der Vorlage aus dem Qt Creator. Zum Testen kann das Projekt einfach in der Entwicklungsumgebung ausgeführt oder direkt in einer Shell kompiliert werden. Z. B. so:

$ cd notifytest
$ ls
CMakeLists.txt
main.cpp
Main.qml
$ mkdir build && cd build
$ ~/Qt/6.6.1/gcc_64/bin/qt-cmake -GNinja ..
$ ninja

Nun sollte eine Datei notifytest ausführbar sein, welche die initiale Version startet.

Benachrichtigung hinzufügen

Der einfachste Weg, eine minimale Qt-Anwendung dazu zu bringen, eine Nachricht über den Benachrichtungsmechanismus des Desktops zu versenden, ist im Prinzip die Verwendung der Notifications-API von freedesktop.org über DBus.

API-Spec beziehen

Zunächst benötigen wir die Beschreibungsdatei für die API, die wir uns z. B. aus der GNOME-Shell holen und in einem Verzeichnis names spec ablegen. Dies ist die Datei org.freedesktop.Notifications.xml.

CMake-Integration

Um von Qt aus ohne viel Aufwand auf diese API zugreifen zu können, kann CMake angewiesen werden, ein entsprechendes Interface zu generieren. Dies kann über den folgenden Aufruf in unserer CMakeLists.txt erfolgen:

# Hinzufügen der DBus Abhängigkeit
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick DBus)

# Erstellen des Interfaces
set(NOTIFY_SOURCES)
qt_add_dbus_interface(NOTIFY_SOURCES specs/org.freedesktop.Notifications.xml notifyinterface)

# Hinzufügen der Interface-Dateien zu unserem QML-Modul
qt_add_qml_module(appnotifytest
    [...]
    SOURCES
        ${NOTIFY_SOURCES}
)

# Hinzufügen des Build-Verzeichnisses für Include-Dateien
target_include_directories(appnotifytest PRIVATE ${CMAKE_BINARY_DIR})

# Hinzufügen der DBus Abhängigkeit für den Linker
target_link_libraries(appnotifytest
    PRIVATE Qt6::Quick Qt6::DBus
)

Das Kommando qt_add_dbus_interface erzeugt zwei Dateien (notifyinterface.h und notifyinterface.cpp) im Build-Verzeichnis. Wie genau sie heißen sollen, kann über den dritten Parameter eingestellt werden. Der erste Parameter ist eine Variable, die nach dem Aufruf die Pfade der erzeugten Dateien enthält, der zweite enthält den Pfad zu unserer Spezifikationsdatei.

Beim nächsten Build-Versuch erhalten wir allerdings eine Fehlermeldung:

qdbusxml2cpp: Got unknown type `a{sv}' processing '/home/prcs1076/tmp/notifytest/specs/org.freedesktop.Notifications.xml'
You should add <annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="<type>"/> to the XML description for ''

Diese besagt, dass das Tool qdbusxml2cpp nicht weiß, wie es den sechsten Input-Parameter von Notify mit der Signatur a{sv} generieren soll. Wie das Problem zu lösen ist, steht zumindest in Teilen schon in der Fehlermeldung. Es fehlt nur der konkrete Typ. In diesem Fall kann die DBus Notation für a{sv} (= Hash mit String Index und Variant als Wert) durch eine QVariantMap abgebildet werden.

Die Lösung des Problems ist also das Modifizieren der Spezifikationsdatei:

   [...]
   <method name="Notify">
      <annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/>
      <arg type="s" direction="in"/>
      <arg type="u" direction="in"/>
   [...]

Ein erneutes Bauen des Projektes schlägt nicht fehl.

Interface verwenden

Im nächsten Schritt möchten wir das Interface von QML aus verwenden und über einen Button die Benachrichtigung auslösen.

Zum Exportieren einer Funktion benötigen wir zunächst eine kleine Hilfsklasse:

helper.h:

#pragma once
#include<QObject>
#include <QQmlEngine>

class Helper : public QObject {
    Q_OBJECT
    QML_SINGLETON
    QML_ELEMENT

public:
    Helper(QObject* parent = nullptr) : QObject(parent) {}

    Q_INVOKABLE void notify(const QString& title, const QString& body, unsigned duration = 5000);

private:
    Q_DISABLE_COPY(Helper)
};

helper.cpp

#include <QDBusConnection>
#include "helper.h"
#include "notifyinterface.h"

void Helper::notify(const QString& title, const QString& body, unsigned duration) {
    OrgFreedesktopNotificationsInterface ni(
        "org.freedesktop.Notifications",
        "/org/freedesktop/Notifications",
        QDBusConnection::sessionBus(),
        this);

    ni.Notify("", 0, "", title, body, QStringList(), QVariantMap(), duration);
}

Diese versteckt den Aufruf der Notification-API in einer mittels Q_INVOKABLE exportierten Methode. Über QML_SINGLETON und QML_ELEMENT steht uns in diesem Beispiel nun ein QML-Objekt mit gleichem Namen (Helper) zur Verfügung.

Damit unsere Hilfsklasse auch kompiliert wird, muss sie natürlich auch noch in der CMakeLists.txt hinterlegt werden:

# Hinzufügen der Interface-Dateien zu unserem QML-Modul
qt_add_qml_module(appnotifytest
    [...]
    SOURCES
        helper.cpp
        helper.h
        ${NOTIFY_SOURCES}
)

Verwendung in QML

Unsere zugegebenerweise recht rudimentäre Main.qml muss nun noch etwas aufgebohrt werden. Wir benötigen einen Button und den Aufruf unserer Helper-Funktion:

import QtQuick
import QtQuick.Controls.Material // Für den Buttton
import notifytest                // Zugriff auf unser Modul

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Notification test")

    Button {
        anchors.centerIn: parent
        text: qsTr("Notify me!")

        // Letztendlich der Aufruf über unser exportiertes Helper Singleton
        onClicked: () => Helper.notify(qsTr("Hey!"), qsTr("That's a message from Qt..."))
    }
}

Sofern nun beim Kompilieren alles glatt geht: herzlichen Glückwunsch! Damit haben wir unser minimales Beispiel zur Kommunikation mit einem DBus-Service fertig. Also fast. Es fehlt noch die Übersetzung …

Übersetzung hinzufügen

Auch hier hilft uns Qt über ein CMake-Kommando:

# Übersetzungstools hinzufügen
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick DBus LinguistTools)

[...]

qt_add_translations(appnotifytest
    RESOURCE_PREFIX /i18n
    TS_FILES
        i18n/notifytest_de.ts
)

Ein erneuter CMake-Aufruf erzeugt eine Vorlage für unsere deutsche Übersetzung:

$ ~/Qt/6.6.1/gcc_64/bin/qt-cmake -GNinja ..
$ ninja update_translations

Die erzeugte Datei liegt wie angegeben unter i18n/notifytest_de.ts im Hauptverzeichnis unseres Quelltextes und kann z. B. mit dem Qt Linguist oder einem Texteditor bearbeitet werden:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE" sourcelanguage="de_DE">
<context>
    <name>Main</name>
    <message>
        <location filename="../Main.qml" line="9"/>
        <source>Notification test</source>
        <translation>Benachrichtigungs-Test</translation>
    </message>
    <message>
        <location filename="../Main.qml" line="13"/>
        <source>Notify me!</source>
        <translation>Sende Benachrichtigung!</translation>
    </message>
    <message>
        <location filename="../Main.qml" line="14"/>
        <source>Hey!</source>
        <translation>Hey!</translation>
    </message>
    <message>
        <location filename="../Main.qml" line="14"/>
        <source>That&apos;s a message from Qt...</source>
        <translation>Ein Nachricht von Qt...</translation>
    </message>
</context>
</TS>

Leider war es das noch nicht ganz. Wir benötigen noch eine kleine Modifikation an der main.cpp, damit die Übersetzungen auch benutzt werden:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QTranslator>

using namespace Qt::Literals::StringLiterals;

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QTranslator translator;
    if (translator.load(QLocale(), "notifytest"_L1, "_"_L1, ":/i18n"_L1)) {
        QCoreApplication::installTranslator(&translator);
    }

    QQmlApplicationEngine engine;
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);
    engine.loadFromModule("notifytest", "Main");

    return app.exec();
}

Damit ist unser Beispiel nun komplett.

×
×

Nehmen Sie Kontakt zu uns auf!

Mit * gekennzeichnete Felder sind Pflichtfelder.

Wir haben Ihre Kontaktanfrage erhalten und melden uns kurzfristig bei Ihnen!

×

Ich möchte digital unabhängig werden!

Vielen Dank für Ihre Mitteilung!