LVGL学习笔记(六)

LVGL学习笔记(六)

事件冒泡

前言

在上一篇中我们介绍了事件的相关概念以及其初步的使用,在阐述的过程中提及了事件冒泡,这一篇主要围绕事件冒泡相关概念进行阐述,并举实例实现一个事件冒泡的实例。

  • 什么是事件冒泡?

    当一个事件在对象上被触发后,LVGL首先执行该对象上所有回调函数。如果事件冒泡未被阻止,同一事件会被重新派发给父对象,然后继续向上传播,直到抵达屏幕根对象。

    由于这个过程像水中的气泡从深处不断向上冒出,因此得名事件冒泡。

  • 为什么需要事件冒泡,事件冒泡的使用场景是什么?

    举例来说,你需要设计一个计算器面板,按照正常事件的逻辑,每一个按钮你都需要绑定一个回调函数,然后将按钮标签上对应的值传递到回调函数中进行处理。

    如果只有1-2个按钮,问题还不是太大。但如果是几十个按钮,挨个绑定回调函数造成的代码重复不说,单单是后期几十份回调函数的维护就十分麻烦,更不用说还会增加内存的开销,造成不需要的浪费。

    这个时候就需要使用冒泡事件了,他的核心价值就是把一堆子控件的事件,交给父对象统一处理,而不用给每一个按钮单独绑定回调函数。

    事件冒泡有如下的使用场景:

    1.一组同类按钮,统一管理,例如数字键盘、功能按键栏、一排选项卡、九宫格菜单等;

    2.容器整体拦截事件。比如一个卡片容器,卡片里面包含文字、小图标按钮。点击卡片内部任意位置,都可以触发卡片的点击效果。开启冒泡后,点里面文字,事件上浮到卡片父对象,卡片回调被触发。

    3.事件统一过滤。父对象先收到冒泡上来的事件,可以做统一处理,过滤掉不需要的信息。

  • 如何实现事件冒泡?

    还是以上面的计算器举例,创建按钮对象后,开启回调函数,然后启动启动函数lv_obj_add_flag(),函数原型如下:

    c 复制代码
    void lv_obj_add_flag(lv_obj_t * obj, lv_obj_flag_t f);

    函数lv_obj_add_flag()是LVGL 中用于给对象添加特定行为或状态标志的函数。通过为对象设置不同的 lv_obj_flag_t 枚举值,可以控制对象在界面中的交互方式、显示效果以及布局行为。

    形参如下:

    形参 说明
    obj 目标对象指针
    f 要添加的标志

    标志结构体如下所示:

    c 复制代码
    enum {
        LV_OBJ_FLAG_HIDDEN          = (1L << 0),  隐藏对象(不参与布局与绘制)
        LV_OBJ_FLAG_CLICKABLE       = (1L << 1),  对象可被点击
        LV_OBJ_FLAG_CLICK_FOCUSABLE = (1L << 2),  
        LV_OBJ_FLAG_CHECKABLE       = (1L << 3),  
        LV_OBJ_FLAG_SCROLLABLE      = (1L << 4),  
        LV_OBJ_FLAG_SCROLL_ELASTIC  = (1L << 5),  
        LV_OBJ_FLAG_SCROLL_MOMENTUM = (1L << 6), 
        LV_OBJ_FLAG_SCROLL_ONE      = (1L << 7),  
        LV_OBJ_FLAG_SCROLL_CHAIN_HOR = (1L << 8), 
        LV_OBJ_FLAG_SCROLL_CHAIN_VER = (1L << 9), 
        LV_OBJ_FLAG_SCROLL_ON_FOCUS = (1L << 10),  获得焦点时自动滚动到可见区域
        LV_OBJ_FLAG_SCROLL_WITH_ARROW  = (1L << 11), 
        LV_OBJ_FLAG_SNAPPABLE       = (1L << 12), 
        LV_OBJ_FLAG_PRESS_LOCK      = (1L << 13), 
        LV_OBJ_FLAG_EVENT_BUBBLE    = (1L << 14), 事件冒泡到父对象
        LV_OBJ_FLAG_GESTURE_BUBBLE  = (1L << 15), 手势事件冒泡到父对象
        LV_OBJ_FLAG_ADV_HITTEST     = (1L << 16), 启用高级命中测试(自定义点击区域)
        LV_OBJ_FLAG_IGNORE_LAYOUT   = (1L << 17), 
        LV_OBJ_FLAG_FLOATING        = (1L << 18), 
        LV_OBJ_FLAG_OVERFLOW_VISIBLE = (1L << 19), 允许子对象绘制超出父对象范围
        LV_OBJ_FLAG_LAYOUT_1        = (1L << 23), 
        LV_OBJ_FLAG_LAYOUT_2        = (1L << 24), 
        LV_OBJ_FLAG_WIDGET_1        = (1L << 25), 
        LV_OBJ_FLAG_WIDGET_2        = (1L << 26), 
        LV_OBJ_FLAG_USER_1          = (1L << 27), 
        LV_OBJ_FLAG_USER_2          = (1L << 28), 
        LV_OBJ_FLAG_USER_3          = (1L << 29), 
        LV_OBJ_FLAG_USER_4          = (1L << 30), 
    
    };
    typedef uint32_t lv_obj_flag_t;

    这里选择LV_OBJ_FLAG_EVENT_BUBBLE,事件冒泡。

    事件从标签依次冒泡到按钮->父对象面板->屏幕,展现了层级传递机制。

    接下来需要获取对象传递过来的信息,这就需要上篇中我们未详细说明的函数lv_event_get_user_data(),其原型如下:

    c 复制代码
    void * lv_event_get_user_data(lv_event_t * e)

    函数lv_event_get_user_data()是 LVGL库中用于获取事件回调函数中用户自定义数据的函数。它在事件处理逻辑中起到了跨作用域传递数据的作用,常用于在回调函数中访问绑定的自定义数据。

    • 形参: e: 事件对象指针,由 LVGL 事件系统自动传递到回调函数中。
    • 返回值: 返回 void* 类型的用户自定义数据指针,需根据实际类型进行强制转换后使用。

接下来,将上述举例的计算器面板实现出来,代码如下所示:

定义全局变量

c 复制代码
// 显示屏标签全局对象
static lv_obj_t* label_display;
static char calc_buffer[64] = { 0 };
// 全局按键字符串表
static const char* key_list[] =
{
    "0","1","2","3","4","5","6","7","8","9","+","-","*","/","="
};

设置LVGL主函数,网格布局函数内容可以翻阅学习笔记四

c 复制代码
void lv_mainstart(void)
{
    //主屏幕
    lv_obj_t* root = lv_scr_act();

    //创建显示屏
    label_display = lv_label_create(root);
    lv_label_set_text(label_display, "");
    lv_obj_set_size(label_display, 300, 80);
    lv_obj_align(label_display, LV_ALIGN_TOP_MID, 0, 20);
    lv_obj_set_style_text_align(label_display, LV_TEXT_ALIGN_RIGHT, 0);

    //按键父对象(网格布局,放置所有按键)
    lv_obj_t* keypad_panel = lv_obj_create(root);
    lv_obj_set_size(keypad_panel, 300, 400);
    lv_obj_align_to(keypad_panel, label_display, LV_ALIGN_OUT_BOTTOM_MID, 0, 20);

    //给父对象添加点击回调函数,所有子按钮冒泡上来的事件在这里接收
    lv_obj_add_event_cb(keypad_panel, keypad_parent_cb, LV_EVENT_CLICKED, NULL);

    // 开启网格布局排列
    lv_obj_set_layout(keypad_panel, LV_LAYOUT_GRID);

    // 定义列描述:4列,宽度都为50px
    static lv_coord_t col_dsc[] = { 50,50,50,50, LV_GRID_TEMPLATE_LAST };
    // 定义行描述:4行,高度都为50px
    static lv_coord_t row_dsc[] = { 50,50,50,50, LV_GRID_TEMPLATE_LAST };

    // 设置对象网格布局行描述数组
    lv_obj_set_style_grid_column_dsc_array(keypad_panel, col_dsc, 0);
    // 设置对象网格布局列描述数组
    lv_obj_set_style_grid_row_dsc_array(keypad_panel, row_dsc, 0);
    //设置网格布局中列与行的整体对齐方式
    lv_obj_set_grid_align(keypad_panel, LV_GRID_ALIGN_CENTER, LV_GRID_ALIGN_CENTER);


    //创建按键
    lv_obj_t* btn;
    btn = create_key_btn(keypad_panel, "1", key_list[1]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 0, 1);

    btn = create_key_btn(keypad_panel, "2", key_list[2]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 0, 1);

    btn = create_key_btn(keypad_panel, "3", key_list[3]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 0, 1);

    btn = create_key_btn(keypad_panel, "+", key_list[10]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 0, 1);

    btn = create_key_btn(keypad_panel, "4", key_list[4]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1);

    btn = create_key_btn(keypad_panel, "5", key_list[5]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 1, 1);

    btn = create_key_btn(keypad_panel, "6", key_list[6]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 1, 1);

    btn = create_key_btn(keypad_panel, "-", key_list[11]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 1, 1);

    btn = create_key_btn(keypad_panel, "7", key_list[7]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 2, 1);

    btn = create_key_btn(keypad_panel, "8", key_list[8]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 2, 1);

    btn = create_key_btn(keypad_panel, "9", key_list[9]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 2, 1);

    btn = create_key_btn(keypad_panel, "*", key_list[12]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 2, 1);

    btn = create_key_btn(keypad_panel, "0", key_list[0]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 3, 1);

    btn = create_key_btn(keypad_panel, "=", key_list[14]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 1, 2, LV_GRID_ALIGN_STRETCH, 3, 1);

    btn = create_key_btn(keypad_panel, "/", key_list[13]);
    lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 3, 1);
    
}

设置按钮创建函数

c 复制代码
static lv_obj_t* create_key_btn(lv_obj_t* parent, const char* text, const char* key_text)
{
    //创建按键
    lv_obj_t* btn = lv_btn_create(parent);
    //设置按钮大小
    lv_obj_set_size(btn, 50, 50);
    //开启事件冒泡:点击事件向上传递给父对象
    lv_obj_add_flag(btn, LV_OBJ_FLAG_EVENT_BUBBLE);
    //获取用户数据
    lv_obj_set_user_data(btn, (void*)key_text);
    //创建标签
    lv_obj_t* label = lv_label_create(btn);
    //设置标签内容 
    lv_label_set_text(label, text);
    //对象居中
    lv_obj_center(label);

    return btn;
}

设置回调函数

c 复制代码
/**
 * @brief 父容器的事件回调(利用冒泡事件统一处理所有子按键)
 */
static void keypad_parent_cb(lv_event_t* e)
{
    // 获取真正被点击的子对象
    lv_obj_t* target = lv_event_get_target(e);

    //取出按键上绑定的字符
    char* key_char = (char*)lv_obj_get_user_data(target);

    if (key_char == NULL)
        return;

    // 判断按下等于号
    if (strcmp(key_char, "=") == 0)
    {
        float res;
        int ret = calc_two_num(calc_buffer, &res);
        if (ret == 0)
        {
            //计算成功,打印结果
            snprintf(calc_buffer, sizeof(calc_buffer), "%.2f", res);
            LV_LOG_USER("sum = %f", res);
        }
        else
        {
            //算式非法
            strcpy(calc_buffer, "ERROR");
        }
        //刷新显示屏标签
        lv_label_set_text(label_display, calc_buffer);
        //计算完成清空缓存,准备下一轮计算
        memset(calc_buffer, 0, sizeof(calc_buffer));
    }
    //普通数字/运算符按键,追加字符
    else
    {
        //追加按键字符到全局缓冲区
        strcat(calc_buffer, key_char);
        //更新屏幕标签
        lv_label_set_text(label_display, calc_buffer);
    }
}

设置计算器应用逻辑(只是举例,可以自行设计)

c 复制代码
/**
 * @brief 简易计算器,解析字符串算式,只支持 两个数运算,例:"12+34"
 * @param expr 算式字符串
 * @param result 输出结果
 * @return 0成功,‑1失败
 */
static int calc_two_num(const char* expr, float* result)
{
    char op = 0;
    int index = -1;
    float num1, num2;

    //遍历字符串找到运算符 + - * /
    for (int i = 0; expr[i] != '\0'; i++)
    {
        if (expr[i] == '+' || expr[i] == '-' || expr[i] == '*' || expr[i] == '/')
        {
            op = expr[i];
            index = i;
            break;
        }
    }

    //没有找到运算符
    if (index <= 0)
        return -1;

    //拆分前后两个数字
    char num1_str[32] = { 0 }, num2_str[32] = { 0 };
    strncpy(num1_str, expr, index);
    strcpy(num2_str, expr + index + 1);

    num1 = atof(num1_str);
    num2 = atof(num2_str);

    switch (op)
    {
    case '+': *result = num1 + num2; break;
    case '-': *result = num1 - num2; break;
    case '*': *result = num1 * num2; break;
    case '/':
        if (num2 == 0) return -1;  //除零报错
        *result = num1 / num2;
        break;
    default: return -1;
    }
    return 0;
}

运行结果:

LVGL计算器演示实例_事件冒泡

相关推荐
zyf1044161 小时前
暑期实践日志 Day33:整体复盘核查视频,完善剪辑成果
学习·计算机网络·剪辑·暑期实践·课题任务
01_ice1 小时前
前端学习css
前端·css·学习
Wang's Blog2 小时前
PostgreSQL笔记49:向量检索核心算法、索引调优与过滤策略深度解析
笔记·算法·postgresql
疯狂打码的少年2 小时前
【数据结构】交换类排序:冒泡与快速排序
数据结构·笔记·算法·排序算法
chnyi6_ya2 小时前
VideoHallu 论文阅读笔记
论文阅读·笔记
Wang's Blog2 小时前
PostgreSQL笔记51: 基于 pgvector 构建企业级智能问答系统
数据库·笔记·postgresql
for_ever_love__3 小时前
PySpark学习: 数据输出
开发语言·python·学习·spark
自小吃多3 小时前
Capture软件原理图添加差分属性笔记
笔记·嵌入式硬件
日常筹谋记3 小时前
ATS选不对运维两行泪:数据中心市电与柴发切换场景的双电源自动转换开关选型笔记
运维·数据库·笔记