4、LVGL对象系统:对象树结构、父子关系、坐标系统、事件传递机制、样式系统基础
LVGL 的对象系统,说白了就是整个 GUI 的骨架。你想想看,屏幕上那么多按钮、标签、滑块,它们怎么组织?怎么知道谁在谁上面?点击事件怎么传递?样式怎么继承?
这些问题的答案,都藏在 LVGL 的对象系统里。我刚开始用 LVGL 的时候,觉得不就是画几个控件嘛,直接往上堆就完了。结果项目一复杂,界面一多,各种问题就冒出来了——控件重叠不知道谁盖谁,事件响应乱套,样式改一个全变了……
嗯,后来我才明白,理解对象系统,是做好嵌入式 GUI 的第一步。
4.1 对象树结构:一切皆对象
LVGL 里,每个可见元素都是一个对象(lv_obj_t)。按钮是对象,标签是对象,滑块也是对象。它们按照树形结构组织在一起。
这棵树有一个根节点,叫 lv_scr_act(),也就是当前活动屏幕。所有对象都是这个根节点的子孙。
核心概念:LVGL 的对象树是一棵多叉树。每个节点可以有任意多个子节点,但只有一个父节点。根节点是屏幕对象。
我画了一张图,帮你直观理解这个结构:
看到这张图了吗?屏幕对象在最上面,下面挂着容器、按钮、标签。容器下面又可以挂滑块、开关。这就是典型的树形结构。
我的经验:我习惯把界面按功能模块分组。比如一个设置页面,我会先创建一个容器作为"设置组",然后把所有设置项挂在这个容器下面。这样管理起来特别清晰,删除或隐藏整个组也只需要操作父对象。
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 的事件传递分三步:
- 命中检测:从最上层的对象开始,检查点击点是否在对象区域内。如果在,就选中这个对象。
- 事件发送:向选中的对象发送事件(如 LV_EVENT_CLICKED)。
- 事件冒泡:如果对象没有处理这个事件,或者处理完后允许继续传递,事件会向上冒泡到父对象。
关键点:事件冒泡默认是关闭的。你需要调用 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。