写界面软件,经常遇到这么一类场景:
主界面点击应用窗口进入某模块显示界面,某模块显示界面再通过按钮进入菜单界面,菜单界面有很多关于该模块显示界面的设置项,比如量程,增益,时间显示,亮度,对比度等等,大概十几个设置。
有些数值类的设置还有子预览菜单,在子预览菜单里面通过滑条去设置数值,回到菜单后,设置会显示子预览菜单设置的数值。
模块显示界面需要显示一些菜单的设置,比如量程,增益等等。
也就是大概这么一个页面关系,几个页面存在线性的导航关系,同时数据来源大量重复。
做这种页面逻辑的一个难点就是,设置某个菜单项,对应的修改数据怎么同步到上一层/几层窗口上去。
解决这个问题的经历经过了两次设计思路的迭代,也借此终于了解了Signal/Slot的原理和用法,算有收获,故此做记录。
旧思路
出于使用的框架特性,个人开发经验等原因,我当时解决此类问题的设计图示如下:
假设存在页面A->B->C的线性导航关系,每个页面根据MVC的设计思路,都做页面逻辑,业务逻辑,持久化(配置文件/数据库)三层逻辑的分层设计。
以一系列操作讲解大致的数据流:
1.比如从A进入页面B(图中步骤1),页面B初始化,从数据库加载设置数据(图中步骤2)
2.在页面B点击设置一些数据(设置页面的各种单选,数值滑条等等),设置的数据经由页面对应的业务逻辑方法,保存到数据库(图中步骤3)
3.返回页面A(图中步骤4),通过一些方法触发页面A的业务逻辑,从数据库再次载入从页面B修改好的数据,完成页面A更新(步骤5)。
这是简化过的设计框架,实际开发还有一些例外情况,比如几个页面有显示同一个数据的通用业务逻辑,不一定所有页面都要读写数据库之类,但是大体不离上图的思路。
如果有看过我的文章,其实这就是我在
这个开发思路面对这种场景够用了,但是缺点也很明显:
1.只能用于线性导航关系,对于分屏等同一个页面上并列的关系,需要另外设计数据结构完成多个屏幕业务逻辑的同步。
2.一个页面只能更新上一个页面,没法连带更新上N个页面,理论上,有N层导航关系,1个数据被N个页面用到,就要访问N次数据库,很不优雅,对IO效率敏感的机器也吃不消。
3.为了实现图中的步骤5,有几种实现方法:
1.在退出页面的exit回调(上图页面B)调用上一个页面(页面A)的函数来完成业务逻辑,最简单直接,但是这样页面和页面间逻辑就耦合了,如果还有其他页面也要调用页面A,且处理方法跟页面A不同,那就不行了。
ret_t on_pageB_exit(win, ctx){
db_data *data = get_db_data();
on_pageA_load(data);
return RET_OK;
}c
2.更一步,可以搞成页面A传return callback上下文给页面B在退出时调用的方式,来避免在页面B暴露具体的页面方法,但是仍旧无法解决缺点2。
ret_t on_pageA_load(){
db_data *data = get_db_data();
// set pageA data
}
ret_t on_pageA_navigate(){
ctx->exit_call = on_pageA_load;
navigator_to("pageB");
}
...
ret_t on_pageB_exit(win, ctx){
ctx = widget_get_prop_pointer(win, "ctx");
ctx->exit_call();
}
ret_t on_pageB_init(win, ctx){
widget_set_prop_pointer(win, "ctx", ctx);
}
其实上述页面页面连带更新的问题正好在Signal/Slot的射程范围内,然而当时,限于经历我并未意识到Signal/Slot的作用,只认为是类似按钮点击事件回调同一类的东西。
新思路:Signal+Slot
机缘巧合,接到一个要搬运其他项目模块到自己项目的任务,要搬运的代码逻辑实现大量使用QT,和自己项目的AWTK不兼容,给实现造成了不小的麻烦。
Signal/Slot是QT框架独特, 核心的特性之一,搬运的代码也大量用到了,遗憾的是,使用的AWTK框架并没有类似的特性,本身类似的emitter模块并不算很好用,也承接不了如lamada这样C++才能用的特性,我最后用了boost的signal2库去弥补这一不足。
不过多少还有点收获,干了这么久非QT的GUI框架,总算能接触到全球最经典最常用的GUI框架,去了解下用它开发的思维模式了。
代码
如果不想看代码可直接跳到标题:原理
还是以上面的页面A,B,C为例,新建一个全局性的SignalManager单例,在里面添加一个名为sigDataChange的signal函数,并在page_A,page_B,page_C页面文件里面都注册这个函数:
SignalManage.hpp
pageA,B,C的逻辑如下, 这些文件实际上是页面c文件的c++扩展,AWTK由于定位偏向纯C开发,页面文件为了兼容需要自己写不少CPP逻辑扩展,比较繁琐。
page_a_cpp.cpp
page_b_cpp.cpp
page_c_cpp.cpp
编译启动,首先打开页面A,然后点击按钮进入页面B,再点击按钮进入页面C,页面会有一个滑条,滑动滑条,滑条的更新数据将通过signal回调去更新页面A和页面B的数据显示。
代码案例已在https://gitee.com/tracker647/awtk-practice/tree/master/signal_slot_test给出,可以验证效果。
原理
为什么更新了页面C之后,页面B和页面A都能同时更新?结合AI看了下Boost源码,大致明白,signal/slot是观察者模式的实现方式,其数据结构本质类似于一个桶链表,根据回调signal的不同来区分不同的“桶”,每个“桶”存储注册的一系列函数(观察者),当“桶”被触发时,桶上所有的注册函数即被触发。
换到这个例子里,就是SignalManger的sigDataChange函数被两个页面page_a,page_b所观察,在page_c页面界面调节滑条时,数据变化将会通过sigDataChange传达给page_a, page_b注册了sigDataChange的函数。
这种实现有效解决了旧思路里2.不好同时更新多个页面的问题,这样在页面C可以只访问一次数据库,新数据的UI更新交给signal函数就行,而且调用方不需要关心signal函数背后的实际实现。
基于MVC的旧思路和signal/slot并不是相互替代的关系,而是补充加强,图示如下,可以看到MVC分层设计和signal/slot存在很有意思的关联:
如果说MVC的分层设计思路是“合纵”,将前端UI,后端业务,数据库逻辑统合在一起,那signal/slot的引入就是“连横”,通过signal/slot的灵活的数据传输机制实现各模块的”协同合作"!
结语
说起来有点惭愧,GUI开发干了这么久才开始了解和使用signal/slot,感觉自己的开发视野还是很闭塞,这多少有目前做的应用场景简单,尚且还未遇到很大的架构问题的原因,如果没有这次搬运QT代码的经历,我估计还是在使用很低效的开发方式。
AWTK虽然支持C++,但几乎所有API都是纯C实现的,当时不熟悉C++,也没有写独立原生逻辑的想法,很多页面代码都是依赖于框架自身的功能去完成,现在看来纯C写界面实在太不方便了,没有面向对象,STL,泛型,各种算法API,lamada,开发思路很受限制,做一些复杂的数据处理十分繁琐。虽说理论上也可以写一些轮子,struct+函数指针实现类面向对象的效果,但显然又多不少兼容工作,麻烦。
好在嵌入式Linux板本身是支持运行C++程序的,与其弄那么多弯弯绕绕还不如直接上C++,有了这份经历,以后警惕纯C,平台在单片机上写GUI的岗位。
讲了这么多,其实就是想说清楚自己目前对于signal/slot的了解,signal/slot的作用显然不止于此,还有更多的作用等着我开发挖掘,就看后面的项目还会遇到什么样的挑战。
2862

被折叠的 条评论
为什么被折叠?



