Benutzerdefinierte Shell

Custom Shell zeigt, wie man eine eigene Shell-Erweiterung implementiert.

Shell-Erweiterungen für Wayland sind Protokolle, die den Zustand, die Position und die Größe von Fenstern verwalten. Die meisten Compositors unterstützen eine oder mehrere der eingebauten Erweiterungen, aber unter bestimmten Umständen kann es nützlich sein, eine eigene zu schreiben, die genau die Funktionen enthält, die Ihre Anwendungen benötigen.

Dies erfordert, dass Sie die Shell-Erweiterung sowohl auf der Server- als auch auf der Client-Seite der Wayland-Verbindung implementieren, so dass sie hauptsächlich dann nützlich ist, wenn Sie eine Plattform aufbauen und sowohl den Compositor als auch die Client-Anwendungen kontrollieren.

Das Beispiel Custom Shell zeigt die Implementierung einer einfachen Shell-Erweiterung. Es ist in drei Teile unterteilt:

  • Eine Protokollbeschreibung für eine benutzerdefinierte Shell-Schnittstelle.
  • Ein Plugin zur Verbindung mit der Schnittstelle in einer Client-Anwendung.
  • Ein Beispielkompositor mit einer serverseitigen Implementierung der Schnittstelle.

Die Protokollbeschreibung folgt dem Standard-XML-Format, das von wayland-scanner gelesen wird. Sie wird hier nicht im Detail behandelt, umfasst aber die folgenden Merkmale:

  • Eine Schnittstelle für die Erstellung einer Shell-Oberfläche für wl_surface. Dies ermöglicht es dem Protokoll, zusätzlich zu den bestehenden APIs von wl_surface weitere Funktionen hinzuzufügen.
  • Eine Anforderung zum Setzen eines Fenstertitels auf der Shell-Oberfläche.
  • Eine Anfrage zum Minimieren/De-Minimieren der Shell-Oberfläche.
  • Ein Ereignis, das den Client über den aktuellen minimierten Zustand der Shell-Oberfläche informiert.

Damit qtwaylandscanner automatisch als Teil des Builds ausgeführt wird, verwenden wir die CMake-Funktionen qt_generate_wayland_protocol_server_sources() und qt_generate_wayland_protocol_client_sources() zur Erzeugung des serverseitigen bzw. clientseitigen Glue-Codes. (Bei der Verwendung von qmake erreichen die Variablen WAYLANDSERVERSOURCES und WAYLANDCLIENTSOURCES das Gleiche).

Das Client-Plugin

Damit die Shell-Integration von einem Qt-Client erkannt werden kann, müssen wir das QWaylandShellIntegrationPlugin neu implementieren.

class QWaylandExampleShellIntegrationPlugin : public QWaylandShellIntegrationPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QWaylandShellIntegrationFactoryInterface_iid FILE "example-shell.json")

public:
    QWaylandShellIntegration *create(const QString &key, const QStringList &paramList) override;
};

QWaylandShellIntegration *QWaylandExampleShellIntegrationPlugin::create(const QString &key, const QStringList &paramList)
{
    Q_UNUSED(key);
    Q_UNUSED(paramList);
    return new ExampleShellIntegration();
}

Dadurch wird der Schlüssel "example-shell" an die Shell-Integration angehängt und eine Möglichkeit geschaffen, die Klasse ExampleShellIntegration zu instanziieren, wenn sich ein Client mit der Schnittstelle verbindet.

Die APIs für die Erstellung von Shell-Erweiterungen sind in der Kopfzeile qwaylandclientshellapi_p.h verfügbar.

#include <QtWaylandClient/private/qwaylandclientshellapi_p.h>

Dieser Header erfordert die Einbindung privater APIs, da er im Gegensatz zu öffentlichen Qt-APIs keine Binärkompatibilitätsgarantie bietet. Die APIs werden immer noch als stabil angesehen und bleiben quellkompatibel, und sind in dieser Hinsicht ähnlich wie andere Plugin-APIs in Qt.

Die ExampleShellIntegration ist der clientseitige Einstiegspunkt für die Erstellung von Shell-Oberflächen wie oben beschrieben. Sie erweitert die Klasse QWaylandShellIntegrationTemplate und verwendet das Curiously Recurring Template Pattern.

