桌面系统UI示例

使用纯粹的QML展示了最小化的桌面系统UI。

Screenshot

注意:如果您想在Linux机器上构建此示例,请阅读此内容

简介

该示例以简单的形式展示了应用程序管理API,作为一个经典的桌面,带有服务器端窗口装饰。该示例更多地关注概念,而不是优雅或完整性。例如,没有执行错误检查。在这个最小化桌面系统的某些功能仅打印调试信息。

以下功能得到支持

  • 通过点击左上角的小图标启动应用程序
  • 再次点击左上角的小图标停止应用程序
  • 通过点击窗口的关闭图标关闭应用程序窗口
  • 按住窗口并移动来拖动窗口,按窗口可以提升它并设置焦点(焦点窗口为蓝色)
  • 通过按住顶部窗口装饰并移动来拖动窗口
  • 应用程序启动时系统UI发送一个'propA'变化
  • 系统UI和App2响应窗口属性更改的调试信息
  • 通过点击来停止或重新启动App1动画
  • 当停止时,App1将旋转角度作为一个窗口属性发送到系统UI
  • 当被暂停时,App1在系统UI上显示一个弹出窗口
  • App2记录启动它的文档URL
  • 当点击灯泡图标时,App2在系统UI中触发一个通知
  • 显示来自appman之外的进程的路由的Wayland客户端窗口

注意:此示例可以以单进程或多进程方式运行。在下面的步骤中,我们使用多进程及其对应术语。术语客户端应用程序服务器系统UI可以互换使用。

要启动此示例,请导航到minidesk文件夹,并运行以下命令

<path-to-appman-binary> -c am-config.yaml

appman二进制文件(可执行文件)通常位于Qt安装的bin文件夹中。

步骤

系统UI窗口
import QtQuick
import QtApplicationManager.SystemUI

