自定义扩展

自定义扩展展示了如何实现自定义 Wayland 扩展。

为 Wayland 编写新扩展非常容易。它们使用基于 XML 的格式定义,并使用 wayland-scanner 工具将其转换为 C 语言中的粘合代码。Qt 通过 qtwaylandscanner 对此进行扩展,这将在 Qt 和 C++ 中生成额外的粘合代码。

自定义扩展示例展示了如何使用这些工具扩展 Wayland 协议,并在 Wayland 客户端和服务器之间发送自定义请求和事件。

该示例包含四个部分

  • 协议本身的定义。
  • 支持扩展的合成器。
  • 支持扩展的基于 C++ 的客户端。
  • 支持扩展的基于 QML 的客户端。

协议定义

XML 文件 custom.xml 定义了协议。它包含一个名为 "qt_example_extension" 的接口。这将是从服务器广播并供客户端附加以发送请求和接收事件的名称。该名称应该是唯一的,因此使用一个前缀将它与官方接口区分开来是好的。

接口通常包括两种类型的远程过程调用:请求事件。"请求" 是客户端在服务器端执行的调用,而 "事件" 是服务器在客户端执行的调用。

示例扩展包含一组 请求,指示服务器对客户端窗口应用某些转换。例如,如果客户端发送 "bounce" 请求,那么服务器应该通过在屏幕上使窗口弹跳来响应。

同样,它也有一个服务器可以用来向客户端提供指令的事件集。例如,"set_font_size" 事件是客户端将默认字体大小设置为特定大小的指令。

协议定义了请求和事件的存在以及它们所采取的参数。当在协议上运行 qtwaylandscanner 时,它会生成需要将过程调用及其参数打包并在连接上传输的代码。在另一端,这变成了对可以实施以提供实际响应的虚拟函数的调用。

为了使 qtwaylandscanner 在构建过程中自动运行,我们使用 CMake 函数 qt_generate_wayland_protocol_server_sources()qt_generate_wayland_protocol_client_sources() 生成服务器端和客户端的粘合代码,分别。(当使用 qmake 时,WAYLANDSERVERSOURCESWAYLANDCLIENTSOURCES 变量可以做到同样的事情。)

合成器实现

合成器应用程序本身使用QML和Qt Quick实现,但扩展是以C++实现的。

第一步是创建一个由qtwaylandscanner生成的粘合代码的子类,以便我们可以访问其功能。我们在类中添加了QML_ELEMENT宏,以便从QML中访问。

