在項目中用了一段時間的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來找到綁定屬性指定的控件,并更改控件數據。