当前位置: 首页 > news >正文

AWTK-MVVM的一些使用技巧总结(1)

在项目中用了一段时间的AWTK-MVVM框架,由于AWTK-MVVM本身的文档十分欠缺,自己经过一段时间的研究折腾出了几个技巧,在此记录总结。

用fscript启用传统UI代码

AWTK-MVVM里面重新设计了navigator机制,重定位了navigator_to的调用方向,原版AWTK的navigator_to调用很简单,是直接向navigator中请求窗口并对窗口做初始化:

demo.exe!home_page_init(_widget_t * win, void * ctx) Line 25 
demo.exe!navigator_window_init(const char * name, _widget_t * win, void * ctx) Line 10 
demo.exe!navigator_window_open_and_close(const char * name, _widget_t * to_close, void * ctx) Line 32 
demo.exe!navigator_to_with_context(const char * target, void * ctx) Line 51 
demo.exe!navigator_to(const char * target) Line 45 
demo.exe!application_init() Line 56 
demo.exe!wWinMain(HINSTANCE__ * hinstance, HINSTANCE__ * hprevinstance, wchar_t * lpcmdline, int ncmdshow) Line 255 
demo.exe!invoke_main() Line 123 
demo.exe!__scrt_common_main_seh() Line 288 
demo.exe!__scrt_common_main() Line 331 
demo.exe!wWinMainCRTStartup(void * __formal) Line 17 
kernel32.dll!00007fff4c88259d() (Unknown Source:0)
ntdll.dll!00007fff4dc6af78() (Unknown Source:0)

但是MVVM里面就重定义成了对窗口绑定的v-model的初始化,原先的窗口init函数没法直接进入了:

awtk.dll!tk_calloc(unsigned int nmemb, unsigned int size, const char * func, unsigned int line) Line 152 
demo.exe!m_home_create(_navigator_request_t * req) Line 19 
demo.exe!m_home_view_model_create(_navigator_request_t * req) Line 155 
mvvm.dll!view_model_factory_create_model_one(const char * type, _navigator_request_t * req) Line 106 
mvvm.dll!view_model_factory_create_model(const char * type, _navigator_request_t * req) Line 115 
mvvm.dll!binding_context_awtk_create_one_view_model(_view_model_t * parent, const char * type, _navigator_request_t * req) Line 68 
mvvm.dll!binding_context_awtk_create_view_model(_view_model_t * parent, const char * type, _navigator_request_t * req) Line 100 
mvvm.dll!binding_context_awtk_create(_binding_context_t * parent, const char * vmodel, _navigator_request_t * req, _widget_t * widget) Line 1266 
mvvm.dll!ui_loader_mvvm_build_widget(_ui_loader_mvvm_t * loader, _rbuffer_t * rbuffer, _ui_builder_t * builder, unsigned int * cursor, const _value_t * id) Line 515 
mvvm.dll!ui_loader_mvvm_load_a_snippet(_ui_loader_mvvm_t * loader, _rbuffer_t * rbuffer, _ui_builder_t * builder, _widget_t * end_widget) Line 971 
mvvm.dll!ui_loader_mvvm_load_bin(_ui_loader_t * l, const unsigned char * data, unsigned int size, _ui_builder_t * b) Line 1030 
mvvm.dll!ui_loader_mvvm_load(_ui_loader_t * l, const unsigned char * data, unsigned int size, _ui_builder_t * b) Line 1074 
mvvm.dll!ui_loader_mvvm_load_widget_with_parent(_navigator_request_t * req, _widget_t * parent) Line 1124 
mvvm.dll!ui_loader_mvvm_load_widget(_navigator_request_t * req) Line 1091 
mvvm.dll!navigator_handler_awtk_window_open_and_close(_navigator_request_t * req, _widget_t * to_close) Line 217 
mvvm.dll!navigator_handler_awtk_window_open(_navigator_request_t * req) Line 253 
mvvm.dll!navigator_handler_awtk_on_request(_navigator_handler_t * handler, _navigator_request_t * req) Line 321 
mvvm.dll!navigator_handler_on_request(_navigator_handler_t * handler, _navigator_request_t * req) Line 51 
mvvm.dll!navigator_handle_request(_navigator_t * nav, _navigator_request_t * req) Line 90 
mvvm.dll!navigator_to(const char * args) Line 138 
demo.exe!application_init() Line 67 
demo.exe!wWinMain(HINSTANCE__ * hinstance, HINSTANCE__ * hprevinstance, wchar_t * lpcmdline, int ncmdshow) Line 255 
demo.exe!invoke_main() Line 123 
demo.exe!__scrt_common_main_seh() Line 288 
demo.exe!__scrt_common_main() Line 331 
demo.exe!wWinMainCRTStartup(void * __formal) Line 17 
kernel32.dll!00007fff4c88259d() (Unknown Source:0)
ntdll.dll!00007fff4dc6af78() (Unknown Source:0)

