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'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.