NEWS

新闻

了解openKylin最新资讯,关注社区和产品动态。

NEWS

Learn about the latest news.

【干货分享】ukui-quick-widget桌面插件开发指南

2024-12-02 10:28:53
01

简介

在openKylin系统默认搭载的桌面环境UKUI中,实现了一个基于Qt Quick的UI开发功能集合,其中包含提供在UKUI上开发QML应用所需要的接口和功能模块—ukui-quick。

ukui-quick为开发QML应用提供了多种组件,包括各种基础UI组件,窗口组件,以及对系统主题(颜色,光标,字体)、窗口系统接口和UKUIFrameWork各种能力的访问接口。此外,ukui-quick中还提供了一个基于插件模式的UI开发框架,目的在于实现对桌面通用插件的定义、加载以及统一配置管理等功能。

其特点包括:
  • 可实现桌面组件一定程度上的功能解耦与功能复用;

  • 插件和宿主应用(Host)之间不会强制有二进制依赖关系,插件的开发相对更加独立,不易产生版本迭代时插件不兼容的情况;

  • 插件开发门槛低,无需了解宿主应用全部功能即可开发插件。

  • 通用性,可实现不同桌面组件之间无缝共享插件;

  • 统一的配置管理模块,便于应用和插件的统一配置管理。

