4. 事件处理实战:四大常用场景
理论讲完了,咱们直接上手。这一章我带你写四个最常用的事件处理场景:按钮点击、滑块拖拽、列表选择、键盘输入。这几个场景覆盖了嵌入式 GUI 里 80% 的交互需求。
我个人习惯是「先跑通,再优化」。所以每个例子我都会给一个完整的、能直接编译运行的代码片段。你复制到你的 LVGL 工程里就能看到效果。
本章核心:掌握 lv_event_code_t 的四种常用类型,学会用回调函数处理用户交互。
4.1 按钮点击事件
按钮点击是最基础的事件。说白了就是用户按下去、抬起来,你响应一下。但这里有个坑——LVGL 的按钮事件默认是 LV_EVENT_CLICKED,不是 LV_EVENT_PRESSED。前者是「按下并抬起」才算一次点击,后者是「刚碰到」就触发。
我的习惯:绝大多数场景用 LV_EVENT_CLICKED。只有做长按检测或触觉反馈时,才用 LV_EVENT_PRESSED。
/* 按钮点击事件示例 */
static void btn_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
lv_obj_t * btn = lv_event_get_target(e);
LV_LOG_USER("按钮被点击了");
/* 修改按钮文字,给用户反馈 */
lv_obj_t * label = lv_obj_get_child(btn, 0);
lv_label_set_text(label, "已点击");
/* 延时恢复文字(实际项目用定时器) */
lv_async_call(NULL, NULL);
}
}
/* 创建按钮并注册事件 */
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 120, 50);
lv_obj_center(btn);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "点击我");
lv_obj_center(label);
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
你看,代码结构很清晰:创建对象 → 添加事件回调 → 在回调里判断事件码。我在项目里见过有人忘了判断事件码,结果一个回调里处理了七八种事件,调试时根本分不清是哪个触发的。
我曾经踩过的坑:在回调里直接调用 lv_obj_del() 删除触发事件的按钮。LVGL 的事件分发机制不允许在回调中删除当前对象,会崩溃。正确的做法是用 lv_async_call() 延迟删除。
4.2 滑块值变化事件
滑块(Slider)的典型场景是音量调节、亮度控制。它的事件码是 LV_EVENT_VALUE_CHANGED,只要滑块值变了就会触发。你想想看,用户拖拽滑块时,这个事件会连续触发很多次。
/* 滑块值变化事件示例 */
static void slider_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_VALUE_CHANGED) {
lv_obj_t * slider = lv_event_get_target(e);
int32_t val = lv_slider_get_value(slider);
/* 更新旁边的数值显示 */
lv_obj_t * label = lv_event_get_user_data(e);
lv_label_set_text_fmt(label, "%d%%", val);
/* 实际项目中这里设置硬件参数 */
// set_volume(val);
LV_LOG_USER("滑块值: %d", val);
}
}
/* 创建滑块和显示标签 */
lv_obj_t * slider = lv_slider_create(lv_scr_act());
lv_obj_set_size(slider, 200, 20);
lv_obj_align(slider, LV_ALIGN_CENTER, 0, -30);
lv_slider_set_range(slider, 0, 100);
lv_slider_set_value(slider, 50, LV_ANIM_OFF);
lv_obj_t * val_label = lv_label_create(lv_scr_act());
lv_label_set_text(val_label, "50%");
lv_obj_align_to(val_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
/* 注册事件,把标签指针作为用户数据传进去 */
lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, val_label);
这里有个技巧:用 lv_event_get_user_data() 传递额外数据。你看我把 val_label 传进去了,回调里直接就能用,不用声明全局变量。我个人习惯把所有相关控件的指针都打包成一个结构体传进去,代码干净很多。
性能提示:如果滑块值变化时需要做重量级操作(比如写 Flash、网络请求),建议加一个防抖机制。比如值变化超过 5 才真正触发操作,或者用定时器延迟 200ms 再执行。
4.3 列表项选择事件
列表(List)在菜单设置、文件选择里很常见。它的选择事件是 LV_EVENT_VALUE_CHANGED,和滑块一样。但列表的事件触发时机是「选中项发生变化时」。
/* 列表项选择事件示例 */
static void list_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_VALUE_CHANGED) {
lv_obj_t * list = lv_event_get_target(e);
/* 获取当前选中的按钮索引 */
uint32_t idx = lv_list_get_selected_btn(list);
/* 获取选中按钮的文字 */
lv_obj_t * btn = lv_list_get_btn(list, idx);
lv_obj_t * label = lv_obj_get_child(btn, 0);
const char * txt = lv_label_get_text(label);
LV_LOG_USER("选中: %s (索引 %d)", txt, idx);
/* 根据选中项执行不同操作 */
if(strcmp(txt, "设置") == 0) {
// open_settings_page();
} else if(strcmp(txt, "关于") == 0) {
// show_about_dialog();
}
}
}
/* 创建列表 */
lv_obj_t * list = lv_list_create(lv_scr_act());
lv_obj_set_size(list, 200, 250);
lv_obj_center(list);
/* 添加列表项 */
lv_list_add_btn(list, LV_SYMBOL_SETTINGS, "设置");
lv_list_add_btn(list, LV_SYMBOL_INFO, "关于");
lv_list_add_btn(list, LV_SYMBOL_WIFI, "网络");
lv_list_add_btn(list, LV_SYMBOL_BLUETOOTH, "蓝牙");
lv_obj_add_event_cb(list, list_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
嗯,这里要注意:列表的 LV_EVENT_VALUE_CHANGED 在初始化时也会触发一次。如果你不想在刚创建列表时就执行操作,可以在回调里加一个标志位判断,或者用 lv_event_get_target() 检查对象是否已经可见。
我曾经遇到的 bug:列表项的文字是动态分配的,我在回调里直接保存了 lv_label_get_text() 返回的指针。结果列表刷新后,那个指针指向的内容变了,程序就乱显示了。正确的做法是 strdup() 复制一份,或者用索引来标识。
4.4 键盘输入事件
键盘输入在 LVGL 里稍微特殊一点。它有两种场景:一种是实体物理按键(比如开发板上的矩阵键盘),另一种是屏幕上的虚拟键盘。这里我讲实体按键的输入事件,虚拟键盘后面章节会专门讲。
实体按键的事件码是 LV_EVENT_KEY,它会携带一个 uint32_t 的键值。LVGL 定义了一套标准键值,比如 LV_KEY_UP、LV_KEY_DOWN、LV_KEY_ENTER 等。
/* 键盘输入事件示例 */
static void key_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_KEY) {
uint32_t key = lv_event_get_key(e);
switch(key) {
case LV_KEY_UP:
LV_LOG_USER("上键按下");
// move_focus_prev();
break;
case LV_KEY_DOWN:
LV_LOG_USER("下键按下");
// move_focus_next();
break;
case LV_KEY_ENTER:
LV_LOG_USER("确认键按下");
// activate_focused_item();
break;
case LV_KEY_ESC:
LV_LOG_USER("返回键按下");
// go_back();
break;
default:
LV_LOG_USER("未知按键: %d", key);
break;
}
}
}
/* 将事件注册到组(Group)上 */
lv_group_t * g = lv_group_create();
lv_group_set_default(g);
/* 把需要键盘控制的控件加入组 */
lv_group_add_obj(g, btn1);
lv_group_add_obj(g, btn2);
lv_group_add_obj(g, slider);
/* 在组上注册键盘事件 */
lv_obj_t * dummy = lv_obj_create(lv_scr_act());
lv_obj_add_event_cb(dummy, key_event_cb, LV_EVENT_KEY, NULL);
lv_group_add_obj(g, dummy);
为什么要在 dummy 对象上注册事件?因为 LVGL 的键盘事件是发给当前焦点对象的。如果你直接给按钮注册 LV_EVENT_KEY,只有按钮获得焦点时才能收到按键。而用组(Group)管理焦点,再在组内的一个对象上统一处理按键,逻辑更清晰。
我的经验:嵌入式设备通常只有 4-6 个物理按键。我一般会创建一个专门的「键盘控制器」对象,它不显示任何 UI,只负责接收 LV_EVENT_KEY 并分发到各个模块。这样 UI 和按键逻辑就解耦了。
4.5 四种事件的对比总结
最后我把这四种事件放在一起对比一下,方便你查阅。
| 事件类型 | 事件码 | 触发时机 | 常用 API | 典型场景 |
|---|---|---|---|---|
| 按钮点击 | LV_EVENT_CLICKED | 按下并抬起后 | lv_event_get_target() | 确认、取消、开关 |
| 滑块值变化 | LV_EVENT_VALUE_CHANGED | 值改变时(连续触发) | lv_slider_get_value() | 音量、亮度、进度 |
| 列表选择 | LV_EVENT_VALUE_CHANGED | 选中项变化时 | lv_list_get_selected_btn() | 菜单、文件列表 |
| 键盘输入 | LV_EVENT_KEY | 物理按键按下时 | lv_event_get_key() | 方向键、确认、返回 |
你看,滑块和列表用的是同一个事件码 LV_EVENT_VALUE_CHANGED。这其实是个设计上的复用——「值发生了变化」这个语义对两者都适用。但你在回调里一定要区分目标对象类型,别把滑块的值当成列表索引去用。