class Q_WAYLANDCLIENT_EXPORT ExampleShellIntegration
        : public QWaylandShellIntegrationTemplate<ExampleShellIntegration>
        , public QtWayland::qt_example_shell
{
public:
    ExampleShellIntegration();

    QWaylandShellSurface *createShellSurface(QWaylandWindow *window) override;
};

Sie erbt auch von der Klasse QtWayland::qt_example_shell, die von qtwaylandscanner auf der Grundlage der XML-Beschreibung des Protokolls generiert wird.

Der Konstruktor gibt die Version des Protokolls an, die wir unterstützen:

ExampleShellIntegration::ExampleShellIntegration()
    : QWaylandShellIntegrationTemplate(/* Supported protocol version */ 1)
{
}

Das example_shell-Protokoll hat derzeit die Version eins, daher übergeben wir der übergeordneten Klasse 1. Dies wird bei der Protokollaushandlung verwendet und stellt sicher, dass ältere Clients weiterhin funktionieren, wenn der Compositor eine neuere Version des Protokolls verwendet.

Wenn ExampleShellIntegration initialisiert ist, ist die Anwendung mit dem Server verbunden und hat den Broadcast der globalen Schnittstellen erhalten, die der Compositor unterstützt. Wenn sie erfolgreich war, kann sie Anfragen für die Schnittstelle stellen. In diesem Fall gibt es nur eine Anforderung zu unterstützen: Die Erstellung einer Shell-Oberfläche. Mit der eingebauten Funktion wlSurfaceForWindow() wird das QWaylandWindow in ein wl_surface umgewandelt, dann wird die Anforderung ausgegeben. Anschließend wird die zurückgegebene Oberfläche um ein ExampleShellSurface Objekt erweitert, das die Anfragen und Ereignisse auf der qt_example_shell_surface Schnittstelle behandelt.

QWaylandShellSurface *ExampleShellIntegration::createShellSurface(QWaylandWindow *window)
{
    if (!isActive())
        return nullptr;
    auto *surface = surface_create(wlSurfaceForWindow(window));
    return new ExampleShellSurface(surface, window);
}

Die ExampleShellSurface erweitert zwei Klassen.

class ExampleShellSurface : public QWaylandShellSurface
        , public QtWayland::qt_example_shell_surface

Die erste ist die Klasse QtWayland::qt_example_shell_surface, die auf der Grundlage der XML-Beschreibung des Protokolls erstellt wird. Sie bietet virtuelle Funktionen für Ereignisse und normale Mitgliedsfunktionen für die Anfragen im Protokoll.

Die Klasse QtWayland::qt_example_shell_surface hat nur ein einziges Ereignis.

    void example_shell_surface_minimize(uint32_t minimized) override;

ExampleShellSurface implementiert dieses, um seinen internen Fensterstatus zu aktualisieren. Wenn sich der Zustand des Fensters ändert, speichert es den anstehenden Zustand bis zu einem späteren Zeitpunkt und ruft applyConfigureWhenPossible() in QWaylandShellSurface auf. Zustands-, Größen- und Positionsänderungen sollten auf diese Weise organisiert werden. Auf diese Weise stellen wir sicher, dass die Änderungen das Rendering der Oberfläche nicht beeinträchtigen, und mehrere zusammenhängende Änderungen können leicht als eine einzige angewendet werden.

Wenn es sicher ist, die Oberfläche neu zu konfigurieren, wird die virtuelle Funktion applyConfigure() aufgerufen.

void ExampleShellSurface::applyConfigure()
{
    if (m_stateChanged)
        QWindowSystemInterface::handleWindowStateChanged(platformWindow()->window(), m_pendingStates);
    m_stateChanged = false;
}

Hier wird der neue (minimierte oder de-minimierte) Zustand an das Fenster übergeben.

Die zweite Superklasse ist QWaylandShellSurface. Dies ist die Schnittstelle, die vom QPA-Plugin von Wayland und QWaylandWindow zur Kommunikation mit der Shell verwendet wird. Auch ExampleShellSurface implementiert einige virtuelle Funktionen dieser Schnittstelle neu.

    bool wantsDecorations() const override;
    void setTitle(const QString &) override;
    void requestWindowStates(Qt::WindowStates states) override;
    void applyConfigure() override;

Wenn zum Beispiel die Qt-Anwendungen den Titel eines Fensters setzen, wird dies in einen Aufruf der virtuellen Funktion setTitle() übersetzt.

void ExampleShellSurface::setTitle(const QString &windowTitle)
{
    set_window_title(windowTitle);
}

In der ExampleShellSurface wird dies wiederum in eine Anfrage an unsere benutzerdefinierte Shell-Oberfläche übersetzt.

Der Kompositor

Der letzte Teil des Beispiels ist der Compositor selbst. Dieser hat die gleiche allgemeine Struktur wie die anderen Compositor-Beispiele. Siehe das Minimal-QML-Beispiel für weitere Details zu den Bausteinen eines Qt Wayland Compositor.

Ein bemerkenswerter Unterschied im Custom Shell Compositor ist die Instanziierung der Shell-Erweiterung. Während im Minimal-QML-Beispiel die Shell-Erweiterungen IviApplication, XdgShell und WlShell instanziiert werden, wird im Custom Shell-Beispiel nur eine Instanz der Erweiterung ExampleShell erstellt.

ExampleShell {
    id: shell
    onShellSurfaceCreated: (shellSurface) => {
        shellSurfaces.append({shellSurface: shellSurface});
    }
}

Wir erstellen die Instanz der Shell-Erweiterung als direktes Kind der WaylandCompositor, um sie als globale Schnittstelle zu registrieren. Dies wird an die Clients weitergegeben, wenn sie sich verbinden, und sie können sich wie im vorherigen Abschnitt beschrieben an die Schnittstelle anhängen.

Die Schnittstelle ExampleShell ist eine Unterklasse der generierten Schnittstelle QtWaylandServer::qt_example_shell, die die im Protokoll-XML definierte API enthält. Sie ist auch eine Unterklasse von QWaylandCompositorExtensionTemplate, wodurch sichergestellt wird, dass die Objekte von QWaylandCompositor als Erweiterungen erkannt werden.

class ExampleShell
        : public QWaylandCompositorExtensionTemplate<ExampleShell>
        , QtWaylandServer::qt_example_shell

Diese doppelte Vererbung ist ein typisches Muster in Qt Wayland Compositor bei der Erstellung von Erweiterungen. Die Klasse QWaylandCompositorExtensionTemplate stellt die Verbindung zwischen QWaylandCompositorExtension und der von qtwaylandscanner erzeugten Klasse qt_example_shell her.

Äquivalent dazu erweitert die Klasse ExampleShellSurface die generierte Klasse QtWaylandServer::qt_example_shell_surface sowie QWaylandShellSurfaceTemplate, was sie zu einer Unterklasse der Klasse ShellSurface macht und die Verbindung zwischen Qt Wayland Compositor und dem generierten Protokollcode herstellt.

Um den Typ für Qt Quick verfügbar zu machen, verwenden wir der Einfachheit halber das Präprozessormakro Q_COMPOSITOR_DECLARE_QUICK_EXTENSION_CLASS. Dieses kümmert sich unter anderem um die automatische Initialisierung der Erweiterung, wenn sie dem Qt Quick Graphen hinzugefügt wurde.

void ExampleShell::initialize() { QWaylandCompositorExtensionTemplate::initialize();    QWaylandCompositor *compositor =  static_cast<QWaylandCompositor *>(extensionContainer()); if (!compositor) {        qWarning() << "Failed to find QWaylandCompositor when initializing ExampleShell";
       return; } init(compositor->display(), 1); }

Die Standardimplementierung der Funktion initialize() registriert die Erweiterung beim Compositor. Darüber hinaus initialisieren wir die Protokollerweiterung selbst. Dies geschieht durch den Aufruf der generierten Funktion init() in der Klasse QtWaylandServer::qt_example_shell_surface.

Wir reimplementieren auch die virtuelle Funktion, die für die surface_create -Anfrage generiert wurde.

