AWTK-MVVM 如何让多个View复用一个Model记录+关于app_conf的踩坑

前言

有这么一个业务,主界面点击应用窗口进入声纳显示界面,声纳显示界面再通过按钮进入菜单界面,菜单界面有很多关于该声纳显示界面的设置项,比如量程,增益,时间显示,亮度,对比度等等,大概十几个设置。

有些数值类的设置还有子预览菜单,在子预览菜单里面通过滑条去设置数值,回到菜单后,设置会显示子预览菜单设置的数值。

声纳显示界面需要显示一些菜单的设置,比如量程,增益等等。

也就是大概这么一个页面关系,其中后面三个页面之间还有数据依赖的关系。

image-20250412120317482

由于菜单的设置项非常多,用传统的基于控件树的方法写起来代码量很大,而且美工时常改动菜单UI,容易影响界面代码,我当时自然而然的选择了AWTK特有的MVVM框架来完成菜单设置的显示逻辑。

一开始由于赶项目时间,我就直接在界面上使用了mvvm的app_conf功能,app_conf在AWTK-MVVM还有专门的model,使得不用写代码就能完成配置文件与多个界面间的数据联动与设置保存,十分贴心。

虽然这种方法能够快速实现功能,但是后期维护性极差,因为业务的配置key都写死在xml中,跟xml耦合。

比如这样的一个设置项:

<window v-model="m_home" name="filter_color">
  <view name="v_setting_color" x="175" y="0" w="387" h="183">
    <label name="title" x="9" y="10" w="58" h="28" text="Color"/>
    <view name="view" x="34" y="58" w="359" h="112" children_layout="default(c=3,r=3)">
      <radio_button name="radio_button" v-data:value="{A.color==1}" text="color1"/>
      <radio_button name="radio_button" v-data:value="{A.color==2}" text="color2"/>
      <radio_button name="radio_button" v-data:value="{A.color==3}" text="color3"/>
      <radio_button name="radio_button" v-data:value="{A.color==4}" text="color4"/>
      <radio_button name="radio_button" v-data:value="{A.color==5}" text="color5"/>
      <radio_button name="radio_button" v-data:value="{A.color==6}" text="color6"/>
      <radio_button name="radio_button" v-data:value="{A.color==7}" text="color7"/>
      <radio_button name="radio_button" v-data:value="{A.color==8}" text="color8"/>
    </view>
  </view>
</window>

实际业务是三种不同的声纳模式的设置菜单A, B, C, 三个设置菜单每个界面都有十几个行十几个设置项,加起来就是三十几个设置项,而且这些设置项的UI大都重复。

每个声纳的大部分属性还是各自独立的,也就是不同菜单的同一个设置,上面的color,A,B,C菜单都在用,A用A.color, B就是B.color,同样的key,父路径不同,这就导致没法用AWTK的component机制将这些设置项UI抽象出来复用,变成这样:

<window v-model="m_home" name="filter_color">
  <?include filename="comp_setting_color.xml" ?>
</window>

(话说AWTK的这个组件机制真的鸡肋,就是单纯的include替换,连个內部slot, 组件通信也没有)

如果用上述的绑死app_conf key的方式来做,后面一旦加了什么影响UI的新功能或者菜单风格更改,又要一个个菜单的去照着美工原型图去改,十分痛苦。

而且app_conf模型自身的命令绑定十分有限,稍微复杂一点的需求(比如点击按钮发送MQTT)就做不了,还是要结合自定义model的命令绑定或者传统的基于控件名索引的widget_on, 自己写函数

等到项目周期开始放缓,我决心把之前写死了配置key的几个菜单页面给重构了,改成用自定义model的自定义的属性来做数据绑定,在代码层面实现具体的选择菜单A,B,C的逻辑。

重构跟本文的逻辑不大,就不展开了,我在这边引出这些,是因为之前直接用app_conf有一个优点,就是不用关心页面之间数据联动的问题,awtk-mvvm内部代码自己会处理好,如果用自定义model, 就要写代码理清窗口导航的数据流通关系了,确保窗口退出时返回正确的设置数据给上一个页面。

