QPCPlatform tutorial

1.概述

之前一直想做一个类似orange的一个平台——在该平台上一个处理流程就是一个可拖动的插件(在arange中称为add-on),将其拖到界面内然后把其输出参数连接到另一个处理流程插件的输入参数上,数据就能自动由一个插件流到另一个插件。这种方式非常利于可视化算法处理过程,而且可以实现一次封装,之后插件的编写就不需要顾及界面,只需要像编写一个cmd窗口程序一样,设定输入参数、设定输出参数,实现处理过程,主程序自己给插件套个壳,管理数据的流动与插件执行与否等过程。该平台的原理如下:

  1. 设计插件接口IPlugin,主程序主要与该接口交互,之后编写的插件都必须实现该接口,并实现反射机制(以便通过插件名找到插件的工厂函数,从而可以获得IPlugin类的一个实例),这里的反射机制比较简单,就是保存了插件名与其构造函数的地址而已,参考我之前的博客c++中的反射机制与插件式编程;关于在主程序中使用插件,类似于我的这篇博客Qt中开发一个插件式程序的简单探索
  2. 实现数据的流动需要抹除数据的类型。在orange中可能因为python本身就是弱类型的,数据流动相对比较容易(这一点不得而知)。但是这里使用的是c++,是强类型的语言,不能在写主界面的时候分情况——假如流动的数据是int型怎么样,假如是string又怎么样——在这里的处理方式是定义一个Any类型用于代理数据流,因为数据类型无非是数据存储地址与解译方式的组合,插件处理使用的输入输出数据一律为在堆上内存存储的数据,然后使用Any类型记录其地址与类型,数据流流动的时候实际是Any类型数据在流动。Any类型的实现方式如c++实现Any类
  3. 插件的界面代理。这里定义一个Shell类代理IPlugin的界面,主程序生成一个插件的实例时,生成一个Shell类,该类负责与IPlugin交互,管理其输入输出参数与外界的通信(比如输入数据流流入时是否让IPlugin运行起来,必须得所有输入参数都以就绪才可让插件的功能执行;以及输出参数更新时,通知其他人作出响应)
  4. 定义Flow类管理数据的流动。其原理为每个Flow持有一个Any类型数据,并连接前后两个Shell,当前面的插件通知它数据发生了变化,它就产生界面上流动的动画特效,并通知后一个Shell数据变化了;后一个Shell依据情况选择是否响应该通知,当该Shell的所有输入均就绪时只要有一个提醒它数据变化了,它就从各个流入的Flow取用数据,通知其管理的IPlugin执行过程;Flow的数据被取用了之后自动取消流动特效
  5. 各个插件执行的过程都是在一个独立的线程里,所以可以同时多分子执行。
  6. 主程序通过配置信息确定其界面效果,所以在程序生成后无需更改程序,只需更改其配置文件即可更改界面效果(目前可设置的效果没有,但是这个方式便于以后的拓展);插件的界面效果与文档也通过配置信息配置(当然生成的插件本身也带有简单的文档与参数说明),可以独立于插件编写;配置信息一律使用xml文档保存,易读易写。

2.主界面介绍

在这里插入图片描述
主界面很简单:

  1. 左边是工具箱,放置编写的插件,点击即可取用(鼠标变为十字架)放置于画板上
  2. 下边是输出参数窗口,查看所有插件的输出参数名称、从属于哪个类型插件、插件名称、从属于该插件的第几个输出参数以及参数类型、数据地址和实际数据的字符串表示(该功能有另外写的TypeDataPeeker实现,目前只能把基本类型intfloatdoublelongstd::string等转为字符串,未来可增加,只需重新编译该dll不需要改变主程序)
  3. 中间是画板,点击插件可以将插件放置在画板上,然后点击插件的输出参数按钮,移动鼠标连接线跟随,再点击另一个插件的输入参数按钮,即可连接(如果二者类型一样的话),其效果如下:
连接中 连接完
在这里插入图片描述 在这里插入图片描述
  1. 菜单功能目前功能有限,View菜单下可选择工具箱与输出参数查看面板是否显示;Plugin菜单下可新建插件(已实现),加载插件(已实现),包装插件(指更改插件的显示样式以及为插件编写文档等,未实现),管理插件(移除插件等,未实现);Help菜单下可查看文档、教程、关于等
