이 페이지에서

사용자 정의 셸

'사용자 정의 셸'에서는 사용자 정의 셸 확장을 구현하는 방법을 설명합니다.

Wayland용셸 확장은 창 상태, 위치 및 크기를 관리하는 프로토콜입니다. 대부분의 컴포지터는 하나 이상의 내장 확장을 지원하지만, 경우에 따라 애플리케이션에 필요한 정확한 기능을 포함하는 사용자 정의 확장을 직접 작성하는 것이 유용할 수 있습니다.

빨간색, 녹색, 흰색 벽이 있는 3D 방과 “라이트맵 사용” 확인란이 표시된 Qt Quick 3D 베이킹된 라이트맵 예제를 실행 중인 컴포지터

이를 위해서는 Wayland 연결의 서버 측과 클라이언트 측 모두에서 셸 확장을 구현해야 하므로, 주로 플랫폼을 구축 중이며 컴포지터와 해당 클라이언트 애플리케이션을 모두 제어할 수 있는 경우에 유용합니다.

'Custom Shell' 예제는 간단한 셸 확장 기능의 구현을 보여줍니다. 이 예제는 다음 세 부분으로 나뉩니다:

  • 사용자 정의 셸 인터페이스에 대한 프로토콜 설명.
  • 클라이언트 애플리케이션에서 인터페이스에 연결하기 위한 플러그인.
  • 인터페이스의 서버 측 구현을 포함하는 예제 컴포지터.

프로토콜 설명은 wayland-scanner 에서 읽어들이는 표준 XML 형식을 따릅니다. 여기서는 자세히 다루지 않겠지만, 다음 기능을 포함합니다:

  • wl_surface 용 셸 표면을 생성하기 위한 인터페이스입니다. 이를 통해 프로토콜은 기존 wl_surface API 위에 기능을 추가할 수 있습니다.
  • 셸 표면에 창 제목을 설정하는 요청.
  • 셸 표면을 최소화하거나 최소화 해제하는 요청.
  • 셸 표면의 현재 최소화 상태를 클라이언트에 알리는 이벤트.

qtwaylandscanner 가 빌드의 일부로 자동으로 실행되도록 하기 위해, CMake 함수 qt_generate_wayland_protocol_server_sources() 와 qt_generate_wayland_protocol_client_sources() 를 사용하여 각각 서버 측 및 클라이언트 측 글루 코드를 생성합니다. ( qmake 를 사용할 경우, WAYLANDSERVERSOURCES 및 WAYLANDCLIENTSOURCES 변수를 통해 동일한 결과를 얻을 수 있습니다.)

클라이언트 플러그인

Qt 클라이언트가 셸 통합을 감지할 수 있도록 하려면 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();
}

이렇게 하면 셸 통합에 "example-shell" 키가 연결되며, 클라이언트가 인터페이스에 연결될 때 ExampleShellIntegration 클래스가 인스턴스화될 수 있는 방법이 제공됩니다.

셸 확장 기능을 생성하기 위한 API는 qwaylandclientshellapi_p.h 헤더에서 확인할 수 있습니다.

#include <QtWaylandClient/private/qwaylandclientshellapi_p.h>

이 헤더는 공개 Qt API와 달리 이진 호환성이 보장되지 않기 때문에 비공개 API를 포함해야 합니다. 해당 API는 여전히 안정적이며 소스 호환성을 유지할 것이며, 이러한 측면에서 Qt의 다른 플러그인 API와 유사합니다.

ExampleShellIntegration 는 위에서 설명한 대로 셸 서페이스를 생성하기 위한 클라이언트 측 진입점입니다. 이 클래스는 Curiously Recurring Template Pattern을 사용하여 QWaylandShellIntegrationTemplate 클래스를 확장합니다.

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

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

또한 이 클래스는 프로토콜의 XML 설명을 기반으로 qtwaylandscanner 에서 생성된 ` QtWayland::qt_example_shell ` 클래스를 상속받습니다.

생성자는 우리가 지원하는 프로토콜의 버전을 지정합니다:

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

example_shell 프로토콜은 현재 버전 1이므로, 부모 클래스에 ` 1 `를 전달합니다. 이는 프로토콜 협상 시 사용되며, 컴포지터가 더 새로운 버전의 프로토콜을 사용하더라도 구형 클라이언트가 계속 작동하도록 보장합니다.

