最近公司需要补充一些项目的文档,我负责的几个项目中比较有实用价值的是这个 dde-dock 插件的开发入门教程,这里是转载,原发在 dde-dock 项目源码中。
插件的工作原理
插件是一种在不需要改动并重新编译主程序本身的情况下去扩展主程序功能的一种机制。
dde-dock 插件是根据 Qt 插件标准所开发的共享库文件(so
),通过实现 Qt 的插件标准和 dde-dock 提供的接口,共同完成 dde-dock 的功能扩展。
可以通过以下链接查看关于 Qt 插件更详细的介绍:
https://wiki.qt.io/Plugins
https://doc.qt.io/qt-5/plugins-howto.html
dde-dock 插件加载流程
在 dde-dock 启动时会跑一个线程去检测目录/usr/lib/dde-dock/plugins
下的所有文件,并检测是否是一个正常的动态库文件,如果是则尝试加载。尝试加载即检测库文件的元数据,插件的元数据定义在一个 JSON 文件中,这个后文会介绍,如果元数据检测通过就开始检查插件是否实现了 dde-dock 指定的接口,这一步也通过之后就会开始初始化插件,获取插件提供的控件,进而将控件显示在任务栏上。
接口列表
这里先列出 dde-dock 都提供了哪些接口,可作为一个手册查看,注意,为 dde-dock 编写插件并不是要实现所有接口,这些接口提供了 dde-dock 允许各种可能的功能,插件开发者可以根据自己的需求去实现自己需要的接口。后续的插件示例也将会用到这里列出的部分接口。
接口定义的文件一般在系统的如下位置:
1 2
| /usr/include/dde-dock/pluginproxyinterface.h /usr/include/dde-dock/pluginsiteminterface.h
|
PluginItemInterface
只有标明必须实现
的接口是必须要由插件开发者实现的接口,其他接口如果不需要对应功能可不实现。
PluginsItemInterface 中定义的接口除了displayMode 和 position(历史遗留),从插件的角度来看都是被动的,只能等待被任务栏的插件机制调用。
名称 |
简介 |
pluginName |
返回插件名称,用于在 dde-dock 内部管理插件时使用 必须实现 |
pluginDisplayName |
返回插件名称,用于在界面上显示 |
init |
插件初始化入口函数,参数 proxyInter 可认为是主程序的进程 必须实现 |
itemWidget |
返回插件主控件,用于显示在 dde-dock 面板上 必须实现 |
itemTipsWidget |
返回鼠标悬浮在插件主控件上时显示的提示框控件 |
itemPopupApplet |
返回鼠标左键点击插件主控件后弹出的控件 |
itemCommand |
返回鼠标左键点击插件主控件后要执行的命令数据 |
itemContextMenu |
返回鼠标右键点击插件主控件后要显示的菜单数据 |
invokedMenuItem |
菜单项被点击后的回调函数 |
itemSortKey |
返回插件主控件的排序位置 |
setSortKey |
重新设置主控件新的排序位置(用户拖动了插件控件后) |
itemAllowContainer |
返回插件控件是否允许被收纳 |
itemIsInContainer |
返回插件是否处于收纳模式(仅在 itemAllowContainer 为 true 时有作用) |
setItemIsInContainer |
更新插件是否处于收纳模式的状态(仅在 itemAllowContainer 主 true 时有作用) |
pluginIsAllowDisable |
返回插件是否允许被禁用(默认不允许被禁用) |
pluginIsDisable |
返回插件当前是否处于被禁用状态 |
pluginStateSwitched |
当插件的禁用状态被用户改变时此接口被调用 |
displayModeChanged |
dde-dock 显示模式发生改变时此接口被调用 |
positionChanged |
dde-dock 位置变化时时此接口被调用 |
refreshIcon |
当插件控件的图标需要更新时此接口被调用 |
displayMode |
用于插件主动获取 dde-dock 当前的显示模式 |
position |
用于插件主动获取 dde-dock 当前的位置 |
PluginProxyInterface
由于上面的接口对于插件来说都是被动的,即插件本身无法确定这些接口什么时刻会被调用,很明显这对于插件机制来说是不完整的,因此便有了 PluginProxyInterface,它定义了一些让插件主动调用以控制 dde-dock 的一些行为的接口。PluginProxyInterface 的具体实例可以认为是抽象了的 dde-dock 主程序,或者是 dde-dock 中所有插件的管理员,这个实例将会通过 PluginItemInterface 中的 init
接口传递给插件,因此在上述 init
接口中总是会先把这个传入的对象保存起来以供后续使用。
名称 |
简介 |
itemAdded |
向 dde-dock 添加新的主控件(一个插件可以添加多个主控件它们之间使用ItemKey 区分) |
itemUpdate |
通知 dde-dock 有主控件需要更新 |
itemRemoved |
从 dde-dock 移除主控件 |
requestWindowAutoHide |
设置 dde-dock 是否允许隐藏,通常被用在任务栏被设置为智能隐藏或始终隐藏而插件又需要让 dde-dock 保持显示状态来显示一些重要信息的场景下 |
requestRefreshWindowVisible |
通知 dde-dock 更新隐藏状态 |
requestSetAppletVisible |
通知 dde-dock 显示或隐藏插件的弹出面板(鼠标左键点击后弹出的控件) |
saveValue |
统一的配置保存函数 |
getValue |
统一的配置读取函数 |
构建一个 dde-dock 插件
接下来将介绍一个简单的 dde-dock 插件的开发过程,插件开发者可跟随此步骤熟悉为 dde-dock 开发插件的步骤,以便创造出更多具有丰富功能的插件。
预期功能
首先来确定下这个插件所需要的功能:
- 实时显示 HOME 分区可使用的剩余大小百分比
- 允许禁用插件
- 鼠标悬浮在插件上显示 HOME 分区总容量和可用容量
- 鼠标左键点击插件显示一个提示框显示关于 HOME 分区更详细的信息
- 鼠标右键点击插件显示一个菜单用于刷新缓存和启动 gparted 程序
安装依赖
下面以 Qt + cmake 为例进行说明,以 Deepin 15.9 环境为基础,安装如下的包:
- dde-dock-dev
- cmake
- qtbase5-dev-tools
- pkg-config
项目基本结构
创建必需的项目目录与文件,插件名称叫做home_monitor
,所以创建以下的目录结构:
1 2 3 4 5
| home_monitor ├── home_monitor.json ├── homemonitorplugin.cpp ├── homemonitorplugin.h └── CMakeLists.txt
|
接着来依次分析各个文件的作用。
cmake 配置文件
CMakeLists.txt
是 cmake 命令要读取的配置文件,用于管理整个项目的源文件,依赖,构建等等,其内容如下:
以#
开头的行是注释,用于介绍相关命令,对创建一份新的 CMakeLists.txt 文件会有所帮助,目前可以简单地过一遍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
|
cmake_minimum_required(VERSION 3.11)
set(PLUGIN_NAME "home_monitor")
project(${PLUGIN_NAME})
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
file(GLOB_RECURSE SRCS "*.h" "*.cpp")
find_package(Qt5Widgets REQUIRED) find_package(DtkWidget REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(DdeDockInterface REQUIRED dde-dock)
add_definitions("${QT_DEFINITIONS} -DQT_PLUGIN")
add_library(${PLUGIN_NAME} SHARED ${SRCS} home_monitor.qrc)
set_target_properties(${PLUGIN_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ./)
target_include_directories(${PLUGIN_NAME} PUBLIC ${Qt5Widgets_INCLUDE_DIRS} ${DtkWidget_INCLUDE_DIRS} ${DdeDockInterface_INCLUDE_DIRS} )
target_link_libraries(${PLUGIN_NAME} PRIVATE ${Qt5Widgets_LIBRARIES} ${DtkWidget_LIBRARIES} ${DdeDockInterface_LIBRARIES} )
set(CMAKE_INSTALL_PREFIX "/usr")
install(TARGETS ${PLUGIN_NAME} LIBRARY DESTINATION lib/dde-dock/plugins)
|
元数据文件
home_monitor.json
文件是插件的元数据文件,指明了当前插件所使用的 dde-dock 的接口版本,dde-dock 在加载此插件时,会检测自己的接口版本是否与插件的接口版本一致,当双方的接口版本不一致或者不兼容时,dde-dock 为了安全将阻止加载对应的插件。另外,元数据文件是在源代码中使用特定的宏加载到插件中的。
在 dde-dock 内建的插件代码中,可以找到当前具体的接口版本,目前最新的版本是 1.2
。
另外(可选的)还支持指定一个 dbus 服务,dock 在加载插件时会检查此插件所依赖的 dbus 服务,如果服务没有启动则不会初始化这个插件,直到服务启动,如下表示依赖 dbus 地址为 “com.deepin.daemon.Network” 的 dbus 服务。
1 2 3 4
| { "api": "1.2", "depends-daemon-dbus-service": "com.deepin.daemon.Network" }
|
插件核心类
homemonitorplugin.h
声明了类 HomeMonitorPlugin
,它继承(实现)了前面提到的 PluginItemInterface
,这代表了它是一个实现了 dde-dock 接口的插件。
下面是最小化实现了一个 dock 插件的源码,只实现了必须实现的接口,请注意,下文的代码只是为了简述开发一个插件的主要过程,详细的示例代码应该查看 home-monitor
目录下的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #ifndef HOMEMONITORPLUGIN_H #define HOMEMONITORPLUGIN_H
#include <dde-dock/pluginsiteminterface.h>
#include <QObject>
class HomeMonitorPlugin : public QObject, PluginsItemInterface { Q_OBJECT Q_INTERFACES(PluginsItemInterface) Q_PLUGIN_METADATA(IID "com.deepin.dock.PluginsItemInterface" FILE "home_monitor.json")
public: explicit HomeMonitorPlugin(QObject *parent = nullptr);
const QString pluginName() const override;
void init(PluginProxyInterface *proxyInter) override;
QWidget *itemWidget(const QString &itemKey) override; };
#endif
|
homemonitorplugin.cpp
中包含对应接口的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include "homemonitorplugin.h"
HomeMonitorPlugin::HomeMonitorPlugin(QObject *parent) : QObject(parent) {
}
const QString HomeMonitorPlugin::pluginName() const { return QStringLiteral("home_monitor"); }
void HomeMonitorPlugin::init(PluginProxyInterface *proxyInter) { m_proxyInter = proxyInter; }
QWidget *HomeMonitorPlugin::itemWidget(const QString &itemKey) { Q_UNUSED(itemKey);
return nullptr; }
|
测试插件加载
当插件的基本结构搭建好之后应该测试下这个插件能否被 dde-dock 正确的加载,这时候测试如果有问题也可以及时处理。
从源码构建
为了不污染源码目录,推荐在源码目录中创建 build
目录用于构建:
1 2 3 4 5 6 7 8 9
| cd home_monitor
mkdir build
cd build
cmake ..
make -j4
|
安装
执行下面的命令即可将插件安装到系统中,也是 CMakeList.txt 文件指定的安装位置:
可以看到有home_monitor.so
文件被安装在了 dde-dock 的插件目录。
1
| install -m 755 -p ./home_monitor/libhome_monitor.so /usr/lib/dde-dock/plugins/libhome_monitor.so
|
测试加载
执行 pkill dde-dock; dde-dock
来重新运行 dde-dock,在终端输出中如果出现以下的输出,说明插件的加载已经正常:
1 2 3
| init plugin: "home_monitor"
init plugin finished: "home_monitor"
|
创建插件主控件
创建新文件 informationwidget.h 和 informationwidget.cpp,用于创建控件类:InformationWidget,这个控件用于显示在 dde-dock 上。
此时的目录结构为:
1 2 3 4 5 6 7 8 9
| home_monitor
├── build/ ├── home_monitor.json ├── homemonitorplugin.cpp ├── homemonitorplugin.h ├── informationwidget.cpp ├── informationwidget.h └── CMakeLists.txt
|
informationwidget.h 文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #ifndef INFORMATIONWIDGET_H #define INFORMATIONWIDGET_H
#include <QWidget> #include <QLabel> #include <QTimer> #include <QStorageInfo>
class InformationWidget : public QWidget { Q_OBJECT
public: explicit InformationWidget(QWidget *parent = nullptr);
inline QStorageInfo * storageInfo() { return m_storageInfo; }
private slots: void refreshInfo();
private: QLabel *m_infoLabel; QTimer *m_refreshTimer; QStorageInfo *m_storageInfo; };
#endif
|
informationwidget.cpp 文件包含了对类 InformationWidget 的实现,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include "informationwidget.h"
#include <QVBoxLayout> #include <QTimer> #include <QDebug>
InformationWidget::InformationWidget(QWidget *parent) : QWidget(parent) , m_infoLabel(new QLabel) , m_refreshTimer(new QTimer(this)) , m_storageInfo(new QStorageInfo("/home")) { m_infoLabel->setStyleSheet("QLabel {" "color: white;" "}"); m_infoLabel->setAlignment(Qt::AlignCenter);
QVBoxLayout *centralLayout = new QVBoxLayout; centralLayout->addWidget(m_infoLabel); centralLayout->setSpacing(0); centralLayout->setMargin(0);
setLayout(centralLayout);
connect(m_refreshTimer, &QTimer::timeout, this, &InformationWidget::refreshInfo);
m_refreshTimer->start(10000);
refreshInfo(); }
void InformationWidget::refreshInfo() { const double total = m_storageInfo->bytesTotal(); const double available = m_storageInfo->bytesAvailable(); const int percent = qRound(available / total * 100);
m_infoLabel->setText(QString("Home:\n%1\%").arg(percent)); }
|
现在主控件类已经完成了,回到插件的核心类,将主控件类添加到核心类中。
在 homemonitorplugin.h
中相应位置添加成员声明:
1 2 3 4 5 6 7
| #include "informationwidget.h"
class HomeMonitorPlugin : public QObject, PluginsItemInterface { private: InformationWidget *m_pluginWidget; };
|
然后在 homemonitorplugin.cpp
中将添加成员的初始化,比如在 init
接口中初始化:
1 2 3 4 5 6
| void HomeMonitorPlugin::init(PluginProxyInterface *proxyInter) { m_proxyInter = proxyInter;
m_pluginWidget = new InformationWidget; }
|
添加主控件到 dde-dock 面板上
在插件核心类的 init
方法中获取到了 PluginProxyInterface
对象,调用此对象的 itemAdded
接口即可实现向 dde-dock 面板上添加项目。
第二个 QString
类型的参数代表了本插件所提供的主控件的 id,当一个插件提供多个主控件时,不同主控件之间的 id 要保证唯一。
1 2 3 4 5 6 7 8
| void HomeMonitorPlugin::init(PluginProxyInterface *proxyInter) { m_proxyInter = proxyInter;
m_pluginWidget = new InformationWidget;
m_proxyInter->itemAdded(this, pluginName()); }
|
在调用 itemAdded
之后,dde-dock 会在合适的时机调用插件的itemWidget
接口以获取需要显示的控件。如果插件提供了多个主控件到 dde-dock 上,那么插件核心类应该在 itemWidget 接口中分析参数 itemKey,并返回与之对应的控件对象,当插件只有一个可显示项目时,itemKey 可以忽略 (但不建议忽略)。
1 2 3 4 5 6
| QWidget *HomeMonitorPlugin::itemWidget(const QString &itemKey) { Q_UNUSED(itemKey);
return m_pluginWidget; }
|
现在再根据“测试插件加载”一节中的步骤,编译、安装、重启 dde-dock,就可以看到主控件在 dde-dock 面板上出现了,如下图所示:

支持禁用插件
与插件禁用和启用相关的接口有如下三个:
- pluginIsAllowDisable
- pluginIsDisable
- pluginStateSwitched
故而在插件的核心类头文件中增加这三个接口的声明:
1 2 3
| bool pluginIsAllowDisable() override; bool pluginIsDisable() override; void pluginStateSwitched() override;
|
同时在插件的核心类实现类中增加这三个接口的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| bool HomeMonitorPlugin::pluginIsAllowDisable() { return true; }
bool HomeMonitorPlugin::pluginIsDisable() { return m_proxyInter->getValue(this, "disabled", false).toBool(); }
void HomeMonitorPlugin::pluginStateSwitched() { const bool disabledNew = !pluginIsDisable(); m_proxyInter->saveValue(this, "disabled", disabledNew);
if (disabledNew) { m_proxyInter->itemRemoved(this, pluginName()); } else { m_proxyInter->itemAdded(this, pluginName()); } }
|
此时就会引入一个新的问题,插件允许被禁用,那么在 dde-dock 启动时,插件有可能处于禁用状态,那么在初始化插件时就不能直接将主控件添加到 dde-dock 中,而是应该判断当前是否是禁用状态,修改接口 init 的实现:
1 2 3 4 5 6 7 8 9 10 11
| void HomeMonitorPlugin::init(PluginProxyInterface *proxyInter) { m_proxyInter = proxyInter;
m_pluginWidget = new InformationWidget;
if (!pluginIsDisable()) { m_proxyInter->itemAdded(this, pluginName()); } }
|
重新编译、安装、重启 dde-dock,然后 dde-dock 面板上点击鼠标右键查看“插件”子菜单就会看到空白项,点击它将禁用插件,再次点击则启用插件。
不过为什么是空白项呢?是因为有一个接口还没有实现:pluginDisplayName
在相应文件中分别添加如下内容,来修复这个问题:
1 2 3
|
const QString pluginDisplayName() const override;
|
1 2 3 4 5 6
|
const QString HomeMonitorPlugin::pluginDisplayName() const { return QString("Home Monitor"); }
|

支持 hover tip
“hover tip” 就是鼠标移动到插件主控件上并悬浮一小段时间后弹出的一个提示框,可以用于显示一些状态信息等待,当然具体用来显示什么完全由插件开发者自己决定,要实现这个功能需要接口:
首先在插件核心类中添加一个文本控件作为 tip 控件:
1 2 3 4
| private: InformationWidget *m_pluginWidget; QLabel *m_tipsWidget;
|
在 init 函数中初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
void HomeMonitorPlugin::init(PluginProxyInterface *proxyInter) { m_proxyInter = proxyInter;
m_pluginWidget = new InformationWidget; m_tipsWidget = new QLabel;
if (!pluginIsDisable()) { m_proxyInter->itemAdded(this, pluginName()); } }
|
下面在插件核心类中实现接口 itemTipsWidget:
1 2 3
| public: QWidget *itemTipsWidget(const QString &itemKey) override;
|
1 2 3 4 5 6 7 8 9 10 11 12 13
|
QWidget *HomeMonitorPlugin::itemTipsWidget(const QString &itemKey) { Q_UNUSED(itemKey);
m_tipsWidget->setText(QString("Total: %1G\nAvailable: %2G") .arg(qRound(m_pluginWidget->storageInfo()->bytesTotal() / qPow(1024, 3))) .arg(qRound(m_pluginWidget->storageInfo()->bytesAvailable() / qPow(1024, 3))));
return m_tipsWidget; }
|
dde-dock 在发现鼠标悬停在插件的控件上时就会调用这个接口拿到相应的控件并显示出来。

支持 applet
上面的 tips 显示的控件在鼠标移开之后就会消失,如果插件需要长时间显示一个窗体及时鼠标离开也会保持显示状态来做一些提示或功能的话那就需要使用 applet,applet 控件在左键点击后显示,点击控件以外的其他地方后消失。
applet 控件其实跟 tip 控件一样都是一个普通的 widget,但是可以在 applet 控件中显示交互性的内容,比如按钮,输入框等等。出于篇幅的原因这里 applet 控件就添加交互性的特性了,只用来显示一些文字,所以依然使用一个 lable 控件。
在插件核心类中添加一个文本控件作为 applet 控件:
1 2 3 4 5
| private: InformationWidget *m_pluginWidget; QLabel *m_tipsWidget; QLabel *m_appletWidget;
|
在 init 函数中初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
void HomeMonitorPlugin::init(PluginProxyInterface *proxyInter) { m_proxyInter = proxyInter;
m_pluginWidget = new InformationWidget; m_tipsWidget = new QLabel; m_appletWidget = new QLabel;
if (!pluginIsDisable()) { m_proxyInter->itemAdded(this, pluginName()); } }
|
接着实现 applet 相关的接口 itemPopupApplet:
1 2 3
| public: QWidget *itemPopupApplet(const QString &itemKey) override;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| QWidget *HomeMonitorPlugin::itemPopupApplet(const QString &itemKey) { Q_UNUSED(itemKey);
m_appletWidget->setText(QString("Total: %1G\nAvailable: %2G\nDevice: %3\nVolume: %4\nLabel: %5\nFormat: %6\nAccess: %7") .arg(qRound(m_pluginWidget->storageInfo()->bytesTotal() / qPow(1024, 3))) .arg(qRound(m_pluginWidget->storageInfo()->bytesAvailable() / qPow(1024, 3))) .arg(QString(m_pluginWidget->storageInfo()->device())) .arg(m_pluginWidget->storageInfo()->displayName()) .arg(m_pluginWidget->storageInfo()->name()) .arg(QString(m_pluginWidget->storageInfo()->fileSystemType())) .arg(m_pluginWidget->storageInfo()->isReadOnly() ? "ReadOnly" : "ReadWrite") );
return m_appletWidget; }
|
编译,安装,重启 dde-dock 之后点击主控件即可看到弹出的 applet 控件。

支持右键菜单
增减右键菜单功能需要实现以下两个接口:
- itemContextMenu
- invokedMenuItem
1 2 3 4
| public: const QString itemContextMenu(const QString &itemKey) override; void invokedMenuItem(const QString &itemKey, const QString &menuId, const bool checked) override;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| const QString HomeMonitorPlugin::itemContextMenu(const QString &itemKey) { Q_UNUSED(itemKey);
QList<QVariant> items; items.reserve(2);
QMap<QString, QVariant> refresh; refresh["itemId"] = "refresh"; refresh["itemText"] = "Refresh"; refresh["isActive"] = true; items.push_back(refresh);
QMap<QString, QVariant> open; open["itemId"] = "open"; open["itemText"] = "Open Gparted"; open["isActive"] = true; items.push_back(open);
QMap<QString, QVariant> menu; menu["items"] = items; menu["checkableMenu"] = false; menu["singleCheck"] = false;
return QJsonDocument::fromVariant(menu).toJson(); }
void HomeMonitorPlugin::invokedMenuItem(const QString &itemKey, const QString &menuId, const bool checked) { Q_UNUSED(itemKey);
if (menuId == "refresh") { m_pluginWidget->storageInfo()->refresh(); } else if ("open") { QProcess::startDetached("gparted"); } }
|
编译,安装,重启 dde-dock 之后右键点击主控件即可看到弹出右键菜单。

至此,一个包含基本功能的插件就完成了。