# Data-Work-Flow **Repository Path**: libaineu2004/data-work-flow ## Basic Information - **Project Name**: Data-Work-Flow - **Description**: 以Qt(C++)和python混合编写的数据处理软件,以工作流为基础,实现数据的导入、清洗、分析、绘图、导出等功能,内部主要对pandas进行了封装,实现pandas内置函数的大部分功能,以不懂python得人也可以非常方便的使用pandas功能,同时利用Qt自身绘图能力实现数据的绘图(暂时不上用matplotlib而是用C++的QCustomerPlot) - **Primary Language**: C++ - **License**: LGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 12 - **Created**: 2022-09-15 - **Last Updated**: 2023-05-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 简介 数据工作流设计器 # 编译 编译前请确保已经拉取了第三方库,由于使用的是`git submodule`方式管理大部分第三方库,因此需要执行: ```shell git submodule update --init --recursive ``` 把所有第三方库拉取 ## bin目录 DA项目编译好的二进制文件统一生成到bin_qt$$[QT_VERSION]_{msvc/mingw}_{debug/release}{/_64}目录下,如:使用qt5.14.2, msvc版本debug模式64位编译,将生成`bin_qt5.14.2_msvc_debug_64`文件夹,[common.pri](./src/common.pri)文件定义了DA项目的目录内容: ```shell # DA_BIN_DIR为生成的bin文件夹 ./bin_qt$$[QT_VERSION]_{msvc/mingw}_{debug/release}{/_64} # DA_SRC_DIR为源代码路径:./src DA_3RD_PARTY_DIR = $${DA_SRC_DIR}/3rdparty # 第三方库路径 BIN_APP_BUILD_DIR = $${DA_BIN_DIR}/build_apps # 生成的app路径 BIN_LIB_BUILD_DIR = $${DA_BIN_DIR}/build_libs # 生成的lib路径 BIN_TEST_BUILD_DIR = $${DA_BIN_DIR}/build_tst # 生成的测试程序路径 BIN_PLUGIN_BUILD_DIR = $${DA_BIN_DIR}/build_plugins # 生成的plugin路径 BIN_PLUGIN_DIR = $${DA_BIN_DIR}/plugins #插件的路径 BIN_TEST_DIR = $${DA_BIN_DIR}/tst #测试程序路径 ``` bin_xx目录下的build_libs将是构建的库所在目录,也是第三方库需要放置的目录,下面先讲如何编译第三方库 ## 第三方库编译 首先需要编译第三方库,第三方库位于`src/3rdparty`, 第三方库使用`git submodule`形式进行管理,因此第三方库需要在根目录下(存在`.gitmodules`的目录)执行`git submodule update --init --recursive`进行拉取 用Qt Creator 打开`src/3rdparty/3rdparty.pro`对第三方库进行编译 编译完第三方库后,需要手动把第三方库编译的结果(`.a/.lib`文件)移动到,`bin_xx`文件夹下,把编译的`*.dll`文件移动到`bin_xx`目录下 需要编译的第三方库如下: - SARibbon 用qt creator 打开`./src/3rdparty/SARibbon/SARibbon.pro`进行编译 编译完成后会把`./src/3rdparty/SARibbon/bin_xx`目录下的`*.lib / *.a`文件拷贝到`bin_xx`目录下的`build_libs`文件夹下,把`dll`文件拷贝到`bin_xx`目录下 - Qt-Advanced-Docking-System 用qt creator 打开`./src/3rdparty/Qt-Advanced-Docking-System/ads.pro`进行编译 编译完成后的二进制文件会在`./src/build-3rdparty-Desktop_Qtxx`下的`Qt-Advanced-Docking-System`里,用户根据自己定义的情况查找,找到其lib文件夹下的lib文件和dll文件复制到`build_libs`文件夹和`bin_xx`目录下 - ctk > 本程序使用的ctk是简化版ctk,仅抽取了使用到的几个类,因此称为liteCtk 用qt creator 打开`./src/3rdparty/ctk/ctk.pro`进行编译 此库已经自动配置编译lib和dll的位置,无需手动移动 ## python环境配置 DA依赖python环境: - 至少是python3.7 - python环境需要安装pandas库 把安装好pandas库的python环境整体拷贝到`bin_xx`目录下,并重命名为`Python`,DA默认的python搜索路径就是程序运行目录下的`Python`文件夹,另外需要把`./src/PyScripts`文件夹拷贝到`bin_xx`目录下,这是DA的固定脚本内容 ## 编译程序 用Qt Creator 打开`./src/DataWorkFlow.pro`进行编译,编译过程会自动把文件编译到`bin_xx`目录下 # 程序框架 todo ## 基于插件的架构 ## 基于脚本的交互 ## 总体框架 ## 交互框架 # 逻辑编辑器 逻辑编辑器负责组织描述逻辑,以工作流为基础,DataWorkFlow的工作流以插件模式加载入程序中,主要涉及以下两个基础个模块(模块顺序为依赖顺序):`DAPlugin`、`DANode`,同时`Plugin\DAUtilNodePlugin`为DataWorkFlow的基础节点插件,其他的插件编写可参考此插件编写。 # 插件 ## DAPlugin插件管理模块 `DAPlugin`模块是为了提供一个统一且简单的插件管理功能,此模块提供了插件的基类`DAAbstractPlugin`,作为DataWorkFlow的插件,只要继承此类,即可实现插件。 同时,`DAPlugin`模块提供了所有插件管理的单例:`DAPluginManager`,这是一个单例,可以用来加载和卸载插件,下面是通过此类加载插件的例子: ```cpp //加载插件 DAPluginManager& plugin = DAPluginManager::instance(); if (!plugin.isLoaded()) { plugin.load(); } //获取插件 QList plugins = plugin.getPluginOptions(); for (int i = 0; i < plugins.size(); ++i) { DAPluginOption opt = plugins[i]; if (!opt.isValid()) { continue; } DAAbstractPlugin *p = opt.plugin(); //开始通过dynamic_cast判断插件的具体类型 if (DAAbstractNodePlugin *np = dynamic_cast(p)) { //说明是节点插件 ... qDebug() << tr("succeed load plugin ") << np->getName(); } } ``` `DAPluginOption`维护着每个插件的内容,内部包含了此插件(dll)的一些基本信息 ## 编写一个DAPlugin插件 要实现一个插件,必须在lib中实现两个函数,和实现一个插件类,以`DAUtilNodePlugin`为例,具体可参见`src\Plugin\DAUtilNodePlugin` - 首先继承`DAAbstractNodePlugin`实现自己的插件类,这里为了进一步抽象,定义了节点插件的基类`DAAbstractNodePlugin`,`DAUtilNodePlugin`继承于`DAAbstractNodePlugin` ```cpp class DANODE_API DAAbstractNodePlugin : public DAAbstractPlugin { public: DAAbstractNodePlugin(); virtual ~DAAbstractNodePlugin(); ... }; class DAUtilNodePlugin : public DAAbstractNodePlugin { public: DAUtilNodePlugin(); ~DAUtilNodePlugin(); }; ``` `DAUtilNodePlugin`里将实现具体的功能,这里不描述 - 导出函数`plugin_create`和`plugin_destory`的实现 在实现了`DAUtilNodePlugin`类后,还需要实现两个C标准的导出函数,`plugin_create`和`plugin_destory`: ```cpp extern "C" DAUTILNODEPLUGIN_API DAAbstractPlugin *plugin_create(); extern "C" DAUTILNODEPLUGIN_API void plugin_destory(DAAbstractPlugin *p); ``` 这两个函数是关键,插件系统在加载时需压根据c函数名来查找这两个函数 这两个函数的功能很简单, 就是生成插件,和删除插件,具体实现如下: ```cpp DAAbstractPlugin *plugin_create() { return (new DAUtilNodePlugin()); } void plugin_destory(DAAbstractPlugin *p) { delete p; } ``` 这样,一个插件就能正常运行。 # 工作流 工作流主要的模块为`DANode`,这个模块为工作流的核心和最基本的抽象,此模块主要的几个类为: - 工作流的基本逻辑类: `DAWorkFlow`,`DAAbstractNode`,`DAAbstractNodeFactory` - 工作流的显示类: `DAAbstractNodeGraphicsItem`,`DAAbstractNodeLinkGraphicsItem`,`DAAbstractNodeWidget`,`DANodeGraphicsScene`,`DANodeGraphicsView` 工作流基于插件模式编写,后续用户可基于`DANode`模块实现自己的逻辑,具体可参见`src\Plugin\DAUtilNodePlugin` ## 工作流的思路 工作流由节点组成,节点间传递数据,每个节点可以运行用户定义的逻辑,一个工作流就是多个节点以及节点间的关系描述,节点有输入和输出,节点的输出可流入到下一个节点中,作为下个节点的输入。 每个节点的数据流出时,会收集输入数据,进行逻辑运算,再输出数据。 因此工作流的关键就是每个节点,每个节点可以理解为编程过程中的函数,节点的输入就是函数的传参,节点的输出就类似于函数的返回。 ## 节点 节点作为工作流的关键,`DANode`模块的关键类就是`DAAbstractNode`,工作流都面向这个基类,具体功能的节点由用户自己继承`DAAbstractNode`来实现,例如`src\Plugin\DAUtilNodePlugin\DAVariantValueNode.h` `DAAbstractNode`描述了节点的抽象信息,包括输入,输出,节点的连接,如果以一个函数为例,要通过一个节点描述一个类似函数的功能,这个节点应该具备如下要素: - 输入 - 输出 - 逻辑 因此DataWorkFlow的节点定义如下: ![img](doc/PIC/节点说明示意图.png) 这个节点表示了一个加法函数 ```py def add(a,b): return a+b ``` 它有两个输入端a和b,他有一个输出端,由于函数的`return`是不需要具体描述的,但节点的输出要为其定义名称,因为后面节点的传递过程需要用到上个节点的输出,为了能知道哪个输出,所以输出也要定义名称。 因此节点的抽象类定义了输入参数名和输出参数名,对应两个接口函数:`getInputKeys`和`getOutputKeys` ```cpp //获取所有输入的参数名 virtual QList getInputKeys() const = 0; //获取所有输出的参数名 virtual QList getOutputKeys() const = 0; ``` 要实现一个节点,必须指定这个节点输入什么,输出什么,当然,可以没有输入,也可以没有输出。 ## 节点的生成 上面说了节点的抽象,实际一个工作流是由多个节点组成,而DataWorkFlow是由插件组成,如何知道程序有哪些节点,这里参考程序开发的`import` 节点的生成由节点工厂实现,节点工厂可以理解为一个程序包,程序包里有多个函数,节点工厂正是这样一个程序包。 `DAAbstractNodeFactory`类就是节点的抽象工厂,此类关键的函数是`create`接口,工厂将通过`DANodeMetaData`来生成一个节点 ```cpp //工厂函数,创建一个DAAbstractNode,工厂不持有DAAbstractNode的管理权 virtual DAAbstractNode *create(const DANodeMetaData& meta) = 0; ``` `DANodeMetaData`是节点的基本信息,其关键函数是: ```cpp QString getNodePrototype() const; ``` `DANodeMetaData`通过NodePrototype来决定`new`哪个节点。 一个程序可以`import`多个包,因此工作流也应该有多个节点工厂,汇总到一个"集团"中,"集团"再生成节点 这个"集团"就是`DAWorkFlow`类,这个类汇总了所有工厂,其关键函数如下: ```cpp //注册工厂 void registFactory(DAAbstractNodeFactory *factory); //创建节点,会触发信号nodeCreated,DAWorkFlow保留节点的内存管理权 DAAbstractNodeSmtPtr createNode(const DANodeMetaData& md); ``` "集团"汇总了所有工厂,节点通过"集团"创建,程序启动过程,先加载插件,插件生成工厂,工厂在集团中注册,最后再通过集团生成节点,具体流程如下: ![img](doc/PIC/节点工厂注册到工作流.png) 在程序启动完成后,所有节点工厂到注册到`DAWorkFlow`中,通过`DAWorkFlow`即可生成节点。 ## 节点的渲染 节点的渲染基于Qt的`Graphics View Framework`,通过Graphics View来实现渲染,整个工作流作为一个画布(QGraphicsScene),为此`DANode`模块提供了`DANodeGraphicsScene`,`DANodeGraphicsView`和`DAAbstractNodeGraphicsItem`来实现 `DAAbstractNodeGraphicsItem`对应一个`DAAbstractNode`,实际是通过`DAAbstractNode`的`createGraphicsItem`接口来生成`DAAbstractNodeGraphicsItem` 因此,要实例化一个节点还需要实现接口函数`DAAbstractNode::createGraphicsItem` ```cpp //节点对应的item显示接口,所有node都需要提供一个供前端的显示接口 virtual DAAbstractNodeGraphicsItem *createGraphicsItem() = 0; ``` 这个接口返回`DAAbstractNode`对应的GraphicsItem,`DANode`模块把节点的GraphicsItem抽象为`DAAbstractNodeGraphicsItem`,`DAAbstractNodeGraphicsItem`实现了节点的基本渲染,结合`DANodeGraphicsScene`和`DAAbstractNodeLinkGraphicsItem`实现节点输入输出的连接工作。 对于用户来说,用户操作节点实现了节点之间的关系连接,这个要反应到逻辑层,逻辑层既为`DAAbstractNode`,因此,前端看到的连线动作,逻辑端实际是调用`DAAbstractNode`的`linkTo`函数, ```cpp //建立连接,如果基础的对象需要校验,可继承此函数 virtual bool linkTo(const QString& outpt, Pointer toItem, const QString& inpt); ``` 在前端是通过`DAAbstractNodeLinkGraphicsItem`实现两个`DAAbstractNodeGraphicsItem`的连接,用户基本不需要关注`DAAbstractNodeLinkGraphicsItem`,用户只需要关心节点的连接点的生成。 用户实现自己的节点需要继承`paint`和`boundingRect`函数 ```cpp //绘图 void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); //绘图相关 QRectF boundingRect() const; ``` `DAAbstractNodeGraphicsItem`已经默认实现了输入输出点的绘制,如果需要自定义,可以重写`paintLinkPoint`函数 ``` //绘制某个连接点 virtual void paintLinkPoint(const DANodeLinkPoint& pl, QPainter *painter); ``` `DANode`模块提供了标准的节点渲染类`DAStandardNodeGraphicsItem`,此类实现了节点的名字显示和输入输出的显示。 ### 响应Node事件 节点存在一些动作,如被传入参数等操作时,应该反映到GraphicsItem执行某些绘图操作,这时可以通过重写`DAAbstractNodeGraphicsItem::nodeAction`函数来实现 ```cpp virtual void nodeAction(int action, const QVariant& v); ``` action动作参见`DAAbstractNode::NodeAction`枚举 ### 标准节点渲染:DAStandardNodeGraphicsItem 标准节点渲染渲染了`DAAbstractNode`的输入端,输出端,和节点名,其效果如下: ![img](doc/PIC/节点连接示意.gif) 上面演示了两种节点,一个节点只有输出,一个节点有3个输入,2个输出。 标准渲染节点具备如下特性 - 标准节点由矩形绘制 - 标准节点内部显示节点的名字 - 输入端位于左侧,垂直均匀分布,以空心方块代表 - 输出端位于右侧,垂直均匀分布,以实心方块代表 - 点击输出端进入连线模式,鼠标点击对应的输入端实现节点与节点之间的关系建立 - 连线过程点击鼠标右键取消连接状态 - 选择连接线能看到连接点的名字 ## 节点的操作 用户对节点的操作,例如赋予初始值等等需要用过界面实现,这时需要提供一个窗口给用户,`DANode`模块`DAAbstractNodeWidget`