有些场景还是需要直接操作界面widget来实现一些效果的,而model文件显然不能也不应该(否则UI和model代码就耦合了)直接获取这些widget,怎么办呢?

AWTK的fscript脚本提供了一个解决方案,步骤如下:

1.在需要使用界面代码的窗口顶部windows标签上,比如home_page.xml,声明一个on:window_open="func_navigator_window_init()"

<window v-model="m_home" name="home_page" on:window_open="func_navigator_window_init()">...
</window>

2.在application.c上注册fscript函数。

ret_t application_init(void) {...fscript_register_func("func_navigator_window_init", func_navigator_window_init);...
}

3.修改common/navigator.c,把navigator_window_init函数从#ifndef WITH_MVVM的宏范围里面提取出来,这是AWTK原版的窗口路由函数,func_navigator_window_init可以直接通过调用这个函数来路由到对应的窗口,缺点就是每次添加新窗口的时候都要手动修改下navigator_window_init内部代码。

common/navigator.c

extern ret_t home_page_init(widget_t* win, void* ctx);ret_t navigator_window_init(const char* name, widget_t* win, void* ctx) {if (tk_str_eq(name, "home_page")) {return home_page_init(win, ctx);}return RET_OK;
}
ret_t func_navigator_window_init(fscript_t *fscript, fscript_args_t *args, value_t *result)
{widget_t *widget = WIDGET(tk_object_get_prop_pointer(fscript->obj, STR_PROP_SELF));widget_t *win = widget_get_window(widget);return navigator_window_init(win->name, win, NULL);
}#ifndef WITH_MVVM
...

这样程序启动的时候,窗口打开会触发fscript函数,在fscript里面能通过fscript自带的对象获取到绑定的widget指针,就能路由到原来的页面init文件了。

跨页面同步数据的model

有些页面, 比如声纳的画面->声纳菜单->某某设置条,几个页面数据来源大量重复, 而且这些页面之间还有线性导航关系,这个时候用AWTK-MVVM原来文档的数据同步的方法显得就有些麻烦了,进页面和出页面都要在给下一个窗口的传递对象中指明传输数据,两个页面就是两处修改,三个页面就是四处修改。

干脆让这些页面都采用同一个model好了,看起来更好处理,但是怎么让这几个页面的model在页面进出时都能共享上一个页面的数据呢?

这个问题之前已在https://blog.csdn.net/Tracker647/article/details/147169060讨论过了,不过最近发现全局model getter的写法有点问题,需要上一个页面手动向下一个页面提供自己当前的model,实际上完全可以改成用一个栈或者链表来维护同类的model。

