# 基于Qt插件系统客户端 **Repository Path**: Rocowolf/PluginSys_Main ## Basic Information - **Project Name**: 基于Qt插件系统客户端 - **Description**: 使用QML和C++的可下载并动态加载插件的系统 - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 0 - **Created**: 2024-05-02 - **Last Updated**: 2025-03-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Qt插件系统(客户端)
## 简介 基于C++和QML实现的插件系统客户端程序。其主要功能为解析和加载动态库使得应用主体编程和应用功能编程分离。程序由 `主体+核心插件+非核心插件` 构成。主体负责加载和初始化核心插件并且加载本地的非核心插件以及后续插件的动态检测和装载。核心插件有三个分别用于插件依赖项检测、插件QML界面加载和插件联网下载。非核心插件用于用户按照需求实现的功能以及用于扩展非核心插件功能。
## 程序结构 开发结构 - core文件夹:存储所有核心插件(命名格式:windows端:core_xxx.dll,linux端:libcore_xxx.so) - plugins文件夹:存储所有非核心插件(命名格式:windows端:plugin_xxx.dll,linux端:libplugin_xxx.so) - config文件夹:存储所有配置文件 - interface文件夹:存储所有接口头.h文件 - MainQML文件夹:主体应用 - core_DependenceCheck文件夹:核心插件(插件依赖项检测) - core_QMLMenu文件夹:核心插件(QML加载器) - core_ServerPlugin文件夹:核心插件(插件下载器)
打包结构 - app文件夹:存放生成的应用程序 - core文件夹:存储所有核心插件 - plugins文件夹:存储所有非核心插件 - config文件夹:存储所有配置文件
## 使用方法 主界面如下图 * 浅蓝色区域为内容区域,所有插件的QML界面都显示在蓝色区域。所有含QML界面的插件安装后入口图标都会显示在当且页面 * 灰色区域为操作区域,这一部分会一直存在,不会因为加载插件QML界面而消失,目前只实现了 `回退` 功能,点击后会回到当前界面。linux端的程序的按钮排布与windows端不同,linux端下 `回退` 按钮功能改到 `主页` 按钮,右侧的回退按钮改成 `退出` 按钮,其功能为退出程序 ![alt text](image.png) 附带的 `core_ServerPlugin` 插件的界面如下图所示,上方显示的内容由 `/config/core_ServerPlugin.conf` 中设置的数据决定,配置文件写法详细见 `代码注意事项--->ServerPlugin--->配置文件` 。 ![alt text](image-1.png) 下拉中间框的内容区域,直到灰色下拉框出现 `松开以刷新列表` ,松开后向服务器发出刷新信号,刷新后结果为下图所示 ![alt text](image-3.png) 当服务器返回共享文件夹内容时主程序会检查返回项,并标记已经安装的插件为 `已安装` 。点击下载可以开始从服务器下载对应的插件,下载中的时候按钮的文本显示为下载进度 `xxx%` ,下载完成后显示 `安装` ,按下后开始安装插件,如果安装失败会显示 `重新安装` ,等需求合适后点击以重新安装,安装完成后会显示 `已安装`。由于QML与C++交互方面的个人技术原因,卸载含有与QML交互的插件会导致在程序结束后释放的资源再次被释放导致的崩溃,因此没有实现插件的卸载功能。
## 代码注意项 ### MainQML - 执行顺序 1. 加载 `core_QMLMenu` 插件 2. 加载 `core_DependenceCheck` 插件 3. 加载 `core_ServerPlugin` 插件 4. 加载所有非核心插件(`load`) 5. 预验证所有非核心插件(`vfunc_Vertify`) 6. 验证所有非核心插件(`core_DependenceCheck::vfunc_LoadPlugin`) 7. 非核心插件信道初始化(`Interface_UserBase::vfunc_Init`) 8. 所有插件QML界面加载(`QMLMenu::vfunc_InitQML`) 9. 所有插件运行开始(`Interface_Base::vfunc_Run`) - prop_LoaderMap:是一个QMap容器,键是插件的 `fileName` ,值是一个 `QPluginLoader` 类型的共享指针,因此不要在别处析构源对象。在创建 `QSharedPointer` 对象时绑定了Deleter,当使用 `take` 或者 `remove` 等方法将其从 `QMap` 中移除的时候会调用插件的 `vfunc_Exit` 以及迭代销毁依赖插件并且释放插件资源 ### DependenceCheck - 检测原理:将所有要检测的插件读进来放入待检测列表,进行循环检测。 1. 如果当前插件不需要依赖的插件则将其放入 `prop_PluginList` 作为通过检测的插件 2. 如果当前插件有需要依赖的插件并且 `prop_PluginList` 中不含所需插件但是待检测列表含有则先跳过该插件的检测 3. 如果当前插件有需要依赖的插件并且 `prop_PluginList` 中不含所需插件并且待检测列表也不含有则认为该插件缺少依赖项,移除待检测列表 - 检测缺陷:由于是线性检测,所以无法处理循环依赖的情况,为了防止出现循环依赖造成程序卡死的情况,设定循环检测最大次数 `row = 30` ,这个值可以根据需求设定。 - 检测记忆: 在插件下载完成后安装时也会调用该核心模块的检测函数,此时并不会重新检测之前通过的插件,之前通过的插件会存储在 `prop_PluginList` 中直接作为通过插件来检测新插件 ### ServerPlugin - 提示: 该模块属于学习性模块,该模块使用原生TCP并自定了一个协议以及写了一个简单的异步下载任务队列,不能保证性能,如果可能还是自己实现一个下载模块以及任务队列,并且与服务器的交互协议是自定的,如果需要配套的服务端程序见插件系统配套服务器程序 - 配置文件: 在 `/config/core_ServerPlugin.conf` 中获取插件配置信息 { "conf_downloadpath": "plugins文件夹路径,以/结尾", "conf_serverip": "服务器ip", "conf_serverport": 服务器端口(int) } - 下载逻辑: 1. 创建下载任务 2. 推入或等待推入下载队列 3. 创建TCP连接,当连接成功发送一个JSON对象 { "client_confirm": true, "source_name": " 请求的文件名,如果是刷新列表则为'ls' " } 4. 如果请求成功 - 如果请求的是刷新列表:则返回一个JSON数组,每个元素是一个JSON对象,然后断开TCP连接 { "source_name": "文件名称", "source_size": 文件大小(B) } - 如果请求的是下载文件:则返回一个JSON对象,判断 `source_state` 是否为0,判断 `source_size` 是否为不为0,判断 `source_name` 与请求文件名是否一致,如果有一项不合要求则断开连接。 { "source_state": 0, "source_size": 文件大小, "source_name: 文件名称 } 如果请求的文件不存在则直接断开TCP连接 验证通过则返回一个JSON对象 { "client_state": "ok", "source_name": "文件名称", "client_confirm": true } 5. 对于下载请求,如果验证成功服务器开始返回文件二进制数据 - 下载相关信号: 提前开启三个线程(继承自QThread的class_Thread类型对象),并且当其无负载时会进行睡眠(class_Thread对象每100ms检测一次标记`prop_Pause`),当有新的下载任务时会停止定时器并且将标记值为`false`。并且绑定状态信号 1. vsig_Buffered:用于数据下载,下载完成时发出的信号 2. vsig_Update:用于文件下载,用以发布下载进度 3. vsig_Aborted:当服务端或监听端断开下载连接时会触发 4. vsig_Downloaded:用于文件下载,表示文件下载完成并且调用了`QFile::close`关闭文件,用于提示主程序可以开始安装插件 5. vsig_Installed:当主程序安装完成后返回的信号,如果第二个参数为false表示安装失败,true表示安装成功 如果要重写该模块,则模块主要需要实现下面信号,下面信号是提供给主函数的 6. vsig_Install:在主程序中被绑定的信号,当主程序接收到该信号则开始安装插件,该信号需要在下载文件调用close后触发 - 多通道下载:开启了三个下载通道进行同步下载,可以根据需求修改开启的线程。
## 跨平台 Windows端:master分支 Linux端:linux分支
## 配套程序 - ### 服务端 https://gitee.com/Rocowolf/PluginSys_Server.git - master分支:使用Qt写的服务端程序
- ### 监听后台服务 https://gitee.com/Rocowolf/PluginSys_Controller.git - master分支:使用`JavaScript`写的http服务端接口,内部通过IPC连接服务端
- ### 监听前端界面 https://gitee.com/Rocowolf/PluginSys_WebClient.git - master分支:使用`Vue3` + `Element Plus` + `Vite`写的Web客户端
## 二次开发 ### `Interface_Base` 1. `vfunc_GetInformation()`:返回一个 `QJsonObject` 类型的插件信息,结构为 { plugin_name: "插件文件的BaseName", plugin_version: "插件文件的版本号", plugin_dependence: 核心插件此属性无效,非核心插件传入所有依赖的插件的文件名(带.dll)的QJsonArray plugin_qml: { //如果核心插件含有QML界面则实现此属性否则不应该添加该属性 qml_name: "被QMLMenu渲染出来的名字", qml_qmlurl: "QML界面url", qml_imgurl: "QML界面图标,不给则使用默认图标", qml_pluginname: "和plugin_name名称相同" } } 2. `vfunc_Vertify(QPluginLoader*)`:传入的是在主体程序中实例化的插件加载器,可以用于绑定一些信号。同时核心模块可以在该方法中预检测运行环境来选择是否进行启动,返回 `true` 表示正常启动,返回 `false` 表示停止启动 3. `vfunc_Run()`:当插件加载(`load`)并检测(`vfunc_Vertify`)完成后执行的函数 4. `vfunc_Exit()`: 当插件卸载时执行的函数 5. `vfunc_Debug()`:开发时用于debug的函数 ### `Interface_UserBase` (继承自`public Interface_Base`) 1. `vfunc_Init(const QMap>*)`:该函数会在所有插件被加载并验证完成后执行,传入所有加载并验证的插件,用于绑定一些信号作为插件间的直接信息传递。 2. `vsig_Unload(QString)`:未启用 ### 核心插件 1. 命名:core_xxx 2. 接口:继承自 `interface_Base.h` 中的 `Interface_Base` 类 3. 存放:core文件夹 4. 开发:核心模块不会和非核心模块一样可以被自动处理,只会被读取,需要直接在 `MainQML` 中的 `class_PluginManager` 中添加模块加载、注册和验证 ### 非核心插件 1. 命名:plugin_xxx 2. 接口:继承自 `interface_UserBase.h` 中的 `Interface_UserBase` 类 3. 存放:plugins文件夹 4. 开发:程序自动处理 5. 参考:插件 `plugin_SerialServer` ,以及插件的插件 `plugin_ModbusProtocol`
## 缺陷以及思路 1. 对于含有与QML交互需求的插件无法做到正常卸载,因为使用 `Q_PROPERTY` 或是使用 `qmlRegisterSingletonInstance` 等注册C++内容到QML后如果卸载插件,那么在程序结束后会导致再一次释放注册的资源导致程序崩溃,同时安装之前被卸载的插件也会导致失败。 或者换个思路,将所有的界面都封装为核心组件,然后逻辑封装为非和新组件,如 `plugin_SerialServer` 和 `plugin_ModbusProtocol` 两个插件,前者做界面并且暴漏相关接口(渲染数据等),后者进行逻辑处理(获取、处理数据)。这种处理是可以成功卸载逻辑组件的。
2. 由于ServerPlugin插件是一个学习时制作的插件,只是单纯实现下载功能,使用了无附加协议的TCP连接,因此一些小问题没有修改 - ServerPlugin的QML界面中由于没有将下载的文件状态进行绑定,导致按键状态会因为 `ListView` 的上下移动而导致重置,准备在 `ListView` 中添加一个 `List` 将活跃的文件进行记录反射回QML - ServerPlugin的刷新插件列表功能和文件下载共用下载队列,因此如果刷新信号也会排队,例如当前面有大于等于三个下载项目时并不会刷新列表,并且会进入模态状态,等到排到刷新列表任务时才会进行刷新操作,这个可以在ServerPlugin中新开一个下载线程并独立于文件下载队列来实现