class CustomExtension  : public QWaylandCompositorExtensionTemplate<CustomExtension>
        , public QtWaylandServer::qt_example_extension
{
    Q_OBJECT
    QML_ELEMENT

除了继承生成的类,我们还继承了提供使用怪异重复模板模式处理扩展时的额外便利性的QWaylandCompositorExtensionTemplate类。

注意,由于QWaylandCompositorExtensionTemplate是一个基于QObject的类,因此它必须位于继承列表的第一位。

子类在生成的基类中重新实现了虚拟函数,在这些函数中我们可以处理由客户端发出的请求。

protected:
    void example_extension_bounce(Resource *resource, wl_resource *surface, uint32_t duration) override;

在这些重新实现中,我们只是将请求翻译成一个信号的发射,这样我们就可以在合成器的实际QML代码中处理它。

void CustomExtension::example_extension_bounce(QtWaylandServer::qt_example_extension::Resource *resource, wl_resource *wl_surface, uint32_t duration)
{
    Q_UNUSED(resource);
    auto surface = QWaylandSurface::fromResource(wl_surface);
    qDebug() << "server received bounce" << surface << duration;
    emit bounce(surface, duration);
}

此外,子类为每个事件定义了,这样它们可以来自QML或连接到信号。槽简单地调用生成的函数,将事件发送到客户端。

void CustomExtension::setFontSize(QWaylandSurface *surface, uint pixelSize)
{
    if (surface) {
        Resource *target = resourceMap().value(surface->waylandClient());
        if (target) {
            qDebug() << "Server-side extension sending setFontSize:" << pixelSize;
            send_set_font_size(target->handle,  surface->resource(), pixelSize);
        }
    }
}

由于我们已向类定义中添加了QML_ELEMENT宏(并将相应的构建步骤添加到了构建系统文件),因此它可以在QML中实例化。

我们将其作为WaylandCompositor对象的直接子对象,以便合成器将其注册为扩展。

    CustomExtension {
        id: custom

        onSurfaceAdded: (surface) => {
            var item = itemForSurface(surface)
            item.isCustom = true
        }

        onBounce: (surface, ms) => {
            var item = itemForSurface(surface)
            item.doBounce(ms)
        }

        onSpin: (surface, ms) => {
            var item = itemForSurface(surface)
            item.doSpin(ms)
        }

        onCustomObjectCreated: (obj) => {
            var item = customObjectComponent.createObject(defaultOutput.surfaceArea,
                                                          { "color": obj.color,
                                                            "text": obj.text,
                                                            "obj": obj } )
        }
    }

    function setDecorations(shown) {
        var n = itemList.length
        for (var i = 0; i < n; i++) {
            if (itemList[i].isCustom)
                custom.showDecorations(itemList[i].surface.client, shown)
        }
    }

该对象为客户可能收到的请求响应用户信号,并相应地做出反应。此外,我们可以调用其槽来发送事件。

            onFontSizeChanged: {
                custom.setFontSize(surface, fontSize)
            }

客户端实现

两个客户端共享接口的C++实现。就像在合成器中一样,我们创建一个生成的代码的子类,它也继承了一个模板类。在这种情况下,我们继承自QWaylandClientExtensionTemplate。

class CustomExtension : public QWaylandClientExtensionTemplate<CustomExtension>
        , public QtWayland::qt_example_extension

这种方法与合成器的非常相似,只是颠倒过来:请求被实现为槽,调用生成的函数,而事件是虚拟函数,我们重新实现以发出信号。

void CustomExtension::sendBounce(QWindow *window, uint ms)
{
    QtWayland::qt_example_extension::bounce(getWlSurface(window), ms);
}

客户端代码本身非常简单,只是用来展示如何触发行为。在自定义绘制事件中,它绘制一系列矩形和标签。当点击其中任何一个时,它会向服务器发出请求。

    void mousePressEvent(QMouseEvent *ev) override
    {
        if (rect1.contains(ev->position()))
            doSpin();
        else if (rect2.contains(ev->position()))
            doBounce();
        else if (rect3.contains(ev->position()))
            newWindow();
        else if (rect4.contains(ev->position()))
            newObject();
    }

当接收到set_font_size事件以更新字体大小时,我们的扩展类的信号连接到一个槽。

        connect(m_extension, &CustomExtension::fontSize, this, &TestWindow::handleSetFontSize);

该槽将更新字体大小并重新绘制窗口。

QML客户端实现

QML客户端与C++客户端类似。它依赖于与C++客户端相同的自定义扩展实现,并在QML中实例化以启用它。

    CustomExtension {
        id: customExtension
        onActiveChanged: {
            registerWindow(topLevelWindow)
        }
        onFontSize: (window, pixelSize) => {
            topLevelWindow.fontSize = pixelSize
        }
    }

用户界面由一些可点击的矩形组成,使用TapHandler在点击矩形时发送相应的请求。

            TapHandler {
                onTapped: {
                    if (customExtension.active)
                        customExtension.sendBounce(topLevelWindow, 1000)
                }
            }

为了简单起见,此示例仅限于演示bouncespin请求以及set_font_size事件。支持其他功能的添加留给读者作为练习。

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。本文件中包含的文档贡献均为其各自所有权者的版权。提供的文档受GNU自由文档许可证版本1.3的许可,该许可证由自由软件基金会发布。Qt及其相关标志是芬兰以及全球其他国家的Qt公司有限公司的商标。所有其他商标均为其各自所有权者的财产。