4、LVGL基础框架:对象(lv_obj)概念、屏幕(lv_scr)管理、坐标系统、事件驱动机制

好,咱们进入正题。这一章是LVGL的骨架,说白了就是搞懂它怎么组织界面、怎么摆放控件、怎么响应你的操作。我刚开始接触LVGL时,觉得它跟其他GUI库不太一样,但摸透了这几个核心概念后,写起来就顺手多了。

4.1 对象(lv_obj)—— 一切控件的祖宗

在LVGL里,所有你能看到的东西,都是对象。按钮是对象,标签是对象,滑块也是对象。它们都继承自一个基类——lv_obj_t

你可以把 lv_obj_t 想象成一个空白的画布。它本身不显示任何东西,但定义了所有控件共有的属性:位置、大小、父对象、样式、事件回调等等。

核心要点: 创建任何控件,本质上都是先创建一个 lv_obj_t,然后给它“穿上”特定控件类型的外衣。

举个例子,创建一个按钮:

lv_obj_t * btn = lv_btn_create(lv_scr_act());  // 在活动屏幕上创建按钮
lv_obj_set_size(btn, 100, 50);                  // 设置大小
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);       // 居中显示

你看,lv_btn_create 内部其实调用了 lv_obj_create,然后初始化了按钮特有的行为。我个人习惯把 lv_obj_t 叫做“对象句柄”,你只要拿着这个句柄,就能操控这个控件的一切。

小技巧: 调试时,可以用 lv_obj_dump_tree(lv_scr_act()) 打印出当前屏幕的对象树,看看父子关系对不对。我在排查界面重叠问题时,这招帮了大忙。

4.2 屏幕(lv_scr)管理—— 多页面切换的秘密

屏幕是LVGL里最顶层的对象。每个屏幕都是一个 lv_obj_t,但它是特殊的——它没有父对象。

系统默认有一个基础屏幕,叫 lv_scr_act()。你创建的所有控件,默认都挂在这个屏幕上。但实际项目中,一个界面肯定不够用。比如充电桩,要有主界面、充电界面、设置界面。

这时候就需要创建多个屏幕:

lv_obj_t * scr_main = lv_obj_create(NULL);  // 创建主屏幕,父对象为NULL
lv_obj_t * scr_charge = lv_obj_create(NULL); // 创建充电屏幕
lv_obj_t * scr_setting = lv_obj_create(NULL); // 创建设置屏幕

切换屏幕用这个函数:

lv_scr_load(scr_charge);  // 切换到充电屏幕

嗯,这里要注意:切换屏幕时,旧屏幕上的对象并不会被销毁,只是被隐藏了。如果你希望切换后释放内存,需要手动删除旧屏幕上的对象,或者用 lv_scr_load_anim() 配合动画切换。

我曾经踩过的坑: 在切换屏幕时,忘记处理旧屏幕上的定时器。结果切换到新屏幕后,旧屏幕的定时器还在跑,导致资源泄漏。建议在切换前,统一停止或删除所有与旧屏幕相关的定时器。

4.3 坐标系统—— 控件到底摆在哪?

LVGL的坐标系统,说白了就是两个东西:父对象坐标对齐方式

每个对象的位置,是相对于它的父对象来计算的。父对象左上角是 (0,0),子对象的 (x,y) 就是相对于这个点的偏移。

但直接设 x,y 太死板了。LVGL提供了强大的对齐机制:

对齐方式 说明
LV_ALIGN_TOP_LEFT 左上角对齐
LV_ALIGN_CENTER 中心对齐
LV_ALIGN_BOTTOM_MID 底部中间对齐
LV_ALIGN_OUT_RIGHT_TOP 父对象右侧外部,顶部对齐

你想想看,如果手动计算每个控件的位置,屏幕分辨率一变,所有坐标都得重算。用对齐方式,LVGL会自动帮你算。

// 让按钮在屏幕正中央,偏移 (0, -20) 像素
lv_obj_align(btn, LV_ALIGN_CENTER, 0, -20);

还有更高级的 lv_obj_set_align(),它会持续保持对齐,即使父对象大小变了。我在做充电桩的充电进度条时,就用这个让进度条始终居中,不管屏幕怎么旋转。

个人经验: 尽量用对齐,少用绝对坐标。除非你确定屏幕尺寸永远不会变。否则,后期维护时你会想哭的。

4.4 事件驱动机制—— 让界面活起来

界面不能光看,还得能交互。LVGL用事件驱动来处理用户操作。比如点击按钮、滑动滑块、触摸屏幕。

事件的核心是 回调函数。你给对象注册一个事件回调,当特定事件发生时,LVGL就会调用这个函数。

static void btn_event_cb(lv_event_t * e) {
    lv_event_code_t code = lv_event_get_code(e);
    if(code == LV_EVENT_CLICKED) {
        printf("按钮被点击了!\n");
    }
}

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

常用的事件类型:

  • LV_EVENT_CLICKED:点击完成(按下+抬起)
  • LV_EVENT_PRESSING:正在按下
  • LV_EVENT_VALUE_CHANGED:值改变(比如滑块拖动)
  • LV_EVENT_RELEASED:手指抬起

事件回调里,你可以通过 lv_event_get_target(e) 获取触发事件的对象,通过 lv_event_get_user_data(e) 获取注册时传入的用户数据。

避坑指南: 我曾经在事件回调里直接调用了 lv_scr_load() 切换屏幕,结果导致事件系统混乱,出现段错误。后来发现,在事件回调中尽量不要做太复杂的操作,尤其是涉及对象创建或删除的。可以用 lv_async_call() 把操作推迟到下一个事件循环。

事件驱动机制,说白了就是“你不动,我不动;你一动,我响应”。这种模式非常适合嵌入式系统,因为它不会浪费CPU去轮询,只在需要时才执行代码。

4.5 小结

这一章的内容,是LVGL的基石。对象是基础,屏幕是容器,坐标是位置,事件是灵魂。把这四个概念搞明白,后面学控件、学样式、学动画,都会轻松很多。

我个人建议,你可以在开发板上先跑一个最简单的例子:创建一个按钮,点击它切换屏幕。亲手试一遍,比看十遍文档都管用。

下一章,我们会深入控件的样式系统,看看怎么让按钮变得更好看。到时候见。