NEWS

新闻

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

NEWS

Learn about the latest news.

【小白课程】UKUI开始菜单插件开发指南

2024-07-23 10:09:50
一、简介


在openKylin UKUI 4.10版本中,开始菜单使用QML重写,拥有流畅过渡动效的同时也增加了许多新功能,如文件夹分类功能、收藏功能、显示最近使用文件功能等。今天我们主要介绍开始菜单新增的扩展插件开发功能。
openKylin(开放麒麟)
如上图所示,收藏、最近、AI助手都是开始菜单的插件。新版本的开始菜单会加载已经安装的插件,并在右侧留出较大的区域用于显示插件界面、供给插件交互。
也就是说,不管你是开发者还是用户,只要按照开始菜单的插件规则来开发,就可以把你自己想要的功能搬到开始菜单上!你可以按照自己的丰富想象力来定制属于你的开始菜单。

那我们该怎样才能开发一个符合规则的插件并显示在开始菜单上呢?接下来,我将以一个简单的提醒事项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_OBJECTpublic:    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_OBJECTpublic:    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_OBJECTpublic:    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"

                import QtQuick 2.15import org.ukui.menu.extension 1.0
                UkuiMenuExtension {    id: root     Rectangle {        anchors.fill: parent        color: "lightyellow"    }}

                其中的UkuiMenuExtension组件就是开始菜单用于加载插件QML界面的组件,源代码如下:

                  import QtQuick 2.0
                  Item { //在ExtensionDemo类中我们通过data()方法传的数据存在此处    property var extensionData;    property Component extensionMenu: null; //可以通过此方法将操作传回ExtensionDemo类的receive()方法    signal send(var data);}


                  5.补全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")

                    进行安装之后我们重启开始菜单,会发现我们的插件已经添加成功啦!

                    openKylin(开放麒麟)


                    6.功能补全

                    接下来我们再对Demo的功能做进一步的补全,毕竟我们要做一个带提醒事项功能的Demo。现在,我们继承QAbstractListModel实现一个数据model用于储存提醒事项的数据,如尺寸(大中小)、内容等。部分实现如下:

                      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()函数传给开始菜单:

                           qRegisterMetaType<ExtensionDemoModel *>("ExtensionDemoModel*");    m_extensionDemoModel = new ExtensionDemoModel(this);    m_data.insert("extensionDemoModel", QVariant::fromValue(m_extensionDemoModel));

                        将QML界面丰富一下,使用GridLayout + Repeater的方法实现不同大小的项目之间的补全和拖拽,并通过TextInput来实现编辑功能。部分实现如下:

                          ... ...
                          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文件中添加上刚刚写好的类文件

                            set(SOURCE        extension-demo.cpp extension-demo.h        extension-demo-model.cpp extension-demo-model.h        demo-item.cpp demo-item.h        )


                            7.成果展示

                            再次重新安装,这时候我们的Demo就完成啦!

                            openKylin(开放麒麟)

                            可以进行编辑:

                            openKylin(开放麒麟)

                            可以拖拽换位置:

                            openKylin(开放麒麟)

                            三、补充说明


                            1.插件菜单接口

                            示例Demo并没有用到右键菜单,也就是ContextMenuExtension类。通过继承并实现该类,我们可以将插件所需要的菜单项传递给开始菜单,从而实现右键菜单功能。ContextMenuExtension源码如下:

                              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函数的优化

                              在上述Demo中,我们的很多操作并没有通过UkuiMenuExtension send() -> WidgetExtension receive()的方式传递数据,而是在WidgetExtension::data()函数将ExtensionDemoModel类的指针传递后直接调用ExtensionDemoModel中注册的方法:

                                //ExtensionDemoModel Q_INVOKABLE void move(int from, int to); Q_INVOKABLE void setText(int i, QString text);
                                //mainextensionData.extensionDemoModel.move(baseArea.sourceIndex, selectIndex);extensionData.extensionDemoModel.setText(index, text);

                                这种方式更加简洁,同时也避免了数据解析,还降低了开发门槛。