ExampleShellIntegration 가 초기화되면 애플리케이션은 서버에 연결되며, 컴포지터가 지원하는 전역 인터페이스에 대한 브로드캐스트를 수신하게 됩니다. 성공하면 해당 인터페이스에 대한 요청을 발행할 수 있습니다. 이 경우 지원해야 할 요청은 단 하나뿐입니다. 바로 셸 서피스(shell surface) 생성입니다. 내장 함수 ` wlSurfaceForWindow() `를 사용하여 `QWaylandWindow`를 ` wl_surface`로 변환한 다음, 요청을 발행합니다. 그런 다음 반환된 서피스를 ` ExampleShellSurface ` 객체로 확장하여, ` 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 는 두 개의 클래스를 상속받습니다.

class ExampleShellSurface : public QWaylandShellSurface
        , public QtWayland::qt_example_shell_surface

첫 번째는 프로토콜의 XML 설명에 기반하여 생성되는 ` QtWayland::qt_example_shell_surface ` 클래스입니다. 이 클래스는 이벤트에 대한 가상 함수와 프로토콜 내 요청에 대한 일반 멤버 함수를 제공합니다.

QtWayland::qt_example_shell_surface 클래스에는 단 하나의 이벤트만 있습니다.

    void example_shell_surface_minimize(uint32_t minimized) override;

ExampleShellSurface 은 내부 윈도우 상태를 업데이트하기 위해 이를 재구현합니다. 윈도우 상태가 변경되면, 보류 중인 상태를 나중에 처리할 수 있도록 저장하고 QWaylandShellSurface 내의 applyConfigureWhenPossible() 를 호출합니다. 상태, 크기 및 위치 변경은 이와 같이 구성되어야 합니다. 이렇게 하면 변경 사항이 표면 렌더링에 간섭하지 않도록 보장할 수 있으며, 여러 관련 변경 사항을 하나의 변경으로 쉽게 적용할 수 있습니다.

서페이스를 재구성해도 안전한 시점이 되면 가상 ` applyConfigure() ` 함수가 호출됩니다.

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

이 단계에서 새로운(최소화되거나 최대화된) 상태를 창에 실제로 반영합니다.

두 번째 상위 클래스는 ` QWaylandShellSurface`입니다. 이는 Wayland의 QPA 플러그인과 QWaylandWindow가 셸과 통신하는 데 사용하는 인터페이스입니다. ` ExampleShellSurface `도 이 인터페이스의 몇 가지 가상 함수를 재구현합니다.

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

예를 들어, Qt 애플리케이션이 창의 제목을 설정하면, 이는 setTitle() 의 가상 함수 호출로 변환됩니다.

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

ExampleShellSurface 에서는 이 작업이 다시 사용자 정의 셸 서피스 인터페이스에 대한 요청으로 변환됩니다.

컴포지터

이 예제의 마지막 부분은 컴포지터 자체입니다. 이 컴포지터는 다른 컴포지터 예제와 전반적으로 동일한 구조를 가지고 있습니다. 컴포지터의 구성 요소에 대한 자세한 내용은 ‘Minimal QML’ 예제를 참조하십시오 Qt Wayland Compositor.

Custom Shell 컴포지터에서 주목할 만한 차이점 중 하나는 셸 확장 기능의 인스턴스화입니다. Minimal QML 예제에서는 셸 확장 기능인 IviApplication, XdgShell 및 WlShell 을 인스턴스화하는 반면, Custom Shell 예제에서는 ExampleShell 확장 기능의 인스턴스만 생성합니다.

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

셸 확장 인스턴스를 WaylandCompositor 의 직접 자식으로 생성하여 전역 인터페이스로 등록되도록 합니다. 이렇게 하면 클라이언트가 연결될 때 해당 인터페이스가 브로드캐스트되며, 클라이언트는 이전 섹션에서 설명한 대로 이 인터페이스에 연결할 수 있게 됩니다.

ExampleShell 는 프로토콜 XML에 정의된 API를 포함하는 생성된 QtWaylandServer::qt_example_shell 인터페이스의 서브클래스입니다. 또한 QWaylandCompositorExtensionTemplate 의 서브클래스이기도 하며, 이를 통해 객체가 QWaylandCompositor 에 의해 확장 기능으로 인식되도록 보장합니다.

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

이러한 이중 상속은 Qt Wayland Compositor 에서 확장 기능을 구축할 때 흔히 사용되는 패턴입니다. QWaylandCompositorExtensionTemplate 클래스는 QWaylandCompositorExtension 와 qtwaylandscanner 에 의해 생성된 qt_example_shell 클래스 간의 연결을 형성합니다.

이와 동등하게, ExampleShellSurface 클래스는 생성된 QtWaylandServer::qt_example_shell_surface 클래스와 QWaylandShellSurfaceTemplate 클래스를 모두 상속받으므로, ShellSurface 클래스의 하위 클래스가 되며 Qt Wayland Compositor 와 생성된 프로토콜 코드 간의 연결을 확립합니다.

Qt Quick 에서 이 타입을 사용할 수 있도록 하기 위해, 편의상 Q_COMPOSITOR_DECLARE_QUICK_EXTENSION_CLASS 전처리기 매크로를 사용합니다. 이 매크로는 무엇보다도 확장 기능이 Qt Quick 그래프에 추가되었을 때 이를 자동으로 초기화해 줍니다.

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

initialize() 함수의 기본 구현은 컴포지터에 확장 기능을 등록합니다. 이 외에도 프로토콜 확장 자체를 초기화합니다. 이를 위해 QtWaylandServer::qt_example_shell_surface 클래스에서 생성된 init() 함수를 호출합니다.

또한 ` 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);
}