view plugin help
在这里插入图片描述 在这里插入图片描述 在这里插入图片描述
  1. 之后计划在右边再添加一个文档窗口,鼠标在插件上F1即可调出相应插件的文档(根据配置信息按照插件名称查找其文档存放位置然后加载),良好的帮助文档是使用一个插件的重要帮助。

3.插件接口

插件接口有六个必须实现的纯虚函数与六个可选的虚函数可实现,六个必须实现的纯虚函数为:

  1. int getInputCount()=0——获取输入参数个数,该函数指定该插件有几个输入参数
  2. std::vector<std::string> getInputTypes()=0——该函数指定插件各是什么类型(必须与typeid(数据类型).name()打印出来的字符串一模一样)
  3. int getOutputCount()=0——该函数指定插件的输出参数个数
  4. std::vector<std::string> getOutputTypes()=0——该函数指定插件的输出参数类型。以上四个函数都非常容易写,不需要写一行代码可以直接生成,如下插件编写示例。
  5. void run()=0——该函数是插件的功能主体,插件实现的功能在这里实现,也是插件最终的部分
  6. void cleanGarbage(std::vector<Any> &outputVars,const std::vector<bool> targets)=0——该函数用于清理插件在堆上生成的数据,因为每一个插件具体生成什么类型的数据不可能提前知道,所以必须把具体数据类型相关的清理工作给插件执行。这一部分也非常好写,因为实际上清理工作都是直接调用Any::clean函数来清理,如any.clean<double>()表示清理保存double类型数的Any对象,参考插件编写示例;outputVars为要清理的数据,targets为一个与清理对象数组大小一致的布尔数组,指定清理对象数组中哪些元素需要清理,哪些不需要。

六个可选的虚函数为:
7. QWidget *getWidget()——该函数返回该插件持有的二级界面(或者是弹出式的或者是内嵌式的),这个函数一般返回NULL表示没有界面,加入有也一般是返回一个叫做pluginWidget的界面。这个函数一般自动生成,不需要写,参考示例。
8. std::string saveState()——该函数与下面的函数一起实现插件内部参数的保存与恢复功能,这属于额外功能,可以不实现,默认返回""
9. bool restoreState(std::string stateFile)——如上,该函数默认返回false,表示恢复失败;目前主程序没有实现保存与恢复工作流程,所以这两个函数实现了也没有意义;之后实现了补一个插件编写示例。
10. std::string getDoc()——返回插件的文档(解释该插件做什么的一个)
11. std::vector<std::string> getInputInfo()——获取输入参数信息
12. std::vector<std::string> getOutputInfo()——获取输出参数信息,以上信息都可以在插件向导中填写生成,无需写代码,如下插件编写示例。

除了以上函数,插件的初始化函数中还需要执行一些步骤(如果有二级界面的话需要初始化截面QWidget以及初始化界面参数以及连接界面的信号,设置embeded参数(决定界面是否为弹出式还是内嵌式);如果没有的话这些都不需要)

4.插件编写

插件的编写只需要实现以上IPlugin接口即可,有以下几步需要完成:

  1. 在Qt Creator中新建c++库项目,配置.pro文件,加入反射机制外部库(Reflex.dll)与接口外部库(IPlugin.dll)并添加其包含目录,添加方式不赘述
  2. 包含Any.hiplugin.hreflex.h头文件(上面包含目录设置无误的话才能包含进去,否则找不到包含文件,当然直接把这些包含文件放在该项目目录下再包含也是可以的)
  3. 让自定义的类继承自IPlugin接口,右键IPlugin,选择插入虚函数,选择需要实现的虚函数(六个纯虚函数必须实现,其他的可选)
    在这里插入图片描述
  4. 然后逐个实现各个函数即可。默认是没有二级界面的,如果要有二级界面还需要自己定义一个QWidget,然后在该插件中持有一个该控件的指针,在初始化函数中初始化该界面及其参数以及连接界面参数设置变化信号与插件相应的响应函数/槽函数(因为需要二级界面肯定是要用它设置一些插件的内部参数,所以肯定要监听其参数变化以反映给插件),然后还要在getWidget函数中返回该控件指针;如果该二级界面是内嵌式的还需要在构造函数中设置embeded参数为true