static std::list<m_sonar_t*> g_model_list;
m_sonar_t* get_m_sonar_model(void) {if(g_model_list.empty()){return NULL;}//栈最顶层为当前的modelreturn g_model_list.back();
}m_sonar_t* m_sonar_create() {m_sonar_t* m_sonar = TKMEM_ZALLOC(m_sonar_t);return_value_if_fail(m_sonar != NULL, NULL);emitter_init(EMITTER(m_sonar));m_sonar_t* prev_model = get_m_sonar_model();//通过AWTK的app conf持久化机制中转数据,实现prev_model到目前model的数据同步m_sonar_config_save(prev_model);m_sonar_config_load(m_sonar);//当前model入栈g_model_list.push_back(m_sonar);return m_sonar;
}ret_t m_sonar_destroy(m_sonar_t* m_sonar) {return_value_if_fail(m_sonar != NULL, RET_BAD_PARAMS);m_sonar_config_save(m_sonar);//退出,当前model出栈g_model_list.remove(m_sonar);m_sonar_t* prev_model = get_m_sonar_model();m_sonar_config_load(prev_model);emitter_deinit(EMITTER(m_sonar));return RET_OK;
}ret_t m_sonar_config_load(m_sonar_t* m_sonar) {m_sonar_set_zoom_mode(m_sonar, app_conf_get_int(CONF_KEY_GAIN_MODE, DEFAULT_VALUE_GAIN_MODE));m_sonar_set_zoom_mode(m_sonar, app_conf_get_int(CONF_KEY_FREQENCY, DEFAULT_VALUE_FREQENCY));m_sonar_set_zoom_mode(m_sonar, app_conf_get_int(CONF_KEY_ZOOM_MODE, DEFAULT_VALUE_ZOOM_MOD));emitter_dispatch_simple_event(EMITTER(m_sonar), EVT_PROPS_CHANGED);return RET_OK;
}ret_t m_sonar_config_save(m_sonar_t* m_sonar) {if(m_sonar == NULL){return RET_FAIL;}app_conf_set_int(CONF_KEY_GAIN_MODE, m_sonar->gain_mode);app_conf_set_int(CONF_KEY_FREQENCY, m_sonar->freqenct);app_conf_set_int(CONF_KEY_ZOOM_MODE, m_sonar->zoom_mode);return RET_OK;
}

这个方法有局限性,只适用于需要同步的这些页面的v-model都是全局的唯一的的情况下, 如果一个页面多次声明同一个v-model就不行了,得另作修改。
而且上一章的model数据同步到这里就不管用了,必须修改,好在这些声纳model只是一层类型不同而已,让声纳model自身的model链表都只进出同类型的声纳model就行了。

写model getter和setter将UI代码和model代码联系到一起

有时候UI交互造成的一些数据变化需要同步到model里面去,而且这些数据变化没办法或者是不想直接在xml上做命令绑定。

比如需要考虑灵活维护,想直接从UI代码接口获取可变量做参数,不想在xml里用给定参数写死的命令绑定,通过用户UI代码事件回调而不是view_model来触发model更新;