이 가상 함수는 클라이언트가 연결을 통해 해당 요청을 보낼 때마다 호출됩니다.

이 셸 확장 기능은 단일 QWaylandSurfaceRole 만 지원하지만, 이를 위한 셸 표면을 생성할 때 QWaylandSurface 에 할당하는 것은 여전히 중요합니다. 그 주된 이유는 동일한 서페이스에 상충되는 역할을 할당하는 것이 프로토콜 오류로 간주되며, 이러한 상황이 발생하면 컴포지터가 해당 오류를 발생시켜야 할 책임이 있기 때문입니다. 서페이스를 채택할 때 역할을 설정해 두면, 나중에 해당 서페이스가 다른 역할로 재사용될 경우 프로토콜 오류가 확실히 발생하도록 보장할 수 있습니다.

내장 함수를 사용하여 Wayland와 Qt 유형 간 변환을 수행하고, ` ExampleShellSurface ` 객체를 생성합니다. 모든 준비가 완료되면 ` shellSurfaceCreated() ` 신호를 발송하며, 이 신호는 QML 코드에서 수신되어 셸 서피스 목록에 추가됩니다.

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

ExampleShellSurface 에서는 이에 상응하여 프로토콜 확장 기능 중 셸 서피스 관련 부분을 활성화합니다.

예제 실행하기

클라이언트가 새로운 셸 확장에 성공적으로 연결되려면 몇 가지 구성 세부 사항을 처리해야 합니다.

우선, 클라이언트가 셸 확장 기능의 플러그인을 찾을 수 있어야 합니다. 이를 위한 간단한 방법 중 하나는 ` QT_PLUGIN_PATH `을 플러그인 설치 디렉터리로 가리키도록 설정하는 것입니다. Qt는 카테고리별로 플러그인을 검색하므로, 플러그인 경로는 ` wayland-shell-integration` 카테고리 디렉터리가 포함된 상위 디렉터리를 가리켜야 합니다. 따라서 설치된 파일이 ` /path/to/build/plugins/wayland-shell-integration/libexampleshellplugin.so`인 경우, ` QT_PLUGIN_PATH `을 다음과 같이 설정해야 합니다:

export QT_PLUGIN_PATH=/path/to/build/plugins

플러그인 디렉터리를 구성하는 다른 방법에 대해서는 플러그인 설명서를 참조하십시오.

마지막 단계는 클라이언트가 실제로 올바른 셸 확장 기능에 연결되도록 하는 것입니다. Qt 클라이언트는 내장된 셸 확장 기능에 자동으로 연결을 시도하지만, QT_WAYLAND_SHELL_INTEGRATION 환경 변수를 로드할 확장 기능의 이름으로 설정하여 이 동작을 재정의할 수 있습니다.

export QT_WAYLAND_SHELL_INTEGRATION=example-shell

이것으로 모든 과정이 끝났습니다. ‘Custom Shell’ 예제는 기능이 매우 제한적인 셸 확장 프로그램이지만, 특화된 확장 프로그램을 구축하기 위한 출발점으로 활용할 수 있습니다.

예제 프로젝트 @ 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.