以上步骤看起来十分繁琐,所以没有展开以实例讲,因为实际创建插件时可以使用主程序菜单的Plugin-Create New选项来使用插件向导来生成项目,以下为示例:

1.无界面插件

=》示例:比如创建一个随机数生成器插件,该插件为起点插件,没有输入参数,输出为一个int,一个double,一个string,因为没有二级界面,所以不能设置内部参数,第一个输出参数为0~99之间随机整数,第二个输出参数为一个均值为0方差为1的标准正态分布双精度浮点数,第三个为生成的一个长度随机(30以内)的随机字符串。
qt生成随机数的方法,qt中主要使用qrand()生成一个随机数(0~RAND_MAX-1),使用qsrand(uint seed)来设置种子,但是怎么生成特殊分布的随机数?

  • 生成指定范围的随机整数这里用qrand()%range,其中range为范围
  • 生成0~1之间均匀分布小数,这里使用(double)(qrand()+1)/RAND_MAX,其区间为(0,1]
  • 生成高斯分布这里使用Box-Muller方法,参考基于Box–Muller变换的正态随机数生成方法
  1. 点击Plugin-CreateNew菜单,设置项目名称与路径
    在这里插入图片描述
    在这里插入图片描述
  2. 输入插件说明(可以不写不是必须),输入参数设为0,输出参数设为3,选择或者输入各个输出参数类型intdoublestring,分别在文本框里给出输出参数的一句话描述作为参数说明(参数说明也可以不写),既然这个插件没有界面,则去掉has widget的勾(这个参数取消选中后embeded widget参数无法选,因为没有二级界面也就无所谓弹出式还是嵌入式),包含目录与依赖项目录不用改。
    在这里插入图片描述
  3. 确认信息,点击Finish即完成创建插件向导
    在这里插入图片描述
  4. 之后会自动打开生成的目录,可以看到目录下有几个新生成的文件,双击.pro文件使用QT Creator打开项目编辑。
    在这里插入图片描述
  5. 打开randomgenerator.cpp文件,可看到生成的模板文件有很多现成的代码和完备的注释,每一个函数干什么的,一般需要执行哪些步骤在注释中都一清二楚
    在这里插入图片描述
  6. 实际上输入输出参数个数、说明文档函数、参数说明函数都实现了,只需要在构造函数中执行一些初始化内部参数(如果有)的操作与run函数中的操作以及cleanGarbage函数执行的清理操作;因为这里没有什么参数需要初始化,而且没有二级界面的交互,所以构造函数里不需更改。
  7. run函数与cleanGarbage函数里中添加如下代码(只需要改这两个函数就创建成了一个简易的插件):
    在这里插入图片描述
  8. 插件可以放在程序根目录/plugins下,则在程序启动时会自动加载(当然插件只能是release版,除非你有debug版的主程序),也可以在主界面点击Plugin-Load Plugin菜单项,手动浏览到插件文件夹加载插件,效果如下(因为插件没有输入参数,一拖放到界面上就会自己运行,所以可以在输出参数窗口查看到三个新生成的随机变量):
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

2.弹出式界面插件

=》示例:创建一个插件计算
y = a x b + c y=a\cdot x^b+c
的值,其中x为输入参数,是一个double变量,y为输出变量也为double型,abc为内部参数,需要在二级界面上设置

  1. 首先像无界面插件一样使用向导创建插件,命名为AXToBthPlusC,选择一个输入与一个输出,设置类型均为double,插件功能说明,参数描述可设可不设(设置了的话在鼠标移到输入输出参数按钮位置时会显示提示文本,文本就是这里给定的参数说明),选中has widget,如下:
