En esta página

Caparazón personalizado

Custom Shell muestra cómo implementar una extensión de shell personalizada.

Lasextensiones deshell para Wayland son protocolos que gestionan el estado, la posición y el tamaño de las ventanas. La mayoría de los compositores soportarán una o más de las extensiones incorporadas, pero en algunas circunstancias puede ser útil poder escribir una personalizada que contenga las características exactas que tus aplicaciones necesitan.

Esto requiere que implementes la extensión shell tanto en el lado del servidor como en el lado del cliente de la conexión Wayland, por lo que es principalmente útil cuando estás construyendo una plataforma y tienes el control tanto del compositor como de sus aplicaciones cliente.

El ejemplo Custom Shell muestra la implementación de una extensión shell simple. Se divide en tres partes:

  • Una descripción del protocolo para una interfaz de shell personalizada.
  • Un plugin para conectarse a la interfaz en una aplicación cliente.
  • Un compositor de ejemplo con una implementación de la interfaz en el lado del servidor.

La descripción del protocolo sigue el formato XML estándar leído por wayland-scanner. No se tratará en detalle aquí, pero cubre las siguientes características:

  • Una interfaz para crear superficies de shell para wl_surface. Esto permite que el protocolo añada funcionalidad a las API existentes de wl_surface.
  • Una solicitud para establecer un título de ventana en la superficie de la carcasa.
  • Una solicitud para minimizar/desminimizar la superficie de la carcasa.
  • Un evento que informa al cliente del estado actual de minimización de la superficie.

Para que qtwaylandscanner se ejecute automáticamente como parte de la compilación, utilizamos las funciones de CMake qt_generate_wayland_protocol_server_sources () y qt_generate_wayland_protocol_client_sources() para generar el código glue del lado del servidor y del lado del cliente, respectivamente. (Cuando se utiliza qmake, las variables WAYLANDSERVERSOURCES y WAYLANDCLIENTSOURCES consiguen lo mismo).

El complemento cliente

Para que la integración shell sea descubierta por un cliente Qt, debemos reimplementar el QWaylandShellIntegrationPlugin.

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();
}

Esto adjunta la clave "example-shell" a la integración shell y proporciona una forma para que la clase ExampleShellIntegration sea instanciada cuando un cliente se conecte a la interfaz.

Las API para crear extensiones de shell están disponibles en la cabecera qwaylandclientshellapi_p.h.

#include <QtWaylandClient/private/qwaylandclientshellapi_p.h>

Esta cabecera requiere incluir API privadas porque, a diferencia de las API públicas de Qt, no vienen con garantías de compatibilidad binaria. Las API se consideran estables y seguirán siendo compatibles con el código fuente, y son similares en este sentido a otras API de complementos de Qt.

ExampleShellIntegration es el punto de entrada del lado del cliente para crear superficies de shell como se ha descrito anteriormente. Extiende la clase QWaylandShellIntegrationTemplate, usando el Patrón de Plantilla Curiosamente Recurrente.

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

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

También hereda de la clase QtWayland::qt_example_shell, que es generada por qtwaylandscanner basándose en la descripción XML del protocolo.

El constructor especifica la versión del protocolo que soportamos:

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

El protocolo example_shell está actualmente en la versión uno, así que pasamos un 1 a la clase padre. Esto se utiliza en la negociación del protocolo, y asegura que los clientes más antiguos seguirán funcionando si el compositor utiliza una versión más reciente del protocolo.

Cuando se inicializa ExampleShellIntegration, la aplicación está conectada al servidor y ha recibido la difusión de las interfaces globales que soporta el compositor. Si tiene éxito, puede emitir peticiones para la interfaz. En este caso, sólo hay una solicitud para apoyar: Crear una superficie de shell. Utiliza la función incorporada wlSurfaceForWindow() para convertir QWaylandWindow en wl_surface, luego emite la petición. Luego extiende la superficie devuelta con un objeto ExampleShellSurface que manejará las peticiones y eventos de la interfaz qt_example_shell_surface.

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

ExampleShellSurface extiende dos clases.

class ExampleShellSurface : public QWaylandShellSurface
        , public QtWayland::qt_example_shell_surface

La primera es la clase QtWayland::qt_example_shell_surface, que se genera a partir de la descripción XML del protocolo. Proporciona funciones virtuales para eventos y funciones miembro ordinarias para las peticiones del protocolo.

La clase QtWayland::qt_example_shell_surface sólo tiene un evento.

    void example_shell_surface_minimize(uint32_t minimized) override;

ExampleShellSurface lo reimplementa para actualizar su estado interno de ventana. Cuando el estado de la ventana cambia, almacena el estado pendiente hasta más tarde y llama a applyConfigureWhenPossible() en QWaylandShellSurface. Los cambios de estado, tamaño y posición deberían organizarse así. De esta forma, nos aseguramos de que los cambios no interfieren con el renderizado en la superficie, y múltiples cambios relacionados pueden ser fácilmente aplicados como uno solo.

Cuando es seguro reconfigurar la superficie, se llama a la función virtual applyConfigure().

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

Aquí es donde realmente confirmamos el nuevo estado (minimizado o desminimizado) de la ventana.

