4、LVGL基础:LVGL架构、对象树、事件驱动模型
好,咱们开始聊LVGL。说实话,我最早接触LVGL是在一个便携式呼吸机的项目上。那时候屏幕小,资源紧,选来选去还是LVGL最靠谱。今天这一章,我带你把它最核心的三个概念捋清楚:架构、对象树、事件驱动。搞懂这些,后面写界面就像搭积木一样简单。
4.1 LVGL的整体架构
LVGL的架构,说白了就是分层。你想想看,一个GUI库要跑在嵌入式设备上,它得把硬件和软件隔开。LVGL就是这么干的。
从上到下,大致分这么几层:
- 应用层:你写的业务代码,比如呼吸机的参数设置、报警逻辑。
- LVGL核心层:对象管理、事件分发、样式计算。这是心脏。
- 硬件抽象层(HAL):负责跟显示屏、触摸屏、定时器打交道。
我个人习惯把HAL层比作「翻译官」。你的屏幕是SPI接口还是RGB接口?触摸是I2C还是ADC?这些差异,HAL层全给你挡掉了。你只需要实现几个简单的回调函数,比如disp_flush()和touch_read()。
核心要点:LVGL不直接操作硬件寄存器。它通过HAL层跟你板子的驱动交互。所以换屏幕?换主控?只要HAL接口不变,你的UI代码几乎不用动。
嗯,这里要注意一点:HAL层的初始化顺序有讲究。我遇到过有人先初始化了LVGL,再去初始化屏幕驱动,结果屏幕死活不亮。正确的顺序是:先初始化硬件驱动(屏幕、触摸),再调用lv_init(),最后配置HAL。
4.2 对象树:一切UI都是树
LVGL里所有的界面元素,都是对象。按钮是对象,标签是对象,滑块也是对象。这些对象不是孤立的,它们组织成一棵树。
为什么是树?因为GUI天然有层级关系。你想想呼吸机的主界面:
- 一个屏幕(根对象)
- 下面挂着几个容器(比如参数区、波形区、报警区)
- 每个容器里再挂按钮、标签、进度条
这棵树长这样:
lv_scr_act() // 当前活动屏幕
├── 容器1 (参数面板)
│ ├── 标签 (显示"潮气量")
│ ├── 标签 (显示数值)
│ └── 按钮 (调整参数)
├── 容器2 (波形显示)
│ └── 画布 (绘制波形)
└── 容器3 (报警区域)
└── 标签 (显示报警信息)
创建对象的API很简单:
lv_obj_t * btn = lv_btn_create(lv_scr_act()); // 在屏幕上创建按钮
lv_obj_t * label = lv_label_create(btn); // 在按钮上创建标签
第二个参数是父对象。父对象移动,子对象跟着动。父对象隐藏,子对象也看不见。这就是继承关系。
我的经验:别把所有对象都直接挂在屏幕下。那样管理起来会疯掉。我习惯先创建几个容器(panel),把功能分区。比如呼吸机,我会建一个「参数区容器」、「波形区容器」、「报警区容器」。这样后面要整体隐藏或移动某个区域,只需要操作容器对象。
还有一个坑:删除对象时,子对象会自动被删除。但如果你用指针保存了子对象,删除父对象后那个指针就变成野指针了。我曾经因为这个查了两天bug……嗯,后来我养成了一个习惯:删除父对象后,把所有子对象指针置NULL。
4.3 事件驱动模型:点一下,发生了什么?
GUI的本质是交互。用户点一下按钮,屏幕要亮。用户旋转编码器,参数要变。这一切,都是事件驱动的。
LVGL的事件模型,我总结为三步:
- 事件源:谁产生了事件?比如按钮被点击、滑块被拖动。
- 事件类型:发生了什么?
LV_EVENT_CLICKED、LV_EVENT_VALUE_CHANGED。 - 事件处理函数:你写的回调函数,响应这个事件。
注册事件处理函数的代码长这样:
lv_obj_add_event_cb(btn, my_button_callback, LV_EVENT_CLICKED, NULL);
void my_button_callback(lv_event_t * e) {
lv_obj_t * btn = lv_event_get_target(e);
LV_LOG_USER("按钮被点击了!");
// 在这里写你的业务逻辑,比如切换呼吸模式
}
这里有个细节:lv_event_get_target(e)拿到的是触发事件的对象。如果你在多个按钮上注册了同一个回调,可以用这个函数区分是哪个按钮被点了。
注意:事件回调里不要做耗时操作!比如读写SD卡、网络通信。LVGL的事件循环是单线程的,你卡住了,整个UI就卡死了。我见过有人直接在回调里写delay(1000),结果界面像幻灯片一样。正确的做法是:在回调里设置一个标志位,在主循环里处理耗时任务。
事件类型有很多,常用的我列一下:
| 事件类型 | 触发时机 | 典型用途 |
|---|---|---|
LV_EVENT_CLICKED |
点击后释放 | 按钮确认、模式切换 |
LV_EVENT_VALUE_CHANGED |
值发生变化 | 滑块拖动、开关切换 |
LV_EVENT_PRESSING |
正在按下 | 长按连续调节 |
LV_EVENT_RELEASED |
手指抬起 | 确认操作完成 |
你可能会问:如果多个对象需要响应同一个事件怎么办?比如呼吸机上的「静音报警」按钮,点击后要停止蜂鸣器,还要在屏幕上显示「已静音」。两种做法:
- 在同一个回调里写两件事
- 或者给同一个对象注册两个回调
我个人推荐第二种。把不同职责分开,代码更干净。LVGL允许一个对象注册多个回调,它们会按注册顺序依次执行。
4.4 事件传递:冒泡还是捕获?
LVGL的事件传递机制,跟浏览器有点像。事件会从父对象传到子对象,或者反过来。默认是「冒泡」——事件从子对象开始,一路向上传到父对象。
举个例子:你点击了容器里的一个按钮。事件传递顺序是:
- 按钮本身
- 按钮的父对象(容器)
- 容器的父对象(屏幕)
如果你想在容器层统一处理所有子按钮的点击事件,可以给容器注册LV_EVENT_CLICKED回调。这样就不用给每个按钮单独注册了。
实用技巧:在呼吸机的报警处理中,我经常用事件冒泡。所有报警相关的控件都放在一个容器里,容器统一处理报警逻辑。这样新增一个报警控件,不需要改任何代码。
如果你想阻止事件继续冒泡,可以调用lv_event_stop_bubbling(e)。不过说实话,我很少用这个。除非你明确知道自己在做什么,否则别轻易打断事件流。
4.5 小结
这一章的内容,说白了就三句话:
- LVGL通过HAL层隔离硬件,让你专心写UI。
- 所有界面元素组成一棵对象树,父管子,子随父。
- 用户交互通过事件驱动,回调函数里别干重活。
下一章,咱们开始动手画呼吸机的第一个界面。到时候你会看到,对象树和事件模型是怎么在实际项目中跑起来的。
课后动手:打开你的开发板,创建一个屏幕,上面放三个按钮。给每个按钮注册不同的事件回调。然后试试给屏幕(父对象)注册一个事件,看看点击按钮时,父对象的回调会不会被触发。这就是冒泡。