step1 step2 step3
在这里插入图片描述 在这里插入图片描述 在这里插入图片描述
  1. 打开项目作出相应更改。有界面的插件要麻烦一点,因为要管理界面的初始化与信号通信,在QT Creator中打开axtobthplusc.ui文件如下设计界面,上面的标签用于显示当前的函数式,三个编辑框输入三个参数abc,OK按钮确定参数更改,Reset按钮恢复初始参数;
    在这里插入图片描述
  2. 在界面头文件中添加initParameters函数与internalArgsChanged信号,在界面源文件中作如下更改——initParameters函数初始化参数,并将其设置到文本框中与顶部显示标签;OK按钮更改顶部标签中a,b,c参数为文本框中数字,同时发出内部参数变更信号;Reset函数重置文本框数字为初始参数;另外在构造函数中设置文本框的输入验证器,该验证器为一个正则表达式验证器,该表达式意思是开头允许一个或零个负号,之后至少有一个以上数字,中间可以有零个或者一个小数点
    在这里插入图片描述
  3. 在插件的构造函数中与run函数中分别添加如下代码,并且在cleanGarbage函数中执行清理(与无界面的差不多);构造函数中连接了界面的参数更改信号,以修改插件的参数,这就是界面上的交互反映到插件上参数变化的方式,基本上带界面的都是以这样的方式,当然也可以直接通过puginWidget访问其参数,但是相应的参数要设成公共的,而且还是得监听信号,不然无法知道什么时候读取这些参数。
构造函数 run
在这里插入图片描述 在这里插入图片描述
  1. 插件效果如下,之前的RandomGeneratorAxToBthPlugC插件:
    在这里插入图片描述

3.内嵌式界面插件

内嵌式界面插件与弹出式界面插件创建方式几乎一样,只不过需要设一个embeded参数,在创建向导里选中即可,比如创建一个OpenCV的Mat图片显示器MatDisplayer,主界面只有一个label用于显示图片,界面类有一个共有函数setImage可以把设置的Mat图片转为QImage然后显示到label上,该插件没有内部参数,只有一个输入参数,为cv::Mat类型,没有输出参数。结合另外的插件MatLoader可以加载Mat图片然后显示,其效果如下:
在这里插入图片描述

4.循环控制插件

循环控制插件为以上嵌入式界面的插件,其功能为控制其他插件循环工作,有两个输入,两个输出,一个输入为初始输入,一个为循环输入;两个输出分别为最终输出与循环输出。数据流从初始输入1端口流入由循环输出3端口流入要循环的控件,控件执行完毕输出流到循环输入4端口,只要未达到循环次数限制,循环输入4流入的数据都流向循环输出3,引起下一轮循环,否则流向最终输出2端口,终止过程。如下图:
在这里插入图片描述
可以看到,循环插件不过是控制数在内部的流动方向与流动次数,不对输入数据做任何处理,实现这样的循环有一个要求就是彻底抹去类型,数据流流动已经是使用Any类型来代理了,已经算是抹去了类型,但是在使用中连接两个插件时还是会检查其插件输入输出类型是否匹配,该程序允许不做这样的检查,任何两个插件的输出与输入都可以连接,要做到这样,只需要在定义插件的向导中选择插件输入输出参数类型时给定插件参数类型为“null”,这样就不会检查类型匹配了。如下为LoopControl插件定义时向导中的设置。

在这里插入图片描述 在这里插入图片描述
1 2

定义一般的插件时不建议使用“null”类型,因为使用“null”类型的输入参数默认接收任何类型,这样的插件一般只是把输入重定向输出,不做任何处理;或者对于输入数据分情况,对于少量的几种已知情况进行处理,未知情况时不做处理,或者抛出一个std::exception异常,该平台会捕捉异常并提示。这里给出一个循环插件的使用示例:
比如查看
x n + 1 = 3 6 3 + x n 2 x_{n+1}=3-\frac{6}{3+x^2_n}
不同循环次数下的收敛效果,上式可化为:
x n + 1 = 6 u 1 + 3 ,     u = x n 2 + 3 x_{n+1}=-6\cdot u^{-1}+3,\ \ \ u=x^2_n+3
所以用两次第二节制作的插件AXToBthPlugC(该插件功能为计算 y = a x b + c y=a\cdot x^b+c 的值)即可,然后使用循环控制插件令其循环即可,如下图:
在这里插入图片描述

循环5次

在这里插入图片描述

循环10次

在这里插入图片描述

循环20次
发布了28 篇原创文章 · 获赞 14 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/liyunxin_c_language/article/details/100894119