La segunda superclase es QWaylandShellSurface. Esta es la interfaz usada por el plugin QPA de Wayland y QWaylandWindow para comunicarse con el shell. ExampleShellSurface también reimplementa algunas funciones virtuales de esta interfaz.

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

Por ejemplo, cuando las aplicaciones Qt establecen el título de una ventana, esto se traduce en una llamada a la función virtual setTitle().

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

En ExampleShellSurface esto se traduce a su vez en una petición a nuestra interfaz de superficie de shell personalizada.

El Compositor

La parte final del ejemplo es el propio compositor. Tiene la misma estructura general que los otros ejemplos de compositor. Consulte el ejemplo QML mínimo para obtener más información sobre los componentes básicos de un compositor . Qt Wayland Compositor.

Una diferencia notable en el compositor Custom Shell es la instanciación de la extensión shell. Mientras que el ejemplo Minimal QML crea instancias de las extensiones de shell IviApplication, XdgShell y WlShell, el ejemplo Custom Shell sólo crea una instancia de la extensión ExampleShell.

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

Creamos la instancia de la extensión shell como hija directa de WaylandCompositor para registrarla como interfaz global. Esto se transmitirá a los clientes cuando se conecten, y podrán conectarse a la interfaz como se describe en la sección anterior.

ExampleShell es una subclase de la interfaz generada QtWaylandServer::qt_example_shell, que contiene la API definida en el protocolo XML. También es una subclase de QWaylandCompositorExtensionTemplate, lo que garantiza que los objetos son reconocidos por QWaylandCompositor como extensiones.

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

Esta doble herencia es un patrón típico en Qt Wayland Compositor a la hora de construir extensiones. La clase QWaylandCompositorExtensionTemplate crea la conexión entre QWaylandCompositorExtension y la clase qt_example_shell generada por qtwaylandscanner.

De forma equivalente, la clase ExampleShellSurface extiende la clase QtWaylandServer::qt_example_shell_surface generada, así como QWaylandShellSurfaceTemplate, lo que la convierte en una subclase de la clase ShellSurface y establece la conexión entre Qt Wayland Compositor y el código de protocolo generado.

Para que el tipo esté disponible para Qt Quick, utilizamos la macro del preprocesador Q_COMPOSITOR_DECLARE_QUICK_EXTENSION_CLASS para mayor comodidad. Entre otras cosas, esto se encarga de inicializar automáticamente la extensión cuando se ha añadido al gráfico Qt Quick.

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

La implementación por defecto de la función initialize() registra la extensión con el compositor. Además de esto, inicializamos la propia extensión del protocolo. Hacemos esto llamando a la función generada init() en la clase QtWaylandServer::qt_example_shell_surface.

También reimplementamos la función virtual generada para la petición surface_create.

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);
}

Esta función virtual es llamada cada vez que un cliente emite la petición en la conexión.

Nuestra extensión de shell sólo admite un único QWaylandSurfaceRole, pero sigue siendo importante que lo asignemos al QWaylandSurface cuando creemos una superficie de shell para él. La razón principal de esto es que asignar roles conflictivos a la misma superficie se considera un error de protocolo, y es responsabilidad del compositor emitir este error si ocurre. Establecer un rol en la superficie cuando la adoptamos, asegura que el error de protocolo será emitido si la superficie es reutilizada con un rol diferente más tarde.

Utilizamos funciones integradas para convertir entre tipos Wayland y Qt, y crear un objeto ExampleShellSurface. Cuando todo está preparado, emitimos la señal shellSurfaceCreated(), que a su vez es interceptada en el código QML y añadida a la lista de superficies shell.

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

En ExampleShellSurface, habilitamos de forma equivalente la parte de superficie de shell de la extensión del protocolo.

Ejecución del ejemplo

Para que un cliente se conecte con éxito a la nueva extensión shell, hay que manejar un par de detalles de configuración.

En primer lugar, el cliente tiene que ser capaz de encontrar el plugin de la extensión. Una forma sencilla de hacerlo es configurar QT_PLUGIN_PATH para que apunte al directorio de instalación del plugin. Como Qt buscará los plugins por categoría, la ruta del plugin debería apuntar al directorio padre que contiene el directorio para la categoría wayland-shell-integration. Así que si el archivo instalado es /path/to/build/plugins/wayland-shell-integration/libexampleshellplugin.so, entonces deberías configurar QT_PLUGIN_PATH de la siguiente manera:

export QT_PLUGIN_PATH=/path/to/build/plugins

Para otras formas de configurar el directorio de plugins, consulte la documentación de plugins.

El último paso es asegurarse de que el cliente se conecta a la extensión shell correcta. Los clientes Qt intentarán conectarse automáticamente a las extensiones de shell incorporadas, pero esto se puede anular estableciendo la variable de entorno QT_WAYLAND_SHELL_INTEGRATION con el nombre de la extensión a cargar.

export QT_WAYLAND_SHELL_INTEGRATION=example-shell

Y esto es todo. El ejemplo de Custom Shell es una extensión de shell limitada con muy pocas características, pero puede usarse como punto de partida para construir extensiones especializadas.

Proyecto de ejemplo @ code.qt.io

© 2026 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.