一、简介
在ukui4.0中,开始菜单使用qml重写,拥有流畅过渡动效的同时也增加了许多新功能,如文件夹分类功能、收藏功能、显示最近使用文件功能等。此处我们主要介绍开始菜单新增的扩展插件开发功能(开始菜单版本:4.10)。

如上图所示,收藏、最近、AI助手都是开始菜单的插件。新版本的开始菜单会加载已经安装的插件,并在右侧留出了较大的区域用于显示插件界面、供给插件交互。
也就是说,不管你是开发者还是用户,只要按照开始菜单的插件规则来开发,就可以把你自己想要的功能搬到开始菜单上!你可以按照自己的丰富想象力来定制属于你的开始菜单,而不用反复给开发组提意见。
听起来很美好,那我们该怎样才能开发一个符合规则的插件并显示在开始菜单上呢?接下来,我将以一个简单的提醒事项demo为例,详细介绍一下开始菜单插件开发的流程。
二、开发示例
开始菜单的源码位于https://gitee.com/openkylin/ukui-menu,插件接口的源码在该项目的src/extension目录下,示例插件demo的代码位于https://gitee.com/youdiansaodongxi/menu-extension-demo,感兴趣的小伙伴可以自行阅览。
2.1 安装开发依赖
安装libukui-menu-dev软件包,并在cmake中找到对应依赖
C++ find_package(ukui-menu REQUIRED) |
2.2 继承MenuExtensionPlugin
基于QtPlugin机制,MenuExtensionPlugin类定义了插件需要实现的标准接口:
C++ class Q_DECL_EXPORT MenuExtensionPlugin : public QObject { Q_OBJECT public: explicit MenuExtensionPlugin(QObject *parent = nullptr); ~MenuExtensionPlugin() override;
/** * 插件的唯一id,会被用于区分插件 * @return 唯一id */ virtual QString id() = 0;
/** * 创建一个Widget扩展 * @return 返回nullptr代表不生产此插件 */ virtual WidgetExtension *createWidgetExtension() = 0;
/** * 创建上下文菜单扩展 * @return 返回nullptr代表不生产此插件 */ virtual ContextMenuExtension *createContextMenuExtension() = 0; };
|
我们需要继承并实现这个类,类名称为ExtensionDemoPlugin,头文件及部分实现如下
C++ //头文件 class ExtensionDemoPlugin : public UkuiMenu::MenuExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID UKUI_MENU_EXTENSION_I_FACE_TYPE FILE "metadata.json") Q_INTERFACES(UkuiMenu::MenuExtensionPlugin) public: ~ExtensionDemoPlugin() override; QString id() override; UkuiMenu::WidgetExtension *createWidgetExtension() override; UkuiMenu::ContextMenuExtension *createContextMenuExtension() override; };
//部分实现 QString ExtensionDemoPlugin::id() { //插件的id return "extension-demo"; }
UkuiMenu::WidgetExtension *ExtensionDemoPlugin::createWidgetExtension() { //继承自WidgetExtension类,用于实现窗口功能 return new ExtensionDemo; }
UkuiMenu::ContextMenuExtension *ExtensionDemoPlugin::createContextMenuExtension() { //此demo不需要右键菜单,所以此处返回空指针 return nullptr; } |
在头文件中需要用Q_INTERFACES宏来声明接口类,并且传入插件的抽象接口类名称(UkuiMenu::MenuExtensionPlugin)。
Q_PLUGIN_METADATA声明自定义插件的元数据信息,FILE 是可选的,并指向一个 json 文件,其中包含插件的类型信息和版本信息,其中版本信息需要和UKUI_MENU_EXTENSION_I_FACE_TYPE宏一致,我们新建一个文件命名为metadata.json
C++ { "Type": "UKUI_MENU_EXTENSION", "Version": "1.0.2" } |
2.3 继承WidgetExtension
WidgetExtension类用于实现可显示UI的插件(显示内容),源代码如下:
C++ class WidgetExtension : public QObject { Q_OBJECT public: explicit WidgetExtension(QObject *parent = nullptr); /** * index是插件显示在开始菜单上的顺序 * index为-1会排序到最后一个 * 如果有多个插件的index为-1,则这些插件均排在最后,顺序随机 */ virtual int index() const; /** * 用于储存插件的信息 * 包括Id,Icon,Name,Tooltip,Version,Description,Main,Type,Flag,Data */ virtual MetadataMap metadata() const = 0;
/** * 将插件需要访问的数据上传给开始菜单 * 插件可上传model等数据,用于插件的交互 */ virtual QVariantMap data(); /** * 可在qml界面中将操作通过send方法发送 * 发送后的操作将通过此函数传递给插件 */ virtual void receive(const QVariantMap &data);
Q_SIGNALS: void dataUpdated(); }; |
我们需要继承并实现这个类, 类名称为ExtensionDemo,也就是之前我们在ExtensionDemoPlugin类中通过UkuiMenu::WidgetExtension *createWidgetExtension()方法返回的类。
头文件及部分实现如下
C++ //头文件 class ExtensionDemo : public UkuiMenu::WidgetExtension { Q_OBJECT public: explicit ExtensionDemo(QObject *parent = nullptr); ~ExtensionDemo() override;
int index() const override; UkuiMenu::MetadataMap metadata() const override; QVariantMap data() override; void receive(const QVariantMap &data) override;
private: UkuiMenu::MetadataMap m_metadata; QVariantMap m_data; };
//部分实现 ExtensionDemo::ExtensionDemo(QObject *parent) : WidgetExtension(parent) { //将所需的插件信息储存在m_metadata中 m_metadata.insert(UkuiMenu::WidgetMetadata::Id, "extension-demo"); m_metadata.insert(UkuiMenu::WidgetMetadata::Name, tr("Extension Demo")); m_metadata.insert(UkuiMenu::WidgetMetadata::Tooltip, tr("Demo")); m_metadata.insert(UkuiMenu::WidgetMetadata::Version, "1.0.0"); m_metadata.insert(UkuiMenu::WidgetMetadata::Description, "extension-demo"); //此qrc文件路径不能和其他插件相同,否则会出现加载错误,可以给自己的qrc文件路径添加特有的前缀 m_metadata.insert(UkuiMenu::WidgetMetadata::Main, "qrc:///main.qml"); m_metadata.insert(UkuiMenu::WidgetMetadata::Type, UkuiMenu::WidgetMetadata::Widget); m_metadata.insert(UkuiMenu::WidgetMetadata::Flag, UkuiMenu::WidgetMetadata::OnlySmallScreen); }
int ExtensionDemo::index() const { //我们的插件序号 return 2; }
UkuiMenu::MetadataMap ExtensionDemo::metadata() const { return m_metadata; }
QVariantMap ExtensionDemo::data() { return m_data; } |
2.4 编写qml界面
然后,我们再简单的写一个qml文件当作窗口,放在对应的路径下(也就是上文代码中的"qrc:///main.qml")
JavaScript import QtQuick 2.15 import org.ukui.menu.extension 1.0
UkuiMenuExtension { id: root Rectangle { anchors.fill: parent color: "lightyellow" } } |
其中的UkuiMenuExtension组件就是开始菜单用于加载插件qml界面的组件,源代码如下
JavaScript import QtQuick 2.0
Item { //在ExtensionDemo类中我们通过data()方法传的数据存在此处 property var extensionData; property Component extensionMenu: null; //可以通过此方法将操作传回ExtensionDemo类的receive()方法 signal send(var data); } |
2.5 补全cmake文件并安装
这时候我们的插件框架已经基本写好啦!然后我们还需要将插件安装在对应的系统目录下。cmake文件为例:
CMake cmake_minimum_required(VERSION 3.16) project(extension-demo)
set(EXTENSION_NAME "extension-demo")
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui Quick REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Quick REQUIRED)
# 添加ukui-menu依赖 find_package(ukui-menu REQUIRED)
set(SOURCE extension-demo.cpp extension-demo.h )
set(QRC_FILES qml/qml.qrc)
add_library(${EXTENSION_NAME} SHARED ${SOURCE} ${QRC_FILES}) target_link_libraries(${EXTENSION_NAME} PRIVATE Qt5::Core Qt5::Gui Qt5::Quick # 链接ukui-menu ukui-menu ) # 安装路径 install(TARGETS ${EXTENSION_NAME} LIBRARY DESTINATION "/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/ukui-menu/extensions")
|
进行安装之后我们重启开始菜单,会发现我们的插件已经添加成功啦
2.6 功能补全
接下来我们再对demo的功能做进一步的补全,毕竟我们要做一个带提醒事项功能的demo。
我们继承QAbstractListModel实现一个数据model用于储存提醒事项的数据,如尺寸(大中小)、内容等。部分实现如下
C++ ExtensionDemoModel::ExtensionDemoModel(QObject *parent) : QAbstractListModel(parent) { //存一些初始数据 m_item.append(new DemoItem(1, 1, "UKUI 4.0 整体界面设计简洁")); m_item.append(new DemoItem(1, 1, "拒绝冗余;友好的交互设计")); m_item.append(new DemoItem(1, 2, "让小白用户也可以轻松上手")); m_item.append(new DemoItem(2, 2, "开机速度、内存占用和续航能力大幅优化")); m_item.append(new DemoItem(1, 1, "长时间运行能够保持流畅")); m_item.append(new DemoItem(1, 1, "保障您每日能够愉悦地使用")); m_item.append(new DemoItem(1, 2, "适配多种架构平台和 Linux 桌面环境")); m_item.append(new DemoItem(2, 2, "支持多种主题切换")); }
... ...
//用于事项标签之间的拖拽换位 void ExtensionDemoModel::move(int from, int to) { if (from == to) return; if (from > to) { beginMoveRows(QModelIndex(), from, from, QModelIndex(), to); } else { beginMoveRows(QModelIndex(), from, from, QModelIndex(), to + 1); } m_item.move(from, to); endMoveRows(); } //提醒事项可以编辑 void ExtensionDemoModel::setText(int i, QString text) { if (i < m_item.length()) { m_item[i]->setText(text); dataChanged(index(i, 0), index(i, 0), {Text}); } } |
将这个model在ExtensionDemo中实例化,并通过data()函数传给开始菜单
C++ qRegisterMetaType<ExtensionDemoModel *>("ExtensionDemoModel*"); m_extensionDemoModel = new ExtensionDemoModel(this); m_data.insert("extensionDemoModel", QVariant::fromValue(m_extensionDemoModel)); |
将qml界面丰富一下,使用GridLayout + Repeater的方法实现不同大小的项目之间的补全和拖拽,并通过TextInput来实现编辑功能。部分实现如下:
JavaScript ... ...
UkuiMenuExtension { id: root
MouseArea { anchors.fill: parent
Rectangle { ... ...
ScrollView { anchors.fill: parent anchors.margins: 10 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
GridLayout { id: gridLayout flow: GridLayout.LeftToRight height: rows * baseArea.standardItemHeight
columns: 2 rowSpacing: baseArea.standardItemSpacing columnSpacing: baseArea.standardItemSpacing
Repeater {
model: DelegateModel { id: itemModel model: extensionData.extensionDemoModel
delegate: DropArea { property int vIndex: DelegateModel.itemsIndex
width: model.Column * baseArea.standardItemWidth + (model.Column - 1) * baseArea.standardItemSpacing height: model.Row * baseArea.standardItemHeight + (model.Row - 1) * baseArea.standardItemSpacing Layout.rowSpan: model.Row Layout.columnSpan: model.Column
onDropped: { } onEntered: { itemModel.items.move(drag.source.selectIndex, vIndex) } onExited: { }
Binding { target: control; property: "selectIndex"; value: vIndex}
Item { id: controlBase anchors.fill: parent
MouseArea { id: control property int selectIndex property bool isDrag: false anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton
Drag.active: drag.active Drag.hotSpot.x: width / 2 Drag.hotSpot.y: height / 2 Drag.dragType: Drag.Automatic Drag.onActiveChanged: { if (Drag.active) { control.isDrag = true; exited(); } }
onClicked: { } onPressed: { if (mouse.button === Qt.LeftButton) { x = control.mapToItem(baseArea,0,0).x; y = control.mapToItem(baseArea,0,0).y; drag.target = control; control.grabToImage(function(result) { control.Drag.imageSource = result.url; }) baseArea.sourceIndex = vIndex; } } onReleased: { Drag.drop(); drag.target = null; control.parent = controlBase; control.isDrag = false x = 0; y = 0; } Drag.onDragFinished: { extensionData.extensionDemoModel.move(baseArea.sourceIndex, selectIndex); } ... ... } } } } } } } } } }
|
再在cmake文件中添加上刚刚写好的类文件
CMake set(SOURCE extension-demo.cpp extension-demo.h extension-demo-model.cpp extension-demo-model.h demo-item.cpp demo-item.h ) |
2.7 成果展示
再重新安装一下,我们的demo就完成啦

可以进行编辑

可以拖拽换位置
三、补充说明
3.1 插件菜单接口
示例demo并没有用到右键菜单,也就是ContextMenuExtension类。通过继承并实现该类,我们可以将插件所需要的菜单项传递给开始菜单,从而实现右键菜单功能。ContextMenuExtension源码如下:
C++ class ContextMenuExtension { public: virtual ~ContextMenuExtension() = default; /** * 控制菜单项显示在哪个位置 * 对于第三方项目们应该选择-1,或者大于1000的值 * @return -1:表示随机放在最后 */ virtual int index() const;
/** * 根据data生成action,或者子菜单 * * @param data app信息 * @param parent action最终显示的QMenu * @param location 请求菜单的位置 * @param locationId 位置的描述信息,可选的值有:all,category,letterSort和favorite等插件的id * @return */ virtual QList<QAction*> actions(const DataEntity &data, QMenu *parent, const MenuInfo::Location &location, const QString &locationId) = 0; }; |
3.2 WidgetExtension类receive函数的优化
在上述demo中,我们的很多操作并没有通过UkuiMenuExtension send() -> WidgetExtension receive()的方式传递数据。而是在WidgetExtension::data()函数将ExtensionDemoModel类的指针传递后直接调用ExtensionDemoModel中注册的方法:
C++ //ExtensionDemoModel Q_INVOKABLE void move(int from, int to); Q_INVOKABLE void setText(int i, QString text); //main extensionData.extensionDemoModel.move(baseArea.sourceIndex, selectIndex); extensionData.extensionDemoModel.setText(index, text); |
这种方式更加简洁,同时也避免了数据解析,还降低了开发门槛