<!-- Don't -->
<window v-model="calculator"><edit name="expr" x="c" y="10" w="90%" h="30" focus="true" readonly="true" input_type="custom" text="" tips="expression" v-data:text="{expr}"/><view y="60" x="c" w="90%" h="-60" is_keyboard="true" children_layout="default(r=4,c=4,m=5,s=5)" ><button name="0" text="0" v-on:click="{addChar, Args=0}"/><button name="1" text="1" v-on:click="{addChar, Args=1}"/><button name="2" text="2" v-on:click="{addChar, Args=2}"/><button name="3" text="3" v-on:click="{addChar, Args=3}"/><button name="4" text="4" v-on:click="{addChar, Args=4}"/><button name="5" text="5" v-on:click="{addChar, Args=5}"/><button name="6" text="6" v-on:click="{addChar, Args=6}"/><button name="7" text="7" v-on:click="{addChar, Args=7}"/><button name="8" text="8" v-on:click="{addChar, Args=8}"/><button name="9" text="9" v-on:click="{addChar, Args=9}"/><button name="+" text="+" v-on:click="{addChar, Args=+}"/><button name="-" text="-" v-on:click="{addChar, Args=-}"/><button name="*" text="*" v-on:click="{addChar, Args=*}"/><button name="/" text="/" v-on:click="{addChar, Args=/}"/><button name="=" text="=" v-on:click="{eval}"/><button name="backspace" text="<=" v-on:click="{removeChar}"/></view>
</window>
// Do
calculator.xml
<window v-model="calculator"><edit name="expr" x="c" y="10" w="90%" h="30" focus="true" readonly="true" input_type="custom" text="" tips="expression" v-data:text="{expr}"/><view y="60" x="c" w="90%" h="-60" is_keyboard="true" children_layout="default(r=4,c=4,m=5,s=5)" ><button name="button" text="0" /><button name="button" text="1" /><button name="button" text="2" /><button name="button" text="3" /><button name="button" text="4" /><button name="button" text="5" /><button name="button" text="6" /><button name="button" text="7" /><button name="button" text="8" /><button name="button" text="9" /><button name="button" text="+" /><button name="button" text="-" /><button name="button" text="*" /><button name="button" text="/" /><button name="button" text="=" /><button name="button" text="<=" /></view>
</window>//calculator.c
ret_t on_button_button_click(void* ctx, event_t* e) {pointer_event_t* evt = pointer_event_cast(e);m_calculator *m_calculator = get_m_calculator();return_value_if_fail(m_calculator != NULL, RET_BAD_PARAMS);widget_t *btn = WIDGET(e->target);int index = widget_index_of(btn);// 直接通过界面文字给计算器model提供符号数据char text[32];widget_get_text_utf8(btn, text, sizeof(text));m_calculator_set_expr(m_calculator, text);}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, "button")) {widget_on(widget, EVT_CLICK, on_button_button_click, win);}}return RET_OK;
}

又或者菜单几十个单选按钮,按钮功能大致相同,xml上几十个按钮标签,与其一个个在xml写命令绑定(这样维护性很差),不如直接给这些按钮设置相同的名字(可以设置相同名字来遍历控件设置同一个回调,QT上好像做不到这么方便),然后在visit_init_child函数上遍历这些控件注册同一个回调,然后给这些按钮设置一个类似唯一id的自定义属性(或者干脆就是按钮上的文本)用于分支判断,在回调中决定设置哪一个数据;

怎么做?

方法很简单,手动实现这些model的getter和setter函数,setter属性完成之后,通过emitter_dispatch_simple_event来通知模型更新界面。

<view name="v_setting_zoom_mode" style="onwa_setting" x="c" y="m" w="562" on:key_down="menu_rbtn_set_scroll_view(widget_get('self','name'))" h="184"><label name="setting_title" h="34" style="setting_title" focusable="true" x="0" y="0" w="562" tr_text="Zoom mode"/><view name="radio_view2_2" h="150" children_layout="default(c=2,r=2,s=10)" style:normal:bg_color="#00244400" x="0" y="b:0" w="562"><!-- 设定自定义的key属性 --><radio_button name="radio_button" style="onwa_radio2" key="zoom_mode" v-data:value="{zoom_mode==0}" focusable="true" value="true" tr_text="No Zoom"/><radio_button name="radio_button" style="onwa_radio2" key="zoom_mode"  v-data:value="{zoom_mode==1}" focusable="true" tr_text="Bottom Lock"/><radio_button name="radio_button" style="onwa_radio2" key="zoom_mode" v-data:value="{zoom_mode==2}" focusable="true"  tr_text="Auto"/><radio_button name="radio_button" style="onwa_radio2" key="zoom_mode" v-data:value="{zoom_mode==3}" focusable="true"  tr_text="Manual"/></view>
</view>
ret_t on_radio_button_click(void* ctx, event_t* e) {pointer_event_t* evt = pointer_event_cast(e);m_sonar_t *m_sonar = get_m_sonar_model();return_value_if_fail(m_sonar != NULL, RET_BAD_PARAMS);int sonar_mode = m_sonar->sonar_mode;widget_t *btn = WIDGET(e->target);int index = widget_index_of(btn);//获取控件上自定义的key属性const char *key = widget_get_prop_str(btn, "key", "unknown");m_sonar_t* m_sonar = get_m_sonar_model();if (index >= 0) {// Call the m_sonar function with the title and button indexm_sonar_set_prop_int(m_sonar, key, index);}}ret_t m_sonar_set_prop_int(m_sonar_t* m_sonar, const char* key, int value) {if (!m_sonar) {printf("Error: Could not get m_sonar model\r\n");return RET_FAIL;}if (tk_str_end_with(key, CONF_KEY_GAIN_MODE)) {m_sonar_set_gain_mode(m_sonar, value);} else if (tk_str_end_with(key, CONF_KEY_FREQUENCY)) {m_sonar_set_frequency(m_sonar, value);} else if (tk_str_end_with(key, CONF_KEY_ZOOM_MODE)) {m_sonar_set_zoom_mode(m_sonar, value);} ...// 设置完成后,同步modelemitter_dispatch_simple_event(EMITTER(m_sonar), EVT_PROPS_CHANGED);return;
}