实践

回到这个界面关系中,由于显示界面,设置菜单,子菜单都指向同一个对象的数据,考虑到三个页面后期可能的变动,我索性让三个页面都使用同一个model了。

我的目标是,弄清楚三个页面使用同一个model之后窗口的传参如何处理,才能实现子菜单设置时能够返回保存的数据。

抽象的例子如下,所有界面绑定一个叫m_home的model。

image-20250412170404083

sonar_page有bottom_lock,noise_limiter,pic_advance三个界面,每个界面都是在独立的子菜单中设置,设置完的结果会在sonar_page上显示。

<window v-model="m_home" name="sonar_page">
  <button name="button" x="272" y="320" w="100" h="36" v-on:click="{mreturn}" text="Back"/>
  <label name="value" x="272" y="49" w="160" h="28" v-data:text="{value_int}"/>
  <label name="key" x="272" y="104" w="160" h="28" v-data:text="{noise_limiter}" text="setting_item"/>
  <label name="key" x="272" y="178" w="160" h="28" v-data:text="{pic_advance}" text="setting_item"/>
  <button name="button1" x="101" y="49" w="100" h="36" text="Button"  v-on:click="{home_navigate, Args=string?page_name=bottom_lock}"/>
  <button name="button1" x="101" y="104" w="100" h="36" text="Button" v-on:click="{home_navigate, Args=string?page_name=noise_limiter}"/>
  <button name="button1" x="101" y="170" w="100" h="36" text="Button" v-on:click="{home_navigate, Args=string?page_name=pic_advance}"/>
</window>

一开始我犯了个错误,感觉每个页面重复建相同的Model,旧Model还要拷贝数据到新Model, 开销比较大,就把Model的创建和销毁搞成了引用计数的模式:

m_home_t *last_page_model = NULL;
m_home_t *g_home_ref = NULL;
static int g_home_ref_count = 0;

m_home_t* m_home_create(navigator_request_t* req)
{
    m_home_t *home = NULL;
    if(!g_home_ref){
        home = TKMEM_ZALLOC(m_home_t);
        str_init(&home->pic_advance, 32);
    }
    else{
        home = g_home_ref;
    }
    g_home_ref = home;
    g_home_ref_count++;
    home->req = req;

    printf("m_home=%#x created, value: %d ref_count: %d\r\n", home, home->value_int, g_home_ref_count);
    return home;
}

ret_t m_home_on_return(navigator_request_t* req, const value_t* result)
{
    m_home_t *home = g_home_ref;
    printf("set last model %p\r\n", home);
    emitter_dispatch_simple_event(EMITTER(home), EVT_PROPS_CHANGED);
    return RET_OK;
}


ret_t m_home_mreturn(m_home_t *home)
{
    value_t v;
    navigator_request_on_result(home->req, &v);
    navigator_back();
    return RET_OK;
}


ret_t m_home_destroy(m_home_t* home)
{
    g_home_ref_count--;
    if(g_home_ref_count == 0){
        str_reset(&home->pic_advance);
        TKMEM_FREE(home);
        g_home_ref = NULL;
    }
    printf("m_home=%#x destroyed\r\n", home);
    return RET_OK;  
}

但是后面发现子菜单上设置的值返回后无法在sonar_page上显示,查了半天,才发现m_home_on_return设置的其实是只跟当前界面有关的view_model,一旦返回这个页面就销毁了,根本影响不到上一个页面的view model。

image-20250412172638543

只好老实了,乖乖用默认的一个view一个model的传统构建方法,导航到新页面时把旧model作为参数, 传给新model,新model拷贝旧model的参数,退出页面时,旧model从新model加载数据。

实际业务是进页面从app_conf load数据,退页面save 数据到app_conf,然后旧model再从app_conf load数据的,这个例子里面省略了,直接copy对象来表示。

#include "m_home.h"
#include "awtk.h"
#include "mvvm/mvvm.h"
#include "mvvm/base/utils.h"

