【干货分享】ukui-quick-widget桌面插件开发指南
在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插件以一个目录的形式安装到系统中,首先,我们来看看插件的目录结构。
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.txt
cmake_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.qml
import 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.qml
import QtQuick 2.15
import 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.txt
set(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
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.cpp
Panel::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。加载数字时钟插件前,任务栏左侧依次加载了开始菜单、搜索、多任务视图等。
我们将"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, ...... ] } ],
加载数字时钟插件后的任务栏:
进一步地,我们为数字时钟添加背景图片。可以将背景图片,可以将背景图片放在 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 }
各位小伙伴,你学会了吗?更多疑问,可在文章下方留言咨询哦~