Window {
    title: "Minidesk - QtApplicationManager Example"
    width: 1024
    height: 640
    color: "whitesmoke"

    Readme {}

    Text {
        anchors.bottom: parent.bottom
        text: `${ApplicationManager.singleProcess ? "Single" : "Multi"}-process mode  |  ${PackageManager.architecture}`
    }
    ...

必须导入QtApplicationManager.SystemUI模块以访问应用程序管理API。系统UI窗口有固定大小和“whitesmoke”背景颜色。根元素可以是普通的项,如Rectangle。应用程序管理器会自动将它包裹在一个窗口内部。在背景上,我们显示了包含可用功能信息的Readme元素。在左下角有一个文本指示应用程序管理器是在单进程还是多进程模式下运行。

启动器
    // Application launcher panel
    Column {
        Repeater {
            model: ApplicationManager

            Image {
                required property string icon
                required property bool isRunning
                required property ApplicationObject application
                source: icon
                opacity: isRunning ? 0.3 : 1.0

                MouseArea {
                    anchors.fill: parent
                    onClicked: parent.isRunning ? parent.application.stop() : parent.application.start("documentUrl");
                }
            }
        }
    }

重复器(Repeater)提供一个在系统界面左上角按列排列的应用程序图标;ColumnApplicationManager 元素是模型。其中,ApplicationManager 提供了 icon 角色,它用作 Image 的源 URL。在应用程序的 info.yaml 文件中定义了 icon URL。要表示应用程序已启动,将相应的应用程序图标与 isRunning 角色绑定,则减少其不透明度。

单击应用程序图标将通过调用 ApplicationObject.start() 启动相应的应用程序。此函数可通过在 ApplicationManager 模型中查找 application 角色来访问。两个应用程序都通过(可选的)文档 URL,documentUrl 启动。如果应用程序已经运行,则调用代替的 ApplicationObject.stop()。

系统界面中的应用程序窗口
    // System UI chrome for applications
    Repeater {
        model: ListModel { id: topLevelWindowsModel }

        delegate: FocusScope {
            id: winChrome
            required property int index
            required property WindowObject window

            width: chromeImg.width
            height: chromeImg.height
            z: index

            Image {
                id: chromeImg
                source: winChrome.activeFocus ? "chrome-active.png" : "chrome-bg.png"

                Text {
                    anchors.horizontalCenter: parent.horizontalCenter
                    y: (25 - contentHeight) / 2
                    color: "white"
                    text: "Decoration: " + (winChrome.window.application?.names["en"] ?? 'External Application')
                }

                MouseArea {
                    width: chromeImg.width; height: 25
                    drag.target: winChrome
                    onPressed: winItem.forceActiveFocus();
                }

                Image {
                    source: "close.png"
                    MouseArea {
                        anchors.fill: parent
                        onClicked: winChrome.window.close();
                    }
                }
            }

            WindowItem {
                id: winItem
                anchors.fill: parent
                anchors.margins: 3
                anchors.topMargin: 25
                window: winChrome.window

                Connections {
                    target: winChrome.window
                    function onContentStateChanged() {
                        if (winChrome.window.contentState === WindowObject.NoSurface)
                            topLevelWindowsModel.remove(winChrome.index, 1);
                    }
                }
            }

            onFocusChanged: (focus) => {
                if (focus)
                    topLevelWindowsModel.move(winChrome.index, topLevelWindowsModel.count - 1, 1);
            }

            Component.onCompleted: {
                x = 300 + winChrome.index * 50;
                y =  10 + winChrome.index * 30;
                winItem.forceActiveFocus();
            }
        }
    }

此第二次重复器为其代表中的应用程序窗口提供窗口装饰。模型是一个普通的ListModel,通过由 WindowManager 创建的窗口对象来填充。以下代码显示了填充此ListModel的窗口角色。现在让我们关注这个重复器的代表由什么组成

  • 封装类型是一个 FocusScope,它可以跟踪焦点变化。
  • 一个主要透明的 Image,充当窗口装饰(边框)。蓝色边框表示焦点窗口,否则边框是灰色。位置取决于 model.index,因此每个应用程序窗口都有不同的初始位置。
  • 创建窗口的应用程序名称,前面加“装饰” atop。此名称来自相关的 ApplicationObject,定义在应用程序的 info.yaml 文件中。
  • 一个 MouseArea,用于拖动和提升窗口。只有那个区域覆盖顶部装饰栏,因此将处理拖动。
  • 在右上角有一个关闭图标,用于关闭窗口(见 WindowObject.close()())。由于我们的示例应用程序只有一个顶级窗口,关闭它会导致相应的应用程序退出。
  • 中心点:一个 WindowItem,用于在系统界面中渲染 WindowObject,类似于图像文件和 QML 的 Image 组件之间的关系。
  • 最后,一旦应用程序(客户端)侧从应用程序中销毁其窗口,则从 ListModel 中删除行的代码 - 要么是关闭了,要么变得不可见,应用程序本身退出或崩溃。任何这些情况都会导致 WindowObject 丢失其表面。更复杂系统界面可以像在 Animated Windows System UI Example 中那样动画化窗口的消失。
弹出

在系统界面中显示弹出菜单实现了两种方法

  • 通过客户端应用程序渲染的窗口
  • 通过应用程序管理器提供 notify API 显示

这是相应的系统界面代码

    // System UI for a pop-up
    WindowItem {
        id: popUpContainer
        z: 9998
        width: 200; height: 60
        anchors.centerIn: parent

        Connections {
            target: popUpContainer.window
            function onContentStateChanged() {
                if (popUpContainer.window.contentState === WindowObject.NoSurface)
                    popUpContainer.window = null;
            }
        }
    }

    // System UI for a notification
    Text {
        z: 9999
        font.pixelSize: 46
        anchors.centerIn: parent
        text: NotificationManager.count > 0 ? NotificationManager.get(0).summary : ""
    }
客户端应用程序渲染

App1在它的ApplicationManagerWindow根元素中实例化了另一个ApplicationManagerWindow以用于弹出窗口,如图所示

    ApplicationManagerWindow {
        id: popUp
        visible: rotation.paused
        color: "orangered"

        Text {
            anchors.centerIn: parent
            text: "App1 paused!"
        }

        Component.onCompleted: setWindowProperty("type", "pop-up");
    }

使用ApplicationManagerWindow.setWindowProperty()方法来设置自由选择的共享属性。这里我们选择了type: "pop-up"以表明窗口应显示为弹出窗口。

在WindowManager::onWindowAdded()信号处理程序下面,系统UI检查这个属性,然后将窗口适当地作为一个弹出窗口处理。

在系统UI代码中,弹出窗口将被设置为popUpContainer WindowItem的内容窗口。出于演示目的,实现只能同时支持一个弹出窗口。这是足够的,因为App1在其动画暂停时只显示一个弹出窗口。重要的是理解,系统UI和应用之间必须有关于窗口映射的协议。与可以自由拖动并且有标题栏和边框的常规应用窗口不同,弹出窗口仅居中且没有任何装饰。还请注意,在popUpContainer中是如何处理WindowObject.contentStateChanged信号的:当窗口没有相关表面时,就会释放窗口。这对于释放任何被窗口对象使用的资源非常重要。请注意,当直接使用WindowManager模型时,这将隐式完成。建议使用这种方法,因为它更方便。

通知API使用

窗口属性方法的一种替代方法是在应用(客户端)侧使用应用程序管理器的Notification API,并在系统UI(服务器)侧使用NotificationManager API。以下代码是在点击App2中的灯泡图标时调用的

                let notification = ApplicationInterface.createNotification();
                notification.summary = "Let there be light!"
                notification.show();

App2创建了一个新的Notification元素,设置了它的summary属性,并在其上调用show()。此调用增加了系统UI侧的NotificationManager.count,随后将Text元素的文本属性设置为第一条通知的summary字符串。为了简洁,示例中仅展示了第一条通知。

WindowManager信号处理程序
    // Handler for WindowManager signals
    Connections {
        target: WindowManager
        function onWindowAdded(window) {
            if (window.windowProperty("type") === "pop-up") {
                popUpContainer.window = window;
            } else {
                topLevelWindowsModel.append({"window": window});
                window.setWindowProperty("propA", 42);
            }
        }

        function onWindowPropertyChanged(window, name, value) {
            console.log("SystemUI: OnWindowPropertyChanged [" + window + "] - " + name + ": " + value);
        }
    }

这是系统UI的关键部分,其中将应用窗口(表面)映射到系统UI中的WindowItem。当有新的应用窗口可用(变为可见)时,将调用onWindowAdded处理程序。

只有App1的“弹出”ApplicationManagerWindow具有用户定义的type属性。这样的窗口放置在popUpContainerWindowItem。其他所有窗口没有type属性;它们被添加到topLevelWindowsModel中。此模型用于System UI chrome Repeater之上。因此,传递给onWindowAdded信号中的window对象被设置为WindowItemwindow属性(在Repeater的委托中)。

顺便说一句,由于配置文件中设置了"flags/noSecurity: yes"(例如在KDE的计算器中),来自应用程序经理之外启动的任何Wayland客户端窗口也将显示。

$ QT_WAYLAND_DISABLE_WINDOWDECORATION=1 WAYLAND_DISPLAY=qtam-wayland-0 kcalc -platform wayland
应用程序终止

当应用程序通过System UI中的ApplicationManager.stopApplication()停止时,它会接收到ApplicationInterface.quit()信号。然后,应用程序可以进行一些清理,并且必须随后使用ApplicationInterface.acknowledgeQuit()进行确认,就像App2所做的那样

    Connections {
        target: ApplicationInterface
        function onOpenDocument(documentUrl, mimeType) {
            console.log("App2: onOpenDocument - " + documentUrl);
        }
        function onQuit() {
            ApplicationInterface.acknowledgeQuit();
        }
    }

请注意,App1表现不佳:它没有确认quit信号,因此将被应用程序管理器直接终止。

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。本文件中包含的文档贡献是各自所有者的版权。本文件提供的文档是在自由软件基金会(Free Software Foundation)出版的GNU自由文档许可协议版本1.3的条款下授权的。Qt及相应的标志是芬兰的Qt公司或世界其他国家的注册商标。所有其他商标均为其各自所有者的财产。