m_home_t *last_page_model = NULL;
m_home_t *g_current_home_ref = NULL;

void m_home_data_copy(m_home_t *ahome, m_home_t *bhome)
{
    ahome->value_int = bhome->value_int;
    str_set(&ahome->pic_advance, bhome->pic_advance.str);
    ahome->noise_limiter = bhome->noise_limiter;
}

m_home_t* m_home_create(navigator_request_t* req)
{
    m_home_t *home = NULL;
    home = TKMEM_ZALLOC(m_home_t);
    str_init(&home->pic_advance, 32);
    m_home_t *last_model = tk_object_get_prop_pointer(TK_OBJECT(req), "last_model");
    if(last_model != NULL){
        m_home_data_copy(home, last_model);
    }
    g_current_home_ref = home;
    home->req = req;

    printf("m_home=%p created, value: %d last_model: %p\r\n", home, home->value_int, last_model);
    return home;
}

ret_t m_home_destroy(m_home_t* home)
{
    str_reset(&home->pic_advance);
    TKMEM_FREE(home);
    g_current_home_ref = NULL;
    printf("m_home=%#x destroyed\r\n", home);
    return RET_OK;  
}

ret_t m_home_on_return(navigator_request_t* req, const value_t* result)
{
    m_home_t *home = g_current_home_ref;
    m_home_t *last_model = tk_object_get_prop_pointer(TK_OBJECT(req), "last_model");
    printf("set last model %p\r\n", last_model);
    if(last_model != NULL){
        m_home_data_copy(last_model, home);
        emitter_dispatch_simple_event(EMITTER(last_model), EVT_PROPS_CHANGED);
    }
    return RET_OK;
}


ret_t m_home_mreturn(m_home_t *home)
{
    value_t v;
    g_current_home_ref = home;
    navigator_request_on_result(home->req, &v);
    navigator_back();
    return RET_OK;
}




ret_t m_home_to_navigate(m_home_t *home, const char *args)
{   
    tk_object_t *obj = object_default_create();
    tk_command_arguments_to_object(args, obj);
    const char *page_name = tk_object_get_prop_str(obj, "page_name");

    navigator_request_t* req = navigator_request_create(page_name, m_home_on_return);
    tk_object_set_prop_pointer(TK_OBJECT(req), "last_model", home);
    navigator_to_ex(req);
    tk_object_unref(TK_OBJECT(req));
    return RET_OK;
}

ret_t m_home_set_prop_int(m_home_t *home, const char *args)
{
    tk_object_t *obj = object_default_create();
    tk_command_arguments_to_object(args, obj);
    const char *key = tk_object_get_prop_str(obj, "key");
    int32_t value = tk_object_get_prop_int(obj, "value", 0);
    if(tk_str_eq(key, "noise_limiter")){
        home->noise_limiter = value;
    }
    else{
        home->value_int = value;
    }
    printf("m_home_set_prop_int: %s = %d\r\n", key, value);
    TK_OBJECT_UNREF(obj);
    return RET_OBJECT_CHANGED;
}

ret_t m_home_set_prop_str(m_home_t *home, const char *args)
{
    tk_object_t *obj = object_default_create();
    tk_command_arguments_to_object(args, obj);
    const char *key = tk_object_get_prop_str(obj, "key");
    const char *value = tk_object_get_prop_str(obj, "value");
    if(tk_str_eq(key, "pic_advance")){
        str_set(&home->pic_advance, value);
        printf("pic_advance set %s\r\n", home->pic_advance.str);
    }
    printf("m_home_set_prop_str: %s = %s\r\n", key, value);
    TK_OBJECT_UNREF(obj);
    return RET_OBJECT_CHANGED;
}

逻辑展示如下:

image-20250412174251820

懒得展开了,放上代码:

https://gitee.com/tracker647/awtk-practice/tree/master/awtk_mvvm_shared_model_return_test

效果:

image-20250412182014755

image-20250412182027504

附录:关于sub_view_model

