LVGL 事件系统实战:点击事件、多事件处理与事件冒泡(X-TRACK 仓库 event 示例全解析)
智能硬件嵌入式硬件开发【免费下载链接】X-TRACKA GPS bicycle speedometer that supports offline maps and track recording项目地址https://gitcode.com/gh_mirrors/xt/X-TRACK点击查看免费下载事件Event是 LVGL 交互逻辑的核心机制无论按钮点击、长按、滚轮、触摸拖动还是绘制回调都以对象 事件码 回调函数的形式分发。本文以 X-TRACK 仓库中 lvgl/examples/event 目录 的三个官方示例为主线逐段剖析事件注册、多事件分发、事件冒泡的完整用法并结合 lv_event.h 的核心定义与 X-TRACK 自身页面代码如 PM_Drag.cpp说明真实工程中的落地姿势。读完本文你可以独立为任意 LVGL 对象注册事件回调、用 switch 分发多种事件、利用冒泡机制在父容器上统一处理子控件交互并学会在嵌入式模拟器环境中验证这些行为。一、LVGL 事件模型三要素与一条主线在深入示例之前先建立最小概念模型。LVGL 的事件机制由三个核心要素组成事件对象lv_event_t承载一次事件的全部信息。其关键字段在 lv_event.h 中定义包括target事件最初触发的对象原始目标current_target当前正在处理该事件的对象冒泡时二者不同code事件码即lv_event_code_tuser_data注册回调时传入的用户数据指针param附加参数例如绘制类事件的绘制上下文stop_processing/stop_bubbling控制事件是否继续被处理或继续向上冒泡。事件码枚举lv_event_code_t从LV_EVENT_ALL到LV_EVENT_LAYOUT_CHANGED的完整事件清单全部定义在 lv_event.h。注册与发送 API用lv_obj_add_event_cb(obj, cb, filter, user_data)注册回调用lv_event_send(obj, code, param)主动发送事件。回调函数签名统一为void (*lv_event_cb_t)(lv_event_t * e)见 lv_event.h也就是说不管处理什么事件你写的都是同一个形式的回调区别只在于注册时的 filter 参数和回调内对code的解读。这个统一回调 过滤码的设计正是下面三个示例展开的基础。二、示例一给按钮添加点击事件lv_example_event_1这是事件系统最基础的用法给按钮注册一个LV_EVENT_CLICKED回调每次点击按钮回调中把按钮上数字标签的计数加一。2.1 C 语言实现完整源码位于 lv_example_event_1.c核心逻辑如下static void event_cb(lv_event_t * e) { LV_LOG_USER(Clicked); static uint32_t cnt 1; lv_obj_t * btn lv_event_get_target(e); lv_obj_t * label lv_obj_get_child(btn, 0); lv_label_set_text_fmt(label, %LV_PRIu32, cnt); cnt; } void lv_example_event_1(void) { lv_obj_t * btn lv_btn_create(lv_scr_act()); lv_obj_set_size(btn, 100, 50); lv_obj_center(btn); lv_obj_add_event_cb(btn, event_cb, LV_EVENT_CLICKED, NULL); lv_obj_t * label lv_label_create(btn); lv_label_set_text(label, Click me!); lv_obj_center(label); }这段代码透露了三个重要的工程细节回调里如何拿到触发事件的对象通过lv_event_get_target(e)返回lv_event_t中的target字段。示例中没有硬编码按钮指针而是从事件对象反查这使得同一个回调可以复用在多个按钮上。如何定位按钮上的子控件lv_obj_get_child(btn, 0)取第一个子对象——因为 label 是唯一子控件。lv_label_set_text_fmt支持格式化输出这里用%LV_PRIu32打印无符号 32 位计数LV_PRIu32是 LVGL 提供的跨平台 printf 格式宏。静态变量做计数static uint32_t cnt 1;使计数在多次回调调用间保持这是嵌入式 GUI 中跨事件保持状态的常见手法。2.2 Python 绑定版本lvgl 官方还提供 MicroPython 绑定示例 lv_example_event_1.pyAPI 与 C 版本一一对应class Event_1(): def __init__(self): self.cnt 1 btn lv.btn(lv.scr_act()) btn.set_size(100, 50) btn.center() btn.add_event_cb(self.event_cb, lv.EVENT.CLICKED, None) label lv.label(btn) label.set_text(Click me!) label.center() def event_cb(self, e): print(Clicked) btn e.get_target() label btn.get_child(0) label.set_text(str(self.cnt)) self.cnt 1 evt1 Event_1()注意 Python 版本把计数器cnt放进了对象实例self.cnt行为上与 C 版静态变量等价同时避免了 C 版中多个按钮共享同一静态计数器的副作用。2.3 条件编译说明示例文件首行#include ../lv_examples.h即 examples/lv_examples.h整体被#if LV_BUILD_EXAMPLES LV_USE_SWITCH包裹只有开启了示例构建宏LV_BUILD_EXAMPLES且启用了 Switch 控件LV_USE_SWITCH时该示例才会参与编译。这意味着示例并不依赖特殊组件按钮 标签是最轻量的演示组合。三、示例二用 switch 处理多个事件lv_example_event_2单一事件回调只能覆盖一种交互实际界面往往需要同时响应按下、点击、长按、长按重复等多种状态。示例二展示了在同一个回调内用switch按事件码分发的标准写法。3.1 C 语言实现完整源码见 lv_example_event_2.cstatic void event_cb(lv_event_t * e) { lv_event_code_t code lv_event_get_code(e); lv_obj_t * label lv_event_get_user_data(e); switch(code) { case LV_EVENT_PRESSED: lv_label_set_text(label, The last button event:\nLV_EVENT_PRESSED); break; case LV_EVENT_CLICKED: lv_label_set_text(label, The last button event:\nLV_EVENT_CLICKED); break; case LV_EVENT_LONG_PRESSED: lv_label_set_text(label, The last button event:\nLV_EVENT_LONG_PRESSED); break; case LV_EVENT_LONG_PRESSED_REPEAT: lv_label_set_text(label, The last button event:\nLV_EVENT_LONG_PRESSED_REPEAT); break; default: break; } } void lv_example_event_2(void) { lv_obj_t * btn lv_btn_create(lv_scr_act()); lv_obj_set_size(btn, 100, 50); lv_obj_center(btn); lv_obj_t * btn_label lv_label_create(btn); lv_label_set_text(btn_label, Click me!); lv_obj_center(btn_label); lv_obj_t * info_label lv_label_create(lv_scr_act()); lv_label_set_text(info_label, The last button event:\nNone); lv_obj_add_event_cb(btn, event_cb, LV_EVENT_ALL, info_label); }本示例有三个值得注意的工程要点LV_EVENT_ALL过滤码注册时第三个参数不再是具体事件码而是LV_EVENT_ALL其枚举值为 0见 lv_event.h。这表示该回调接收发往该对象的全部事件然后由回调内部自行决定响应哪些。好处是按钮的按下、抬起、长按、重复长按等全部状态变化只进一个函数状态机逻辑集中且便于维护。user_data传递上下文lv_obj_add_event_cb(btn, event_cb, LV_EVENT_ALL, info_label)的第四个参数把屏幕上的info_label指针作为用户数据传入。回调内用lv_event_get_user_data(e)取回从而避免了在回调里再依赖外部全局变量。这是 LVGL 推荐的把上下文打包进事件的解耦手法。用switch处理事件码lv_event_get_code(e)返回lv_event_code_t枚举天然适合switch分发。注意default分支留空——因为按钮还会产生PRESSING、RELEASED、SHORT_CLICKED等其他事件本例只关心其中四种。3.2 Python 绑定版本lv_example_event_2.py 用if/elif完成同样的分发并通过 lambda 闭包把info_label绑定进回调def event_cb(e, label): code e.get_code() if code lv.EVENT.PRESSED: label.set_text(The last button event:\nLV_EVENT_PRESSED) elif code lv.EVENT.CLICKED: label.set_text(The last button event:\nLV_EVENT_CLICKED) elif code lv.EVENT.LONG_PRESSED: label.set_text(The last button event:\nLV_EVENT_LONG_PRESSED) elif code lv.EVENT.LONG_PRESSED_REPEAT: label.set_text(The last button event:\nLV_EVENT.LONG_PRESSED_REPEAT) btn lv.btn(lv.scr_act()) btn.set_size(100, 50) btn.center() btn_label lv.label(btn) btn_label.set_text(Click me!) btn_label.center() info_label lv.label(lv.scr_act()) info_label.set_text(The last button event:\nNone) btn.add_event_cb(lambda e: event_cb(e, info_label), lv.EVENT.ALL, None)3.3 相关事件码语义速查回调中出现的四种事件码在 lv_event.h 中的官方注释如下事件码触发时机LV_EVENT_PRESSED对象被按下LV_EVENT_LONG_PRESSED按下时间达到long_press_time且未发生滚动LV_EVENT_LONG_PRESSED_REPEAT长按超过long_press_time后每隔long_press_repeat_time毫秒重复触发LV_EVENT_CLICKED抬起时触发只要未发生滚动与是否长按无关同一交互序列中这几个事件会先后到达按下 →长按时间到→ 长按 → 长按重复周期触发→ 抬起时若未滚动 → 点击。理解这个顺序就能利用长按 点击组合实现旋钮/按钮的双重操作语义。四、示例三事件冒泡lv_example_event_3事件冒泡是 LVGL 组织复杂界面交互的高效手段父容器统一注册回调子控件把事件冒泡给父容器由父容器集中处理。示例三用 30 个按钮组成的 flex 容器演示了这一机制。4.1 C 语言实现完整源码见 lv_example_event_3.cstatic void event_cb(lv_event_t * e) { /*The original target of the event. Can be the buttons or the container*/ lv_obj_t * target lv_event_get_target(e); /*The current target is always the container as the event is added to it*/ lv_obj_t * cont lv_event_get_current_target(e); /*If container was clicked do nothing*/ if(target cont) return; /*Make the clicked buttons red*/ lv_obj_set_style_bg_color(target, lv_palette_main(LV_PALETTE_RED), 0); } void lv_example_event_3(void) { lv_obj_t * cont lv_obj_create(lv_scr_act()); lv_obj_set_size(cont, 290, 200); lv_obj_center(cont); lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP); uint32_t i; for(i 0; i 30; i) { lv_obj_t * btn lv_btn_create(cont); lv_obj_set_size(btn, 80, 50); lv_obj_add_flag(btn, LV_OBJ_FLAG_EVENT_BUBBLE); lv_obj_t * label lv_label_create(btn); lv_label_set_text_fmt(label, %LV_PRIu32, i); lv_obj_center(label); } lv_obj_add_event_cb(cont, event_cb, LV_EVENT_CLICKED, NULL); }冒泡机制的三个关键点开启冒泡开关子按钮必须显式添加LV_OBJ_FLAG_EVENT_BUBBLE标志lv_obj_add_flag(btn, LV_OBJ_FLAG_EVENT_BUBBLE)其事件才会在自身处理完后继续向父级传播。这是 LVGL 与 Web DOM 事件模型的重要差异——冒泡不是默认行为需要逐对象开启。target与current_target的区分回调挂在容器cont上因此lv_event_get_current_target(e)恒等于容器而lv_event_get_target(e)返回真正被点击的按钮也可能是容器自身如果点到容器空白处。代码用if(target cont) return;排除点到容器本身的情况只对被点击的按钮染红。父容器统一处理30 个按钮都不注册自己的回调只由容器注册一个回调。新增按钮无需再绑定事件逻辑天然集中——这正是冒泡对批量交互控件的价值。4.2 Python 绑定版本lv_example_event_3.py 的实现思路一致通过类型判断过滤容器点击def event_cb(e): target e.get_target() if type(target) ! type(lv.btn()): return target.set_style_bg_color(lv.palette_main(lv.PALETTE.RED), 0) cont lv.obj(lv.scr_act()) cont.set_size(320, 200) cont.center() cont.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP) for i in range(30): btn lv.btn(cont) btn.set_size(80, 50) btn.add_flag(lv.obj.FLAG.EVENT_BUBBLE) label lv.label(btn) label.set_text(str(i)) label.center() cont.add_event_cb(event_cb, lv.EVENT.CLICKED, None)Python 版用type(target) ! type(lv.btn())判断目标是否为按钮与 C 版比较 target 与 current_target的过滤策略互为参照前者按控件类型过滤后者按事件来源对象过滤实际工程中可自由组合。4.3 冒泡与特殊事件值得注意的是并非所有事件都依赖LV_OBJ_FLAG_EVENT_BUBBLE才传播。lv_event.h 中注释明确指出LV_EVENT_CHILD_CREATED与LV_EVENT_CHILD_DELETED总是会冒泡到所有父级——这两类事件用于父容器感知子节点增删与普通输入事件传播规则不同。在阅读事件源码如 lv_event.c时需留意这类特例。五、绘制类事件lv_example_event_4 的延伸index.rst之外examples/event 目录还包含一个官方未在索引中单独列出的第四示例 lv_example_event_4.c其函数原型同样声明在 lv_example_event.h。它演示了用LV_EVENT_DRAW_MAIN在绘制阶段介入渲染回调通过lv_event_get_draw_ctx(e)拿到绘制上下文配合lv_draw_polygon实时绘制一个边数不断变化的多边形并用lv_timer_create(timer_cb, 17, cont)以约 17ms≈60FPS周期驱动边数变化与lv_obj_invalidate重绘。这段代码展示了事件系统的另一大用途——对象级自定义绘制当 LVGL 自带控件无法满足视觉需求时可挂载LV_EVENT_DRAW_MAIN/LV_EVENT_DRAW_PART_BEGIN/LV_EVENT_DRAW_PART_END等绘制事件事件码定义见 lv_event.h用lv_draw_*API 绘制自定义形状、动画或仪表盘刻度这在 X-Track 这类嵌入式仪表项目中尤其常见。六、事件 API 速查综合三个示例可归纳出事件编程的五个高频 APIAPI作用示例中的用法lv_obj_add_event_cb(obj, cb, filter, user_data)为对象注册回调filter决定接收哪些事件注册LV_EVENT_CLICKED或LV_EVENT_ALLlv_event_get_code(e)获取当前事件码示例二 switch 分发lv_event_get_target(e)获取原始触发对象示例一改按钮文本、示例三染红按钮lv_event_get_current_target(e)获取当前处理回调所在的对象示例三判断是否点到容器lv_event_get_user_data(e)获取注册时传入的用户数据示例二更新 info_label此外lv_event_get_draw_ctx(e)示例四与lv_event_send(obj, code, param)主动发送事件分别服务于绘制回调和程序化触发场景。完整的回调函数签名、lv_event_t结构体字段与全部事件码均以 lv_event.h 为准。七、在 X-TRACK 工程中的实际应用事件机制不是孤立的教学概念——X-TRACK 的模拟器与嵌入式固件代码中处处可见其身影。最典型的案例是页面管理器 PM_Drag.cppRootEnableDrag用LV_EVENT_ALL挂载统一回调PM_Drag.cppvoid PageManager::RootEnableDrag(lv_obj_t* root) { PageBase* base (PageBase*)lv_obj_get_user_data(root); lv_obj_add_event_cb( root, onRootDragEvent, LV_EVENT_ALL, base ); PM_LOG_INFO(Page(%s) Root drag enabled, base-_Name); }这与示例二一个回调 LV_EVENT_ALL的思路完全一致onRootDragEvent内部按LV_EVENT_PRESSED/LV_EVENT_PRESSING/LV_EVENT_RELEASED分段处理PM_Drag.cpp实现页面的触摸拖动切换。lv_event_send主动发送事件PM_Drag.cpp拖拽结束的异步回调里通过lv_event_send(base-_root, LV_EVENT_LEAVE, base)主动向页面根对象派发LV_EVENT_LEAVE通知页面执行退出逻辑。这展示了事件机制的另一半——不仅是接收还能主动广播。各页面大量使用事件回调搜索lv_obj_add_event_cb/LV_EVENT_*可以发现Dialplate.cpp、LiveMap.cpp、SystemInfos.cpp、_Template/Template.cpp 等页面文件均依赖事件机制完成按键、触摸与状态刷新。可以说理解事件系统是读懂 X-Track 整个 UI 层的必要条件。八、如何运行与验证这些示例X-Track 仓库提供了多套可运行 LVGL 示例的运行环境Linux 模拟器Software/X-Track/Linux/main.cpp与LinuxSDL2Software/X-Track/LinuxSDL2/main.cpp轻量级桌面验证环境修改其main调用lv_example_event_1()等函数即可观察效果。Windows 模拟器Simulator/LVGL.Simulator基于 Visual Studio 的完整模拟工程内置 HAL 与 LVGL 源码。嵌入式工程MDK-ARM_F435 / MDK-ARM_F403AKeil 工程示例代码同样可移植运行。运行前置条件在lv_conf.h中开启示例宏LV_BUILD_EXAMPLES并确保对应组件开关已打开——lv_example_event_1/2需要LV_USE_SWITCHlv_example_event_3需要LV_USE_FLEX两个文件头部的条件编译已给出明确依赖。宏定义模板可参考 lvgl 自带的 lv_conf_template.h。桌面模拟器下操作方式与真机一致鼠标点击即触发LV_EVENT_PRESSED/LV_EVENT_CLICKED按住不动超过long_press_time即触发长按系列事件。九、小结从三个官方示例到 X-Track 的真实页面代码LVGL 事件系统的使用模式可以浓缩为四句话统一回调所有事件都进入void (*cb)(lv_event_t *)用lv_event_get_code区分身份按需过滤lv_obj_add_event_cb的 filter 参数用LV_EVENT_ALL收全量、用具体事件码收定向事件冒泡集中子控件加LV_OBJ_FLAG_EVENT_BUBBLE父容器统一处理用target与current_target区分来源user_data 传参把需要访问的上下文标签、页面对象等通过回调注册参数传入避免全局变量。掌握这套模型后无论是按钮计数、状态机式的多事件响应还是 30 个按钮的批量交互都能以清晰的模式落地而 lv_event.h 中完整的事件枚举与 lv_event.c 的实现则是你进一步钻研事件传播细节的最佳起点。赞分享智能硬件嵌入式硬件开发【免费下载链接】X-TRACKA GPS bicycle speedometer that supports offline maps and track recording项目地址https://gitcode.com/gh_mirrors/xt/X-TRACK点击查看免费下载相关推荐X-TRACK 中的 LVGL 事件机制全解析从事件码到事件冒泡的完整指南X TRACK 中的 LVGL 事件机制全解析从事件码到事件冒泡的完整指南 本文以 LVGL 官方文档《Events》为基础结合 X TRACK 开源 GP智能硬件嵌入式硬件开发Cytoscape.js 事件系统完全指南事件对象、事件冒泡与全量事件类型详解Cytoscape.js 事件系统完全指南事件对象、事件冒泡与全量事件类型详解 Cytoscape.js 是用于图可视化与图分析的 JavaScript 库数据可视化JavaScript 事件委托详解基于事件冒泡的事件处理优化与实战JavaScript 事件委托详解基于事件冒泡的事件处理优化与实战 事件委托Event Delegation是 JavaScript 中最重要的事件处理技文档教程前端上一篇Fleet 匿名使用统计扩展补丁策略与软件自动化覆盖度指标解析下一篇APITable 模板中心专辑 API 全解析getAlbumContent 与 getRecommendedAlbums 的调用与源码实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考