动态加载绑定model的UI片段

在AWTK,可以通过navigator_request_t给model设置初始化的参数,但是设置初始化参数的场景AWTK-MVVM原文档里只写明了从一个窗口导航到另一个窗口,对于重复的UI片段就语焉不详了,而如果遇到了一个页面就有这些重复的UI片段,外加这些UI片段都绑定了某个同样的model的场景呢?

项目开发就遇到了这种情况,声纳本身有多个类型,海图机产品有一种能把这些不同的声纳类型画面都显示出来的分屏模式,最多支持3个不同类型声纳的同时显示,要命的是,由于这些声纳类型的参数和模型逻辑高度重合,我都是用的同一个模型来处理的,没能把它们分开(一个声纳模型有几十个参数设置,总共3个类型,要分开就是两百多个参数,十分臃肿了,倒霉的是AWTK-MVVM没有一种类似于继承的机制能让派生类复用父类的数据和函数去做代码绑定),只通过导航到某个类型单个声纳显示的窗口时,通过指定初始化参数来确定不同的声纳类型。

最简单直接的方法就是,一个分屏就是一个model绑定的UI片段,窗口三个分屏就是窗口里面有3个model, 这样可以直接使用原来单屏模式就写好的model代码。

但是业务限制,分屏的数量和各个分屏的模式都不是固定的,这部分的UI只能在代码中动态生成。

对于UI复用,AWTK有提供一个组件机制(本质是简单粗暴的include替换)来实现,通过ui_loader_load_widget_with_parent(组件xml名,父widget)可把对应的组件加载到UI中, 但是接口是属于原版AWTK的,并没有考虑到mvvm的绑定机制,那么这种API有没有MVVM的版本?

查找AWTK-MVVM代码,还真发现有这么一个API:

/*** @method ui_loader_mvvm_load_widget_with_parent* 加载导航请求指定的控件,并指定父控件对象。* @param {navigator_request_t*} req 导航请求。* @param {widget_t*} parent 父控件对象。** @return {widget_t*} 控件对象。*/
widget_t* ui_loader_mvvm_load_widget_with_parent(navigator_request_t* req, widget_t* parent);

声纳模式m_sonar是通过mode来区分不同类型的,那么对于分屏同时显示多个类型的声纳,就可以这么处理:

// 伪代码,看看理解就行了,可以自己试试
sonar_combo.xml
<view v-model="m_sonar"></view>split_window.xml
<view name="combo_views" x="0" y="0" w="800" h="480"><!-- 假如要动态生成下面三个标签: --><view v-model="m_sonar"></view><view v-model="m_sonar"></view><view v-model="m_sonar"></view>
</view>split_window.c
ret_t init_window(widget_t *win)
{widget_t *combo_views = widget_lookup(win, "combo_views", TRUE);...for(int i = 0; i < split_num; i++){navigator_request_t *req = navigator_request_create("sonar_combo", NULL);tk_object_set_prop_pointer(TK_OBJECT(req), "sonar_mode", get_sonar_mode(i));widget_t *combo_view = ui_loader_mvvm_load_widget_with_parent(req, combo_views);return_value_if_fail(combo_view != NULL, RET_BAD_PARAMS);}return RET_OK;
}