void ExampleShell::example_shell_surface_create(Resource *resource, wl_resource *surfaceResource, uint32_t id)
{
    QWaylandSurface *surface = QWaylandSurface::fromResource(surfaceResource);

    if (!surface->setRole(ExampleShellSurface::role(), resource->handle, QT_EXAMPLE_SHELL_ERROR_ROLE))
        return;

    QWaylandResource shellSurfaceResource(wl_resource_create(resource->client(), &::qt_example_shell_surface_interface,
                                                           wl_resource_get_version(resource->handle), id));

    auto *shellSurface = new ExampleShellSurface(this, surface, shellSurfaceResource);
    emit shellSurfaceCreated(shellSurface);
}

Diese virtuelle Funktion wird immer dann aufgerufen, wenn ein Client eine Anfrage an die Verbindung stellt.

Unsere Shell-Erweiterung unterstützt nur eine einzige QWaylandSurfaceRole, aber es ist trotzdem wichtig, dass wir sie der QWaylandSurface zuweisen, wenn wir eine Shell-Oberfläche für sie erstellen. Der Hauptgrund dafür ist, dass die Zuweisung widersprüchlicher Rollen an dieselbe Oberfläche als Protokollfehler angesehen wird, und es liegt in der Verantwortung des Compositors, diesen Fehler zu melden, wenn dies geschieht. Das Festlegen einer Rolle für die Oberfläche bei der Übernahme stellt sicher, dass der Protokollfehler ausgegeben wird, wenn die Oberfläche später mit einer anderen Rolle wiederverwendet wird.

Wir verwenden eingebaute Funktionen, um zwischen Wayland- und Qt-Typen zu konvertieren und ein ExampleShellSurface Objekt zu erstellen. Wenn alles vorbereitet ist, geben wir das Signal shellSurfaceCreated() aus, das wiederum im QML-Code abgefangen und zur Liste der Shell-Oberflächen hinzugefügt wird.

ExampleShell {
    id: shell
    onShellSurfaceCreated: (shellSurface) => {
        shellSurfaces.append({shellSurface: shellSurface});
    }
}

In ExampleShellSurface aktivieren wir entsprechend den Teil der Shell-Oberfläche der Protokollerweiterung.

Ausführen des Beispiels

Damit sich ein Client erfolgreich mit der neuen Shell-Erweiterung verbinden kann, sind einige Konfigurationsdetails zu beachten.

Zunächst einmal muss der Client in der Lage sein, das Plugin der Shell-Erweiterung zu finden. Eine einfache Möglichkeit, dies zu tun, besteht darin, QT_PLUGIN_PATH so einzustellen, dass es auf das Plugin-Installationsverzeichnis zeigt. Da Qt die Plugins nach Kategorien sucht, sollte der Plugin-Pfad auf das übergeordnete Verzeichnis zeigen, das das Verzeichnis für die Kategorie wayland-shell-integration enthält. Wenn also die installierte Datei /path/to/build/plugins/wayland-shell-integration/libexampleshellplugin.so ist, dann sollten Sie QT_PLUGIN_PATH wie folgt setzen:

export QT_PLUGIN_PATH=/path/to/build/plugins

Für andere Möglichkeiten, das Plugin-Verzeichnis zu konfigurieren, lesen Sie bitte die Plugin-Dokumentation.

Der letzte Schritt besteht darin, sicherzustellen, dass der Client tatsächlich mit der richtigen Shell-Erweiterung verbunden ist. Qt-Clients versuchen automatisch, sich mit den eingebauten Shell-Erweiterungen zu verbinden, aber dies kann durch Setzen der Umgebungsvariablen QT_WAYLAND_SHELL_INTEGRATION auf den Namen der zu ladenden Erweiterung außer Kraft gesetzt werden.

export QT_WAYLAND_SHELL_INTEGRATION=example-shell

Und das ist alles, was es zu tun gibt. Das Custom Shell Beispiel ist eine eingeschränkte Shell-Erweiterung mit nur wenigen Funktionen, aber es kann als Ausgangspunkt für die Erstellung spezialisierter Erweiterungen verwendet werden.

Beispielprojekt @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.