【小白课程】UKUI开始菜单插件开发指南
那我们该怎样才能开发一个符合规则的插件并显示在开始菜单上呢?接下来,我将以一个简单的提醒事项demo为例,详细介绍一下开始菜单插件开发的流程。
UKUI 4.10开始菜单源码地址如下:
https://gitee.com/openkylin/ukui-menu
其中,插件接口的源码在该项目的src/extension目录下,示例插件Demo源码地址如下,感兴趣的小伙伴可自行查阅:
https://gitee.com/iaom/menu-extension-demo
1.安装开发依赖
安装libukui-menu-dev软件包,并在cmake中找到对应依赖:
find_package(ukui-menu REQUIRED)
2.继承MenuExtensionPlugin
基于QtPlugin机制,MenuExtensionPlugin类定义了插件需要实现的标准接口:
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,头文件及部分实现如下:
//头文件
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:
{"Type": "UKUI_MENU_EXTENSION","Version": "1.0.2"}
3.继承WidgetExtension
WidgetExtension类用于实现可显示UI的插件(显示内容),源代码如下:
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()方法返回的类。
其中,头文件及部分实现如下:
//头文件
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;
}
4.编写QML界面
接下来,我们再简单的写一个QML文件当作窗口,放在对应的路径下(也就是上文代码中的"qrc:///main.qml")
其中的UkuiMenuExtension组件就是开始菜单用于加载插件QML界面的组件,源代码如下: 这时候我们的插件框架已经基本写好啦!然后我们还需要将插件安装在对应的系统目录下。cmake文件为例: 进行安装之后我们重启开始菜单,会发现我们的插件已经添加成功啦! 接下来我们再对Demo的功能做进一步的补全,毕竟我们要做一个带提醒事项功能的Demo。现在,我们继承QAbstractListModel实现一个数据model用于储存提醒事项的数据,如尺寸(大中小)、内容等。部分实现如下: 将这个model在ExtensionDemo中实例化,并通过data()函数传给开始菜单: 将QML界面丰富一下,使用GridLayout + Repeater的方法实现不同大小的项目之间的补全和拖拽,并通过TextInput来实现编辑功能。部分实现如下: 再在cmake文件中添加上刚刚写好的类文件 再次重新安装,这时候我们的Demo就完成啦! 可以进行编辑: 可以拖拽换位置: 示例Demo并没有用到右键菜单,也就是ContextMenuExtension类。通过继承并实现该类,我们可以将插件所需要的菜单项传递给开始菜单,从而实现右键菜单功能。ContextMenuExtension源码如下: 在上述Demo中,我们的很多操作并没有通过UkuiMenuExtension send() -> WidgetExtension receive()的方式传递数据,而是在WidgetExtension::data()函数将ExtensionDemoModel类的指针传递后直接调用ExtensionDemoModel中注册的方法: 这种方式更加简洁,同时也避免了数据解析,还降低了开发门槛。import QtQuick 2.15
import org.ukui.menu.extension 1.0
UkuiMenuExtension {
id: root
Rectangle {
anchors.fill: parent
color: "lightyellow"
}
}
import QtQuick 2.0
Item {
//在ExtensionDemo类中我们通过data()方法传的数据存在此处
property var extensionData;
property Component extensionMenu: null;
//可以通过此方法将操作传回ExtensionDemo类的receive()方法
signal send(var data);
}
5.补全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")
6.功能补全
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});
}
}
qRegisterMetaType<ExtensionDemoModel *>("ExtensionDemoModel*"); m_extensionDemoModel = new ExtensionDemoModel(this); m_data.insert("extensionDemoModel", QVariant::fromValue(m_extensionDemoModel));
... ...
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);
}
... ...
}
}
}
}
}
}
}
}
}
}
set(SOURCE extension-demo.cpp extension-demo.h extension-demo-model.cpp extension-demo-model.h demo-item.cpp demo-item.h )
7.成果展示
1.插件菜单接口
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;
};
2. WidgetExtension类receive函数的优化
//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);