这种方法也有缺陷,如果加载的组件标签里面有来自其他model的数据/命令绑定,那么就算当前窗口顶层已经声明了这个model,也是没法绑定的。

修改已经加载了的UI片段的绑定属性

研究AWTK-MVVM代码的的另一个小收获,可以用于解决上面提到的顶层model属性没法绑定的问题。

直接放测试代码,另外也可以看mvvm控件的binding_context_test.cc来了解awtk-mvvm怎么实现绑定。

static binding_context_t* binding_context_create(binding_context_t* parent, const char* vmodel,widget_t* widget) {navigator_request_t* req = navigator_request_create("test", NULL);binding_context_t* ctx = binding_context_awtk_create(parent, vmodel, req, widget);tk_object_unref(TK_OBJECT(req));return ctx;
}static binding_rule_t* binding_rule_create(widget_t* widget, const char* name, const char* val) {binding_rule_t* rule = binding_rule_parse(name, val, widget->vt->inputable);rule->widget = widget;return rule;
}void mvvm_test(widget_t* win, void* ctxx){binding_context_t* ctx;binding_rule_t* rule;ctx = widget_get_prop_pointer(win, WIDGET_PROP_V_MODEL);return_if_fail(ctx != NULL);widget_t *slider = widget_lookup(win, "slider", TRUE);return_if_fail(slider != NULL);rule = binding_rule_create(slider, "v-data:value", "{conf_test.value, Trigger=Changing}");tk_object_set_prop_bool(TK_OBJECT(rule), BINDING_RULE_PROP_INITED, TRUE);binding_context_bind_data(ctx, rule);binding_context_set_bound(ctx, TRUE);// 必须声明这个,不然model的数据没法更新到viewbinding_context_update_to_view(ctx);
}
/*** 初始化窗口*/
ret_t test_mvvm_dynamic_load_init(widget_t* win, void* ctx) {(void)ctx;return_value_if_fail(win != NULL, RET_BAD_PARAMS);widget_foreach(win, visit_init_child, win);mvvm_test(win, ctx);return RET_OK;
}

binding_context_t可以理解为对xml上声明的v-model功能的实际对象,内部对于数据绑定和命令绑定各自维护了一套链表。

对于数据绑定,当数据事件发生时,就会通过查找链表data_binding来找到绑定属性指定的控件,并更改控件数据。

相关文章:

  • 爪形行列式
  • 建造者模式 - Flutter中的乐高大师,优雅组装复杂UI组件!
  • 前后端分离场景下的用户登录玩法Sa-token框架使用
  • 【系统分析师】2021年真题:论文及解题思路
  • XWPFDocument导出word文件
  • 计算机组成原理与体系结构-实验四 微程序控制器 (Proteus 8.15)
  • day46/60
  • Java垃圾收集机制Test
  • Modbus 报文结构与 CRC 校验实战指南(一)
  • 5 c++核心——文件操作
  • 【NLP 实战】蒙古语情感分析:从 CNN 架构设计到模型训练的全流程解析(内附项目源码及模型成果)
  • EPLAN Electric P8 2.9 零基础保姆级安装教程
  • 力扣第84题-柱状图中最大的矩形
  • [Python 基础课程]Python 规范
  • 【QT】信号和槽(1) 使用 || 定义
  • 信号处理分析工具——时频分析(二)
  • python sklearn 机器学习(1)
  • 【C++】C++的虚析构函数
  • Linux基本命令篇 —— head命令
  • 什么是P2P 网络(Peer-to-Peer Network)