ukui-quick-widget是基于ukui-quick框架的通用插件合集。这些插件以统一的结构形式来实现不同的UI和功能,如日历、网速显示等。浏览源码戳这里(https://gitee.com/openkylin/ukui-quick-widgets),这些插件都可以加载到基于ukui-quick开发的应用中。

接下来,一起看看如何开发一个ukui-quick-widget插件吧!


02

开发一个通用插件

ukui-quick插件以一个目录的形式安装到系统中,首先,我们来看看插件的目录结构。

1.插件的目录结构

    /*ukui-quick插件命名前缀为“org.ukui” *//*文件夹 org.ukui.myplugin 是整个插件项目的顶层文件夹*/
    ├── org.ukui.myplugin    #插件顶层文件夹│    ├── metadata.json    #插件描述文件│    ├── translations│    │    └── *.qm        #多语言翻译文件│    └── ui│        └── main.qml    #插件主入口文件,插件界面

    其中,metadata.json是插件的描述文件;translations目录存放翻译文件;ui目录存放插件的界面文件,主要包括qml文件,界面用到的相关资源文件也会安装到此目录,main.qml是插件的主入口文件。

    由此可见,开发一个插件程序,只需要按照规则组织好目录结构,创建metadata.json和main.qml即可。

    接下来,我们以“数字时钟”为例,逐步开发一个通用插件。

    2.开发示例:数字时钟

    数字时钟可以拆分为功能和界面两部分,功能部分需要实现系统时间的获取;界面部分则需要实现系统时间的显示。本示例工程使用CMake构建,需要手动编写一个CMakeLists.txt。

    • 编写 CMakeLists.txt

    在CMakeLists.txt 中定义数字时钟插件名称和版本号,查找并载入必要的Qt模块,链接对应的库。这里我们定义插件名称为ukui-digitalclock。

      #CMakeLists.txtcmake_minimum_required(VERSION 3.14)
      project(ukui-digitalclock)set(VERSION_MAJOR 1)set(VERSION_MINOR 0)set(VERSION_MICRO 0)set(UKUI_DIGITALCLOCK_VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_MICRO})
      set(CMAKE_INCLUDE_CURRENT_DIR ON)set(CMAKE_AUTOMOC ON)set(CMAKE_AUTORCC ON)set(CMAKE_CXX_STANDARD_REQUIRED ON)
      find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui Widgets Quick REQUIRED)find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Widgets Quick REQUIRED)
      target_link_libraries(${PROJECT_NAME}        PRIVATE        Qt${QT_VERSION_MAJOR}::Core        Qt${QT_VERSION_MAJOR}::Quick        Qt${QT_MAJOR_VERSION}::Gui        Qt${QT_VERSION_MAJOR}::Widgets)
      • 编写插件类

      编写QML 模块定义文件:qmldir,声明模块标识符和插件名称。

        module org.ukui.digitalclockplugin ukui-digitalclock

        插件类继承自QQmlExtensionPlugin,在这里重写函数 registerTypes() ,将"org.ukui.digitalclock"注册为 qml 模块。

          //头文件 ukui-digitalclock-plugin.h#ifndef UKUIDIGITALCLOCKPLUGIN_H#define UKUIDIGITALCLOCKPLUGIN_H
          #include
          class UkuiDigitalclockPlugin : public QQmlExtensionPlugin{    Q_OBJECT    Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)public:    void registerTypes(const char *uri) override;};
          #endif // UKUIDIGITALCLOCKPLUGIN_H
            //ukui-digitalclock-plugin.cpp#include "ukui-digitalclock-plugin.h"#include "digitalclock.h"
            #include #include
            void UkuiDigitalclockPlugin::registerTypes(const char *uri){    Q_ASSERT(QString(uri) == QLatin1String("org.ukui.digitalclock"));    qmlRegisterModule(uri, 1, 0);    qmlRegisterType(uri, 1, 0, "Digitalclock");}

            Digitalclock为时钟功能类,在这里实现获取系统时间的功能,具体实现如下:

              //头文件 digitalclock.h#ifndef DIGITALCLOCK_H#define DIGITALCLOCK_H
              #include#include
              class Digitalclock : public QObject{    Q_OBJECT    Q_PROPERTY(QString time READ time NOTIFY timeUpdated)
              public:    explicit Digitalclock(QObject *parent = nullptr);    ~Digitalclock() = default;    QString time() const;private:    QTimer * m_timer = nullptr;
              Q_SIGNALS:    void timeUpdated();};
              #endif // DIGITALCLOCK_H
                //digitalclock.cpp#include "digitalclock.h"#include
                Digitalclock::Digitalclock(QObject *parent){    m_timer = new QTimer(this);    m_timer->start(1000);    connect(m_timer, &QTimer::timeout, this, &Digitalclock::timeUpdated);}
                QString Digitalclock::time() const{    return QTime::currentTime().toString(Qt::DateFormat::TextDate);}
                • 编写 widget 描述文件 metadata.json

                "Id": "org.ukui.digitalclock"是插件(widget)的标识,加载插件时需要用到。“Contents”: {“Main”: "ui/main.qml"} 用于加载插件的界面 main.qml。

                “Contents”: {“I18n”: " "} 用于加载插件的多语言翻译。该字段写入翻译文件路径或文件名,插件就可以自动加载翻译文件,不需要使用 QTranslator加载。如果没有翻译文件,该字段可以为空,或者不写该字段。

                  {  "Authors": [    {      "Name": "",      "Email": ""    }  ],  "Id": "org.ukui.digitalclock",  "Icon": "",  "Name": "ukui-digitalclock",  "Name[zh_CN]": "数字时钟",  "Tooltip": "Digital clock",  "Tooltip[zh_CN]": "数字时钟",  "Description": "Digital clock",  "Description[zh_CN]": "数字时钟",  "Version": "1.0",  "Website": "https://ukui.org",  "BugReport": "https://gitee.com/openkylin/ukui-quick-widgets/issues",
                    "Contents": {    "Main": "ui/main.qml",    "I18n": ""  }}
                  • 编写 QML 界面

                  QML界面分为主入口qml文件和插件界面qml文件。主入口qml文件是插件被加载时实例化的第一个对象。

                  需要注意的是,主入口的顶级对象必须是“ WidgetItem ”。代码实现如下:

                    // main.qmlimport QtQuick 2.15               // 导入qtquick模块import QtQuick.Layouts 1.15       // 导入qml布局模块import org.ukui.quick.widgets 1.0 // 导入ukui-quick插件框架模块
                    // 定义顶级对象,该对象必须是 " WidgetItem "WidgetItem {    id: rootWidget    // 在此处设置Widget的尺寸策略    // 截至目前,只有任务栏会尊重Widget的Layout请求,并给予相应的尺寸    Layout.preferredWidth: digitalclock.width    Layout.fillHeight: true    clip: false
                        //此处是我们实现的插件界面    DigitalClockItem {        id: digitalclock        anchors.centerIn: parent    }}

                    插件界面代码实现如下:

                      //DigitalClockItem.qmlimport QtQuick 2.15import org.ukui.digitalclock 1.0 //导入自定义数字时钟模块
                      Rectangle {    implicitWidth: 106    implicitHeight: 40    radius: 6    color: Qt.rgba(255, 255, 255, 0.5)    border.color: "black"
                          Digitalclock {        id: clock    }
                          Text {        anchors.centerIn: parent        text: clock.time        font.pointSize: 18    }}
                      • 补全 CMakeLists.txt 并安装

                      当我们编写完插件的主要逻辑后,还需要将插件安装到文件系统中,这样才能被ukui-quick应用加载。

                      通用插件有两个主要的安装路径:

                        # 系统路径/usr/share/ukui/widgets# 用户路径${HOME}/.local/share/ukui/widgets

                        这里我们将插件安装到了系统路径。CMakeLists.txt需要补全的内容如下:

                          #CMakeLists.txtset(PROJECT_SOURCES    plugin/ukui-digitalclock-plugin.h plugin/ukui-digitalclock-plugin.cpp    plugin/digitalclock.h plugin/digitalclock.cpp)
                          set(UKUI_DIGITALCLOCK_DATA_DIR "/usr/share/ukui/widgets/org.ukui.digitalclock")set(UKUI_DIGITALCLOCK_TRANSLATION_DIR "${UKUI_DIGITALCLOCK_DATA_DIR}/translations")
                          if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)    add_library(${PROJECT_NAME} SHARED MANUAL_FINALIZATION ${PROJECT_SOURCES})else()    add_library(${PROJECT_NAME} SHARED ${PROJECT_SOURCES})endif()
                          #安装路径install(DIRECTORY "widget/" DESTINATION ${UKUI_DIGITALCLOCK_DATA_DIR})install(FILES "plugin/qmldir" DESTINATION "/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/qt5/qml/org/ukui/digitalclock")install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION  "/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/qt5/qml/org/ukui/digitalclock")

                          数字时钟源码:https://gitee.com/zy-yuan1/ukui-quick-widgets/tree/digital-clock

                          将工程编译安装,包括在/usr/share/ukui/widgets/目录下安装我们的时钟插件,以及 /usr/lib/x86_64-linux-gnu/qt5/qml/org/ukui/digitalclock 路径安装qml module文件(用于在插件中improt插件库文件 libukui-digitalclock.so)。


                          03

                          加载插件到任务栏

                          1.插件加载机制

                          Ukui-quick framework 实现了一套插件的接口定义,使得应用通过配置文件(Config)加载和管理插件。更多信息可以参见源码:

                          https://gitee.com/openkylin/ukui-quick/blob/upstream/framework/config/config.h。

                          配置文件通过专门的配置文件加载器(ConfigLoader)获取,加载器负责初始化配置并处理节点映射等工作。

                            /** ConfigLoader 负责处理配置文件路径映射等功能,从文件系统加载配置文件* 配置文件有两个域:* 根据Domain进行映射:domain {*     Local: ~/.config/org.ukui/appid/id.json*     Global: ~/.config/org.ukui/_ukui-config-global/id.json* }*/
                            static Config *getConfig(const QString &id, Domain domain = Global, const QString &appid = QString());

                            ukui-panel作为一个ukui-quick应用,主界面视图Panel继承自 UkuiQuick::IslandView,构造时定义了id为“panel”,appid为“ukui-panel”,可以确定任务栏的配置文件为~/.config/org.ukui/ukui-panel/panel.json。

                            IslandView 绑定了配置文件Config,可以通过接口读取配置信息。以下是任务栏加载插件的代码,mainView() 为主界面的根视图,通过 mainView()->config() 可以读取配置文件。

                            mainView()->config()->children(QStringLiteral("widgets")) 读取了配置文件的 “widgets” 信息,即任务栏需要加载的插件列表。之后,遍历插件列表,向根视图添加插件。

                              //https://gitee.com/openkylin/ukui-panel/blob/upstream/panel/src/view/panel.cppPanel::Panel(Screen *screen, const QString &id, QWindow *parent)  : UkuiQuick::IslandView(QStringLiteral("panel"), QStringLiteral("ukui-panel")),    m_id(id){    ......        void Panel::initWidgets()    {        // 加载Widget        QStringList allWidgets;        UkuiQuick::ConfigList children = mainView()->config()->children(QStringLiteral("widgets"));        for (const auto &config : children) {        const QString widgetId = config->getValue(QStringLiteral("id")).toString();        allWidgets << widgetId;
                                      if (m_disabledWidgets.contains(widgetId)) {            continue;        }        // 向视图添加插件的widget        mainView()->addWidget(widgetId, config->id().toInt());    }        ......    }}

                              2.插件加载示例

                              我们已经知道任务栏的配置文件为panel.json ,存储了任务栏加载的插件信息。接下来,把添加数字时钟“org.ukui.digitalclock”添加到在 panel.json 中的 “widgets” 模块,即可实现把数字时钟加载到任务栏上。

                              "widgetsOrder"代表插件顺序,数字为widgets instanceId。加载数字时钟插件前,任务栏左侧依次加载了开始菜单、搜索、多任务视图等。

                              openKylin(开放麒麟)

                              我们将"org.ukui.digitalclock"的"instanceId"设置10,并将其添加到"widgetsOrder"中,如下所示:

                                //任务栏配置文件 .config/org.ukui/ukui-panel/panel.json //“widgets” 模块"widgets": [                {                    "id": "org.ukui.menu.starter",                    "instanceId": 0                },                {                    "id": "org.ukui.panel.search",                    "instanceId": 1                },                {                    "id": "org.ukui.panel.taskView",                    "instanceId": 2                },                                ......                                {                    "id": "org.ukui.digitalclock",                    "instanceId": 10                }            ],            "widgetsOrder": [                0,                1,                10,                2,                                ......                                       ]        }    ],

                                加载数字时钟插件后的任务栏:

                                openKylin(开放麒麟)

                                进一步地,我们为数字时钟添加背景图片。可以将背景图片,可以将背景图片放在 plugin/res 文件夹,并添加为资源文件。

                                  #CMakeLists.txt#添加资源文件,需要注意的是,在插件注册时要使用 Q_INIT_RESOURCE(res) 来初始化资源qt5_add_resources(QRC_FILES plugin/res.qrc)
                                  if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)    add_library(${PROJECT_NAME} SHARED MANUAL_FINALIZATION ${QRC_FILES})else()    add_library(${PROJECT_NAME} SHARED ${QRC_FILES})endif()

                                  在插件注册时使用 Q_INIT_RESOURCE(res) 来初始化资源:

                                    //ukui-digitalclock-plugin.cpp......
                                    void UkuiDigitalclockPlugin::registerTypes(const char *uri){    ......    Q_INIT_RESOURCE(res) }

                                    在 DigitalClockItem.qml中添加以下代码,为数字时钟添加背景图片。

                                      //DigitalClockItem.qml        
                                      //背景图片    Image {        id: backgroundImg        anchors.fill: parent        fillMode: Image.Stretch        source: "qrc:///res/background.png"        smooth: true        visible: false    }   
                                       //圆角遮罩    Rectangle {        id: mask        anchors.centerIn: parent        
                                       //宽高各减2是为了显示数字时钟的1像素边框        width: parent.width - 2        height: parent.height - 2        radius: 6        visible: false    }   
                                        //遮罩后的图片    OpacityMask {        anchors.centerIn: parent        width: parent.width - 2        height: parent.height - 2        source: backgroundImg        maskSource: mask    }
                                      成果展示:
                                      openKylin(开放麒麟)

                                      各位小伙伴,你学会了吗?更多疑问,可在文章下方留言咨询哦~