虽然app_conf有一个sub_view_model的功能可以缓解不同object有一样的配置key的问题,但是实际业务里配置既有私有配置也有共通配置,共通配置还是混杂在私有配置里面的,配置文件的结构是这样:

shared_conf:{
	range_mode:1
	range_val:10
};
A:{
  color:1
}
B:{
 color:2
}
C:{
 color:3
}

设置项的位置表现上,是这种情况:

私有属性
共有属性
私有属性
共有属性

如果使用sub_view_model,就要另外给相关的配置包上带sub_view_model属性的view标签。

上面的例子就要包两次sub_view_model标签,对于之前业务那种设置项多的情况就是会建立很多个冗余的只用于限定设置作用域的model, 程序上十分不优雅且有不稳定的风险,我找了一圈AWTK库,没有找到在sub_view_model的标签作用域里引用父级model来索引到公共属性的方法,只好放弃。

AWTK开发手册-AWTK开发实践指南-中文手册.pdf AWTK = Toolkit AnyWhere 随着手机、智能手表等便携式设备的普及,用户对 GUI 的要求越来越高,嵌入式系统对高性能、高可靠性、低功耗、美观炫酷的 GUI 的需求也越来越迫切,ZLG开源 GUI 引擎 AWTK 应运而生。AWTK 全称为 Toolkit AnyWhere,是 ZLG 倾心打造的一套基于 C 语言开发的 GUI 框架。旨在为用户提供一个功能强大、高效可靠、简单易用、可轻松做出炫酷效果的 GUI 引擎,并支持跨平台同步开发,一次编程,终生使用。 最终目标: 支持开发嵌入式软件。 支持开发Linux应用程序。 支持开发MacOS应用程序。 支持开发Windows应用程序。 支持开发Android应用程序。 支持开发iOS应用程序。 支持开发2D游戏。 其主要特色有: 小巧。在精简配置下,不依赖第三方软件包,仅需要32K RAM + 256K FLASH即可开发一些简单的图形应用程序。 高效。采用脏矩形裁剪算法,每次只绘制和更新变化的部分,极大提高运行效率和能源利用率。 稳定。通过良好的架构设计和编程风格、单元测试、动态(valgrind)检查和Code Review保证其运行的稳定性。 丰富的GUI组件。提供窗口、对话框和各种常用的组件(用户可以配置自己需要的组件,降低对运行环境的要求)。 支持多种字体格式。内置位图字体(并提供转换工具),也可以使用stb_truetype或freetype加载ttf字体。 支持多种图片格式。内置位图图片(并提供转换工具),也可以使用stb_image加载png/jpg等格式的图片。 紧凑的二进制界面描述格式。可以手工编辑的XML格式的界面描述文件,也可以使用Qt Designer设计界面,然后转换成紧凑的二进制界面描述格式,提高运行效率,减小内存开销。 支持主题并采用紧凑的二进制格式。开发时使用XML格式描述主题,然后转换成紧凑的二进制格式,提高运行效率,减小内存开销。 支持裸系统,无需OS和文件系统。字体、图片、主题和界面描述数据都编译到代码中,以常量数据的形式存放,运行时无需加载到内存。 内置nanovg实现高质量的矢量动画,并支持SVG矢量图。 支持窗口动画、控件动画、滑动动画和高清LCD等现代GUI常见特性。 支持国际化(Unicode、字符串翻译和输入法等)。 可移植。支持移植到各种RTOS和嵌入式Linux系统,并通过SDL在各种流行的PC/手机系统上运行。 脚本化。从API注释中提取API的描述信息,通过这些信息可以自动生成各种脚本的绑定代码。 支持硬件2D加速(目前支持STM32的DMA2D和NXP的PXP)和GPU加速(OpenGL/OpenGLES/DirectX/Metal),充分挖掘硬件潜能。 丰富的文档和示例代码。 采用LGPL协议开源发布,在商业软件中使用时无需付费。 目前核心功能已经完成,内部开始在实际项目中使用了,欢迎有兴趣的朋友评估和尝试,期待您的反馈。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值