写界面软件,经常遇到这么一类场景:
主界面点击应用窗口进入某模块显示界面,某模块显示界面再通过按钮进入菜单界面,菜单界面有很多关于该模块显示界面的设置项,比如量程,增益,时间显示,亮度,对比度等等,大概十几个设置。
有些数值类的设置还有子预览菜单,在子预览菜单里面通过滑条去设置数值,回到菜单后,设置会显示子预览菜单设置的数值。
模块显示界面需要显示一些菜单的设置,比如量程,增益等等。
也就是大概这么一个页面关系,几个页面存在线性的导航关系,同时数据来源大量重复。
做这种页面逻辑的一个难点就是,设置某个菜单项,对应的修改数据怎么同步到上一层/几层窗口上去。
解决这个问题的经历经过了两次设计思路的迭代,也借此终于了解了Signal/Slot的原理和用法,算有收获,故此做记录。
旧思路
出于使用的框架特性,个人开发经验等原因,我当时解决此类问题的设计图示如下:
假设存在页面A->B->C的线性导航关系,每个页面根据MVC的设计思路,都做页面逻辑,业务逻辑,持久化(配置文件/数据库)三层逻辑的分层设计。
以一系列操作讲解大致的数据流:
1.比如从A进入页面B(图中步骤1),页面B初始化,从数据库加载设置数据(图中步骤2)
2.在页面B点击设置一些数据(设置页面的各种单选,数值滑条等等),设置的数据经由页面对应的业务逻辑方法,保存到数据库(图中步骤3)
3.返回页面A(图中步骤4),通过一些方法触发页面A的业务逻辑,从数据库再次载入从页面B修改好的数据,完成页面A更新(步骤5)。
这是简化过的设计框架,实际开发还有一些例外情况,比如几个页面有显示同一个数据的通用业务逻辑,不一定所有页面都要读写数据库之类,但是大体不离上图的思路。
如果有看过我的文章,其实这就是我在AWTK-MVVM的一些使用技巧总结: 跨页面同步数据的model,写model getter和setter将UI代码和model代码联系到一起的实践,只不过页面逻辑与业务逻辑的交互变成了MVVM框架的数据绑定和命令绑定(其实我那样写还更有问题,model和几个页面的业务逻辑耦合了),但是由于偏题,框架的东西就不说了。
这个开发思路面对这种场景够用了,但是缺点也很明显:
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
- #ifndef SIGNAL_MANAGER_HPP
- #define SIGNAL_MANAGER_HPP
- #include <boost/signals2.hpp>
- // 类型别名定义
- template<typename T>
- using signal = boost::signals2::signal<T>;
- using connection = boost::signals2::connection;
- class SignalManager {
- public:
- static SignalManager &instance(){
- static SignalManager instance;
- return instance;
- }
-
- signal<void(int)> sigDataChange;
- private:
- SignalManager();
- ~SignalManager();
- SignalManager(const SignalManager &) = delete;
- SignalManager &operator=(const SignalManager &) = delete;
- SignalManager(SignalManager &&) = delete;
- SignalManager &operator=(SignalManager &&) = delete;
- };
- #endif // SIGNAL_MANAGER_HPP
复制代码 pageA,B,C的逻辑如下, 这些文件实际上是页面c文件的c++扩展,AWTK由于定位偏向纯C开发,页面文件为了兼容需要自己写不少CPP逻辑扩展,比较繁琐。
page_a_cpp.cpp
- #include "awtk.h"
- #include "../common/navigator.h"
- #include "page_a_cpp.h"
- #include "../common/SignalManager.hpp"
- #include <boost/current_function.hpp>
- #include <stdio.h>
- class PageA {
- public:
- PageA() : m_win(NULL) {}
- ~PageA() {}
- void PageInit(widget_t *win){ m_win = win; }
- void PageDeinit(){}
- void on_data_change(int data) {
- printf("%s\r\n", BOOST_CURRENT_FUNCTION);
- char text[128];
- snprintf(text, sizeof(text), "data: %d", data);
- widget_t* label = widget_lookup(m_win, "lbl_data", TRUE);
- if (label != NULL) {
- widget_set_text_utf8(label, text);
- }
- }
- private:
- widget_t *m_win;
- };
- PageA s_pageA;
- static ret_t on_btn_page_b_click(void* ctx, event_t* e) {
- return navigator_to("page_b");
- }
- /**
- * 初始化窗口的子控件
- */
- static ret_t visit_init_child(void* ctx, const void* iter) {
- widget_t* win = WIDGET(ctx);
- widget_t* widget = WIDGET(iter);
- const char* name = widget->name;
- // 初始化指定名称的控件(设置属性或注册事件),请保证控件名称在窗口上唯一
- if (name != NULL && *name != '\0') {
- if(tk_str_eq(name, "btn_page_b")){
- widget_on(widget, EVT_CLICK, on_btn_page_b_click, win);
- }
- }
- return RET_OK;
- }
- /**
- * 初始化窗口
- */
- ret_t page_a_cpp_init(widget_t* win, void* ctx) {
- (void)ctx;
- return_value_if_fail(win != NULL, RET_BAD_PARAMS);
- widget_foreach(win, visit_init_child, win);
- s_pageA.PageInit(win);
- // 连接信号
- SignalManager::instance().sigDataChange.connect([](int data){
- s_pageA.on_data_change(data);
- });
- return RET_OK;
- }
复制代码 page_b_cpp.cpp
- #include "awtk.h"
- #include "../common/navigator.h"
- #include "page_b_cpp.h"
- #include "../common/SignalManager.hpp"
- #include <boost/current_function.hpp>
- #include <stdio.h>
- #include <vector>
- class PageB {
- public:
- PageB() : m_win(NULL) {}
- ~PageB() {}
- void PageInit(widget_t *win){
- m_win = win;
- m_connections.push_back(SignalManager::instance().sigDataChange.connect([this](int data){
- on_data_change(data);
- })
- );
- }
- void PageDeinit(){
- for(auto connection : m_connections){
- connection.disconnect();
- }
- m_connections.clear();
- }
- void on_data_change(int data) {
- printf("%s\r\n", BOOST_CURRENT_FUNCTION);
- char text[128];
- snprintf(text, sizeof(text), "data: %d", data);
- widget_t* label = widget_lookup(m_win, "lbl_data", TRUE);
- if (label != NULL) {
- widget_set_text_utf8(label, text);
- }
- }
- private:
- widget_t *m_win;
- std::vector<connection> m_connections;
- };
- PageB s_pageB;
- static ret_t on_btn_to_page_c_click(void* ctx, event_t* e) {
- return navigator_to("page_c");
- }
- static ret_t on_btn_return_click(void* ctx, event_t* e) {
- return window_close(WIDGET(ctx));
- }
- static ret_t on_window_close(void* ctx, event_t* e) {
- s_pageB.PageDeinit();
- return RET_OK;
- }
- /**
- * 初始化窗口的子控件
- */
- static ret_t visit_init_child(void* ctx, const void* iter) {
- widget_t* win = WIDGET(ctx);
- widget_t* widget = WIDGET(iter);
- const char* name = widget->name;
- // 初始化指定名称的控件(设置属性或注册事件),请保证控件名称在窗口上唯一
- if (name != NULL && *name != '\0') {
- if(tk_str_eq(name, "btn_to_page_c")){
- widget_on(widget, EVT_CLICK, on_btn_to_page_c_click, win);
- }
- else if(tk_str_eq(name, "btn_return")){
- widget_on(widget, EVT_CLICK, on_btn_return_click, win);
- }
- }
- return RET_OK;
- }
- /**
- * 初始化窗口
- */
- ret_t page_b_cpp_init(widget_t* win, void* ctx) {
- (void)ctx;
- return_value_if_fail(win != NULL, RET_BAD_PARAMS);
- widget_foreach(win, visit_init_child, win);
- widget_on(win, EVT_WINDOW_CLOSE, on_window_close, win);
- s_pageB.PageInit(win);
- return RET_OK;
- }
复制代码 page_c_cpp.cpp
- #include "awtk.h"
- #include "../common/navigator.h"
- #include "../common/SignalManager.hpp"
- #include <boost/current_function.hpp>
- #include <stdio.h>
- #include "conf_io/app_conf.h"
- #include "page_c_cpp.h"
- static ret_t on_btn_return_click(void* ctx, event_t* e) {
- return window_close(WIDGET(ctx));
- }
- static ret_t on_slider_value_changed(void* ctx, event_t* e) {
- value_change_event_t *evt = value_change_event_cast(e);
- app_conf_set_int("data", value_int(&evt->new_value));
- SignalManager::instance().sigDataChange(value_int(&evt->new_value));
- return RET_OK;
- }
- static ret_t on_slider_value_changing(void* ctx, event_t* e) {
- value_change_event_t *evt = value_change_event_cast(e);
- app_conf_set_int("data", value_int(&evt->new_value));
- SignalManager::instance().sigDataChange(value_int(&evt->new_value));
- return RET_OK;
- }
- /**
- * 初始化窗口的子控件
- */
- static ret_t visit_init_child(void* ctx, const void* iter) {
- widget_t* win = WIDGET(ctx);
- widget_t* widget = WIDGET(iter);
- const char* name = widget->name;
- // 初始化指定名称的控件(设置属性或注册事件),请保证控件名称在窗口上唯一
- if (name != NULL && *name != '\0') {
- if(tk_str_eq(name, "btn_return")){
- widget_on(widget, EVT_CLICK, on_btn_return_click, win);
- }
- else if(tk_str_eq(name, "slider")){
- widget_on(widget, EVT_VALUE_CHANGED, on_slider_value_changed, win);
- widget_on(widget, EVT_VALUE_CHANGING, on_slider_value_changing, win);
- }
- }
- return RET_OK;
- }
- /**
- * 初始化窗口
- */
- ret_t page_c_cpp_init(widget_t* win, void* ctx) {
- (void)ctx;
- return_value_if_fail(win != NULL, RET_BAD_PARAMS);
- widget_foreach(win, visit_init_child, win);
- widget_set_value_int(widget_lookup(win, "slider", TRUE), app_conf_get_int("data", 0));
- return RET_OK;
- }
复制代码 编译启动,首先打开页面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的作用显然不止于此,还有更多的作用等着我开发挖掘,就看后面的项目还会遇到什么样的挑战。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |