4、LVGL对象系统:对象树结构、父子关系、坐标系统、事件传递机制、样式系统基础

LVGL 的对象系统,说白了就是整个 GUI 的骨架。你想想看,屏幕上那么多按钮、标签、滑块,它们怎么组织?怎么知道谁在谁上面?点击事件怎么传递?样式怎么继承?

这些问题的答案,都藏在 LVGL 的对象系统里。我刚开始用 LVGL 的时候,觉得不就是画几个控件嘛,直接往上堆就完了。结果项目一复杂,界面一多,各种问题就冒出来了——控件重叠不知道谁盖谁,事件响应乱套,样式改一个全变了……

嗯,后来我才明白,理解对象系统,是做好嵌入式 GUI 的第一步。

4.1 对象树结构:一切皆对象

LVGL 里,每个可见元素都是一个对象(lv_obj_t)。按钮是对象,标签是对象,滑块也是对象。它们按照树形结构组织在一起。

这棵树有一个根节点,叫 lv_scr_act(),也就是当前活动屏幕。所有对象都是这个根节点的子孙。

核心概念:LVGL 的对象树是一棵多叉树。每个节点可以有任意多个子节点,但只有一个父节点。根节点是屏幕对象。

我画了一张图,帮你直观理解这个结构:

LVGL 对象树结构图 lv_scr_act() 屏幕对象 容器对象 (lv_cont) 按钮对象 (lv_btn) 标签对象 (lv_label) 滑块 (lv_slider) 开关 (lv_switch) 标签 (lv_label) 子标签 说明 每个对象只有一个父节点 但可以有多个子节点 子节点在父节点之上绘制

看到这张图了吗?屏幕对象在最上面,下面挂着容器、按钮、标签。容器下面又可以挂滑块、开关。这就是典型的树形结构。

我的经验:我习惯把界面按功能模块分组。比如一个设置页面,我会先创建一个容器作为"设置组",然后把所有设置项挂在这个容器下面。这样管理起来特别清晰,删除或隐藏整个组也只需要操作父对象。

4.2 父子关系:谁管谁?

父子关系决定了三件事:显示层级、坐标参考、生命周期。

  • 显示层级:子对象永远在父对象之上绘制。你创建一个按钮放在容器里,按钮一定在容器上面。
  • 坐标参考:子对象的坐标是相对于父对象的。父对象移动,子对象跟着动。
  • 生命周期:删除父对象,所有子对象也会被自动删除。这个特性用好了很方便,用不好就是坑。

我曾经在一个项目里,动态创建了一堆控件,但忘了它们挂在哪个父对象下面。后来想清理内存,删了几个父对象,结果子对象也跟着没了,界面直接崩了。嗯,从那以后我每次创建对象都会明确指定父对象。

// 创建对象时指定父对象
lv_obj_t * parent = lv_obj_create(lv_scr_act());  // 父对象是当前屏幕
lv_obj_t * child = lv_btn_create(parent);          // 子对象是按钮,挂在 parent 下面

// 修改父子关系
lv_obj_set_parent(child, new_parent);              // 把 child 移到 new_parent 下面

// 获取父子关系
lv_obj_t * p = lv_obj_get_parent(child);           // 获取父对象
uint16_t count = lv_obj_count_children(parent);    // 获取子对象数量

注意:不要循环设置父子关系。比如 A 是 B 的父对象,B 又是 A 的父对象,这会导致死循环。LVGL 不会帮你检测这个,程序会直接卡死。

4.3 坐标系统:对象在哪儿?

LVGL 的坐标系统,说白了就是每个对象都有一个矩形区域。这个区域由 x、y、宽度、高度 四个值决定。

但这里有个关键点:坐标是相对的。子对象的 (0,0) 点,是父对象内容区域的左上角。

属性 含义 示例
x 相对于父对象内容区域左边界的偏移 lv_obj_set_x(obj, 10)
y 相对于父对象内容区域上边界的偏移 lv_obj_set_y(obj, 20)
width 对象的宽度(包括边框和填充) lv_obj_set_width(obj, 100)
height 对象的高度(包括边框和填充) lv_obj_set_height(obj, 50)

你可能会问:那怎么获取对象在屏幕上的绝对坐标?

// 获取对象在屏幕上的绝对坐标
lv_point_t point;
lv_obj_get_coords(obj, &point);  // point.x 和 point.y 就是屏幕坐标

我个人习惯在调试布局的时候,打印出对象的绝对坐标和大小,看看是不是我预期的位置。尤其是在嵌套多层容器的时候,坐标很容易算错。

4.4 事件传递机制:谁响应?

事件传递,就是当用户点击屏幕时,LVGL 怎么找到该响应这个点击的对象。

LVGL 的事件传递分三步:

  1. 命中检测:从最上层的对象开始,检查点击点是否在对象区域内。如果在,就选中这个对象。
  2. 事件发送:向选中的对象发送事件(如 LV_EVENT_CLICKED)。
  3. 事件冒泡:如果对象没有处理这个事件,或者处理完后允许继续传递,事件会向上冒泡到父对象。

关键点:事件冒泡默认是关闭的。你需要调用 lv_obj_add_flag(obj, LV_OBJ_FLAG_EVENT_BUBBLE) 来开启。

我曾经遇到一个情况:一个按钮放在容器里,我想让容器也响应点击事件。但不管我怎么点,事件都只被按钮吃掉,容器收不到。后来才发现,按钮默认不冒泡事件。加上冒泡标志后,容器就能收到事件了。

// 开启事件冒泡
lv_obj_add_flag(btn, LV_OBJ_FLAG_EVENT_BUBBLE);

// 注册事件回调
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(container, container_event_cb, LV_EVENT_CLICKED, NULL);

// 事件回调函数
static void btn_event_cb(lv_event_t * e) {
    // 处理按钮点击
    // 如果不调用 lv_event_stop_bubbling(e),事件会继续向上传递
}

static void container_event_cb(lv_event_t * e) {
    // 处理容器点击(按钮冒泡上来的事件)
}

我的建议:事件冒泡虽然方便,但不要滥用。我一般只在需要"组控制"的时候才用。比如一个面板上的所有控件点击后,面板本身也要做某些处理。其他情况,尽量让每个对象自己处理自己的事件,逻辑更清晰。

4.5 样式系统基础:对象长什么样?

样式,就是决定对象外观的东西。颜色、边框、圆角、阴影、字体……这些都是样式控制的。

LVGL 的样式系统有几个核心概念:

  • 样式属性:每个样式由多个属性组成,比如背景颜色、边框宽度、文字颜色等。
  • 样式状态:同一个对象在不同状态下可以有不同的样式。比如按钮默认是蓝色,按下时变成深蓝色。
  • 样式继承:子对象可以继承父对象的样式属性。如果子对象没有设置某个属性,就会从父对象那里拿。

我刚开始用样式的时候,总是每个控件单独设置样式,代码又长又乱。后来学会了用样式类,把通用的样式提取出来,代码清爽多了。

// 创建一个样式
static lv_style_t style_btn;
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_color_hex(0x3498db));
lv_style_set_border_width(&style_btn, 2);
lv_style_set_border_color(&style_btn, lv_color_hex(0x2980b9));
lv_style_set_radius(&style_btn, 8);

// 应用到对象
lv_obj_add_style(btn, &style_btn, 0);  // 0 表示默认状态

// 不同状态下的样式
static lv_style_t style_btn_pressed;
lv_style_init(&style_btn_pressed);
lv_style_set_bg_color(&style_btn_pressed, lv_color_hex(0x2980b9));

lv_obj_add_style(btn, &style_btn_pressed, LV_STATE_PRESSED);  // 按下状态
状态 宏定义 说明
默认 LV_STATE_DEFAULT (0) 对象正常显示时的样式
按下 LV_STATE_PRESSED 用户正在按下对象
禁用 LV_STATE_DISABLED 对象不可交互
聚焦 LV_STATE_FOCUSED 对象获得键盘焦点
悬停 LV_STATE_HOVER 鼠标悬停在对象上(仅限鼠标输入设备)

注意:样式对象(lv_style_t)需要是全局变量或者静态变量,不能是局部变量。因为 LVGL 只是保存了样式的指针,不会复制样式数据。如果局部变量被销毁,样式就失效了,程序会崩溃。

我曾经犯过这个错,在函数里定义了一个局部样式变量,函数返回后样式指针变成了野指针,界面直接花屏。排查了好久才找到原因。

4.6 综合示例:一个带样式的按钮组

说了这么多,我们来写一个完整的例子。创建一个容器,里面放三个按钮,每个按钮都有不同的样式状态。

// 创建容器
lv_obj_t * cont = lv_obj_create(lv_scr_act());
lv_obj_set_size(cont, 300, 200);
lv_obj_align(cont, LV_ALIGN_CENTER, 0, 0);

// 创建按钮通用样式
static lv_style_t style_btn_base;
lv_style_init(&style_btn_base);
lv_style_set_bg_color(&style_btn_base, lv_color_hex(0x3498db));
lv_style_set_border_width(&style_btn_base, 2);
lv_style_set_border_color(&style_btn_base, lv_color_hex(0x2980b9));
lv_style_set_radius(&style_btn_base, 6);
lv_style_set_text_color(&style_btn_base, lv_color_hex(0xffffff));

// 创建按钮按下样式
static lv_style_t style_btn_pressed;
lv_style_init(&style_btn_pressed);
lv_style_set_bg_color(&style_btn_pressed, lv_color_hex(0x2980b9));
lv_style_set_border_color(&style_btn_pressed, lv_color_hex(0x1f6fa5));

// 创建三个按钮
for (int i = 0; i < 3; i++) {
    lv_obj_t * btn = lv_btn_create(cont);
    lv_obj_set_size(btn, 80, 40);
    lv_obj_align(btn, LV_ALIGN_TOP_LEFT, 10, 10 + i * 50);
    
    // 应用样式
    lv_obj_add_style(btn, &style_btn_base, 0);
    lv_obj_add_style(btn, &style_btn_pressed, LV_STATE_PRESSED);
    
    // 添加标签
    lv_obj_t * label = lv_label_create(btn);
    lv_label_set_text_fmt(label, "Btn %d", i + 1);
    lv_obj_center(label);
}

这个例子虽然简单,但包含了对象树、父子关系、坐标定位、样式应用这几个核心概念。你可以在自己的开发板上跑一下,看看效果。

我的习惯:我会把样式定义放在一个单独的头文件里,比如 ui_styles.h。这样多个界面文件可以共用一套样式,改起来也方便。项目大了之后,你会发现统一管理样式能省很多事。

好了,LVGL 的对象系统就讲到这里。理解了这个基础,后面学控件、学布局、学动画就会轻松很多。记住一句话:对象树是骨架,样式是皮肉,事件是灵魂。三者结合,才能做出好用的嵌入式 GUI。