嵌入式设备上的 UI 开发,经常会遇到一个让人头疼的问题:页面一多,界面切换逻辑就变得混乱。今天我们用 ESP32 + LVGL,把“多页面切换”这个需求用选项卡视图(Tabview)来彻底解决。文章会从基础概念讲起,结合完整的可运行代码,带你从零实现一个带选项卡切换的嵌入式界面,并解决中文显示、样式定制和常见崩溃问题。
1. 为什么嵌入式 UI 需要选项卡视图
1.1 从“页面堆叠”到“多标签切换”
在单片机屏幕上做交互界面,最普通的方式是一个界面显示一套控件,通过按钮触发lv_scr_load()切换到另一个界面。这种方式在小程序里够用,但一旦功能变多,你会陷入“页面导航地狱”:每个页面都要处理跳转逻辑、返回逻辑、参数传递逻辑,代码越写越散,后期每加一个页面都要动一堆旧代码。
选项卡视图(Tabview)解决的是这个场景下的结构问题。它把多个页面放在同一个容器里,每个页面对应顶部或者底部的一个标签按钮,用户点击标签,内容区就切换到对应的页面。最直观的例子就是手机里的“设置”界面:顶部栏有“通用”“显示”“网络”几个标签,点击任意一个,中间内容区跟着切换。
对嵌入式设备来说,这种交互方式有几个明显的好处:
- 结构清晰:每个 Tab 页面是一个独立对象,代码上天然隔离。
- 无需手动管理页面切换:Tabview 内部自己处理了按钮选中和内容区显示。
- 减少界面跳转深度:用户始终停留在同一个“大的界面”中,点击不同标签获取不同功能,学习成本低。
- 动画体验好:LVGL 默认支持 Tab 切换时的滑动动画,感官上比生硬切屏流畅。
1.2 LVGL 选项卡的核心价值
LVGL(Light and Versatile Graphics Library)是目前嵌入式领域使用最广的开源图形库之一。它不像传统的 LCD 驱动库里那样,你需要自己实现按钮、滑动、触摸命中等逻辑,LVGL 直接提供了一整套可裁剪的控件库,其中lv_tabview就是我们今天的主角。
Tabview 在 LVGL 里的地位,可以理解成一个“复合容器”。它本身不是一个简单的矩形块,而是由几个子对象组合而成:
- 标签按钮条:水平排列的一组按钮。
- 内容容器:放置实际页面内容的对象。
- 每个标签页:一个被按钮控制显示/隐藏的子容器。
所以你在使用 Tabview 时,不需要再单独写“按钮点击后切换页面”的回调逻辑,创建 Tab 和添加页面内容的过程是绑定在一起的:调用lv_tabview_add_tab()添加一个标签时,它会自动创建一个对应页面,你只需要往这个页面里填控件即可。
学习 Tabview 的价值不仅在于这个控件本身。你会接触到 LVGL 的对象树结构、样式系统、事件机制和布局方式,这些知识在后面使用lv_btn、lv_label、lv_list、lv_chart等控件时是通用的。可以说,搞懂了 Tabview,就等于入门了 LVGL 的控件组合与页面管理思路。
2. 环境准备与工程搭建
2.1 硬件与软件清单
本文的代码可以在真实硬件上运行,也可以在 PC 模拟器里直接验证。如果你手上已经有 ESP32 开发板和一块可以驱动的屏幕,建议直接在板子上烧录体验;如果还没有,可以先在电脑上把界面效果跑通,再移植到硬件。
硬件部分:
- ESP32 开发板(ESP32-WROOM-32 或 ESP32-S3 均可)。
- TFT LCD 屏幕,常见的有 ILI9341、ST7789、ST7735 等驱动芯片,分辨率以 240x320 或 320x480 为主。
- 如果屏幕带触摸功能,也可以顺手把触摸事件接上,Tabview 的点击切换会更直观。
软件部分:
- ESP-IDF 开发环境,或者 Arduino ESP32 环境。
- LVGL 源码,推荐使用 v8.3 以上版本。如果你用的是 v9,少数 API 名称有调整,本文主要基于 v8 系列写法,v9 用户对照官方迁移笔记即可。
- 屏幕驱动库,比如
lvgl_esp32_drivers,也可以直接使用你手头屏幕对应的驱动代码。
版本说明:LVGL 是一个非常活跃的开源库,API 在 v8 和 v9 之间有一些不兼容变化。本文以 v8 系列常用 API 为例,重点演示实现思路。你在实际项目中如果用了更新版本,先查阅官方文档确认 API 签名变化。
2.2 获取 LVGL 源码与配置
如果你使用 ESP-IDF,可以在工程目录下通过组件方式引入 LVGL。操作方式和常规组件类似,把 LVGL 源码放到components/目录,或者使用项目自带的组件管理器。
关键配置项在lv_conf.h里。你需要打开以下宏:
/* 文件路径:components/lvgl/lv_conf.h */ #define LV_COLOR_DEPTH 16 #define LV_TICK_CUSTOM 1 #define LV_USE_TABVIEW 1LV_USE_TABVIEW默认就是开启的,但如果你对 LVGL 做过裁剪,需要确认这个宏没有被关掉。LV_TICK_CUSTOM表示使用自定义 tick 源,通常由系统心跳或定时器提供,保证 LVGL 的动画和事件正常调度。
注意:lv_conf.h这个文件是 LVGL 的全局配置头文件,不同移植方式的路径略有不同。如果你的工程里已经有现成的配置,优先基于现有配置修改,不要重复复制一份造成编译冲突。
2.3 在 PC 模拟器中先行验证
如果你不想一开始就折腾硬件驱动,可以先跑 LVGL 的 PC 模拟器。VSCode 搭配lv_sim_vscode_sdl项目模板,可以在 Windows/Linux/macOS 上直接看到渲染效果。
模拟器里验证 UI 的好处是:
- 省去烧录步骤,改完代码立刻看效果。
- 方便调试布局,尤其适合先画界面、后接逻辑的开发模式。
- 可以在没有硬件的情况下提前学习 LVGL 各种控件。
不过模拟器的触摸输入和真机有一点差异,最终还是要到板子上验证触摸坐标和屏幕适配。
3. LVGL Tabview 核心概念拆解
3.1 Tabview 的整体结构
先看一张简化示意图,理解对象层级:
screen(屏幕) └── tabview(整体容器) ├── tab_bar(按钮条,位于顶部或底部) │ ├── tab_btn_1(标签1) │ ├── tab_btn_2(标签2) │ └── tab_btn_3(标签3) └── content(内容容器) ├── page_1(标签1对应的页面) ├── page_2(标签2对应的页面) └── page_3(标签3对应的页面)lv_tabview_create()创建的实际上是一个容器对象,它内部自动划分为两个区域。创建 Tab 时,按钮会自动加到按钮条区域,页面容器会自动加到内容区域。开发者不需要自己去lv_obj_create()后手动做布局。
这里有一个容易混淆的点:lv_tabview_add_tab()返回的对象是内容页容器,不是按钮本身。你往里添加的任何控件,都会显示在这个内容页中。如果你误以为返回的是按钮对象,后续操作就会“看不到控件”或者“控件位置乱跑”。
3.2 按钮区与内容区的联动机制
Tabview 的核心联动逻辑是:
- 点击标签按钮时,按钮进入选中状态(
LV_STATE_CHECKED)。 - LVGL 内部根据按钮索引,计算出需要显示的内容页索引。
- 内容区容器执行滚动或切换动画,把目标页面移动到可见区域。
在 LVGL v8 中,内容区的切换其实是滚动动画实现的。所以如果你在代码里主动修改了内容区的位置,可能会破坏 Tabview 自己的切换逻辑。理解这一点,能帮你避免很多位置异常问题。
lv_tabview_get_tab_btns()返回的是按钮条对象,lv_tabview_get_content()返回的是内容区对象。你可以通过它们去调整按钮条的背景色、内容区的边距等样式。
3.3 常用 API 梳理
以 v8 为例,数一下最常用的 API:
lv_obj_t * lv_tabview_create(lv_obj_t * parent, lv_dir_t tab_pos, lv_coord_t tab_size); lv_obj_t * lv_tabview_add_tab(lv_obj_t * tabview, const char * name); lv_obj_t * lv_tabview_get_tab_btns(lv_obj_t * tabview); lv_obj_t * lv_tabview_get_content(lv_obj_t * tabview); void lv_tabview_set_tab_act(lv_obj_t * tabview, uint32_t id, bool anim_en); uint16_t lv_tabview_get_tab_act(lv_obj_t * tabview);参数解释:
tab_pos:标签条的位置,LV_DIR_TOP表示在顶部,LV_DIR_BOTTOM表示在底部,LV_DIR_LEFT、LV_DIR_RIGHT可以放置在左右两侧。tab_size:标签条的高度或宽度,根据tab_pos的方向决定。name:标签按钮上显示的文字。id:需要切换到第几个标签页,从 0 开始计数。anim_en:是否启用切换动画。
如果你用的是 v9,lv_tabview_create的签名有变化,不再需要传tab_pos和tab_size两个参数。这是因为 v9 把所有布局属性统一到了 style 里。遇到编译报错时,优先检查这一点。
4. 实战:ESP32 选项卡界面完整实现
下面我们真刀真枪写一个完整示例。假设场景是做一个“环境监测终端”,三个 Tag 页分别显示:首页概览、实时数据、系统设置。
4.1 创建项目结构
以 ESP-IDF 工程为例,项目结构如下:
esp32_tabview_demo/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ ├── main.c │ └── ui/ │ ├── ui_tabview.c │ └── ui_tabview.h └── components/ └── lvgl/ └── ...把 LVGL 相关代码放入components目录,业务 UI 代码写在main/ui下,这样看起来清晰,而且方便以后复用。
如果你的环境是 Arduino + PlatformIO,也可以把目录结构调整为src/ui/,原理一样。核心是 UI 代码与主循环分离,不要让界面初始化代码塞满main.c。
4.2 编写 LVGL 配置
在lv_conf.h中确认以下配置开启:
/* 文件路径:components/lvgl/lv_conf.h */ #define LV_COLOR_DEPTH 16 #define LV_TICK_CUSTOM 1 #define LV_USE_TABVIEW 1 #define LV_FONT_MONTSERRAT_14 1LV_FONT_MONTSERRAT_14是 LVGL 内置的英文字体,先开启一个常用字号,保证后面 Label 能正常显示。中文字体后面单独处理。
如果你使用触摸屏,需要打开输入设备驱动相关配置,在lv_drv_conf.h或驱动代码里注册触摸设备。没有触摸的话,可以用开发板上的按键模拟点击,核心思路不变。
4.3 编写 UI 核心代码
先看头文件:
/* 文件路径:main/ui/ui_tabview.h */ #ifndef UI_TABVIEW_H #define UI_TABVIEW_H #include "lvgl.h" void ui_tabview_create(lv_obj_t * parent); #endif然后是实现文件:
/* 文件路径:main/ui/ui_tabview.c */ #include "ui_tabview.h" /* 创建选项卡界面的核心函数 */ void ui_tabview_create(lv_obj_t * parent) { /* 1. 创建 Tabview,标签条放在顶部,高度 50 像素 */ lv_obj_t * tabview = lv_tabview_create(parent, LV_DIR_TOP, 50); /* 让 Tabview 填满整个父容器 */ lv_obj_set_size(tabview, lv_pct(100), lv_pct(100)); /* 2. 添加三个标签页 */ lv_obj_t * tab_home = lv_tabview_add_tab(tabview, "首页"); lv_obj_t * tab_data = lv_tabview_add_tab(tabview, "数据"); lv_obj_t * tab_set = lv_tabview_add_tab(tabview, "设置"); /* 3. 在“首页”标签页里添加内容 */ lv_obj_t * label_title = lv_label_create(tab_home); lv_label_set_text(label_title, "环境监测终端"); lv_obj_set_style_text_font(label_title, &lv_font_montserrat_14, 0); lv_obj_align(label_title, LV_ALIGN_TOP_MID, 0, 20); lv_obj_t * label_temp = lv_label_create(tab_home); lv_label_set_text(label_temp, "当前温度:25.6 ℃"); lv_obj_align(label_temp, LV_ALIGN_CENTER, 0, 0); /* 4. 在“数据”标签页里添加一个简单表格占位 */ lv_obj_t * label_history = lv_label_create(tab_data); lv_label_set_text(label_history, "历史数据列表"); lv_obj_align(label_history, LV_ALIGN_TOP_MID, 0, 10); /* 5. 在“设置”标签页里添加开关控件 */ lv_obj_t * sw = lv_switch_create(tab_set); lv_obj_align(sw, LV_ALIGN_CENTER, 0, 0); lv_obj_t * label_switch = lv_label_create(tab_set); lv_label_set_text(label_switch, "自动刷新"); lv_obj_align_to(label_switch, sw, LV_ALIGN_OUT_LEFT_MID, -10, 0); }这段代码做了三件事:
- 创建 Tabview 容器,指定标签条位于顶部、高度 50 像素。
- 添加了“首页”“数据”“设置”三个标签页。
- 往每个标签页里塞入了不同类型的控件,证明标签页容器是普通 LVGL 对象,可以放任意控件。
lv_obj_set_size(tabview, lv_pct(100), lv_pct(100))很重要。如果你的父容器比屏幕小,Tabview 默认大小可能不够撑满屏幕,导致底部按钮无法点击。用百分比设置尺寸,可以自适应不同分辨率。
lv_obj_align_to用来让一个对象相对于另一个对象对齐。上面的代码把“自动刷新”文字放在开关的左边,中间留了 10 像素间距。
4.4 初始化并运行
接下来在主程序中调用 UI 创建函数,并处理 LVGL 的 tick 心跳和任务调度。
Espressif IDF 环境可以参考如下写法:
/* 文件路径:main/main.c */ #include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "lvgl.h" #include "ui/ui_tabview.h" /* 屏幕初始化函数,根据你的屏幕驱动实现 */ extern void lvgl_driver_init(void); /* LVGL 心跳定时器回调,放在定时器中断或高优先级任务中 */ static void lv_tick_task(void * arg) { (void)arg; while (1) { lv_tick_inc(10); vTaskDelay(pdMS_TO_TICKS(10)); } } /* LVGL 刷新与事件处理任务 */ static void lvgl_task(void * arg) { (void)arg; while (1) { lv_task_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } } void app_main(void) { /* 1. 初始化屏幕驱动、触摸驱动 */ lvgl_driver_init(); /* 2. 初始化 LVGL 本身 */ lv_init(); /* 3. 创建心跳任务 */ xTaskCreate(lv_tick_task, "lv_tick", 2048, NULL, 1, NULL); /* 4. 创建 UI */ ui_tabview_create(lv_scr_act()); /* 5. 创建 LVGL 任务 */ xTaskCreate(lvgl_task, "lvgl_task", 4096, NULL, 1, NULL); }这里有两个常见坑点需要特别说明:
lv_tick_inc(10)的周期必须与延时一致。如果你用 10ms 周期就延时 10ms,用 1ms 周期就延时 1ms,否则动画速度不对。lv_task_handler()不要在中断服务函数里调用,它内部可能耗时较长,应该放在任务或主循环中。
如果你的工程里已经有 LVGL 心跳和任务处理机制,不要重复创建,否则会引发不可预料的崩溃或 UI 卡顿。
4.5 预期效果
烧录运行后,屏幕顶部会出现三个标签按钮:“首页”“数据”“设置”。默认显示“首页”页面,中间显示“当前温度:25.6 ℃”;点击“数据”,页面切换到历史数据界面;点击“设置”,页面显示一个开关和“自动刷新”文字。
如果点击标签时页面没有切换,优先检查触摸驱动是否注册成功。如果内容页文字乱码,优先检查字体配置和编码方式。
5. 样式定制与状态交互
5.1 修改标签栏背景与文字颜色
Tabview 默认的样式比较朴素。我们可以通过样式系统把标签栏改成符合产品风格的界面。
/* 修改标签栏背景色和圆角 */ lv_obj_t * tab_bar = lv_tabview_get_tab_btns(tabview); lv_obj_set_style_bg_color(tab_bar, lv_color_hex(0x2196F3), 0); lv_obj_set_style_bg_opa(tab_bar, LV_OPA_COVER, 0); lv_obj_set_style_pad_all(tab_bar, 0, 0); /* 修改标签按钮的背景色 */ lv_obj_set_style_bg_color(tab_bar, lv_color_hex(0xFFFFFF), LV_PART_ITEMS); lv_obj_set_style_bg_color(tab_bar, lv_color_hex(0x2196F3), LV_PART_ITEMS | LV_STATE_CHECKED);这里使用了LV_PART_ITEMS和LV_STATE_CHECKED组合。解释一下:
LV_PART_ITEMS表示样式应用到标签栏里的子项,也就是每个标签按钮。LV_STATE_CHECKED表示选中状态。默认选中项是 checked 状态,所以你可以分别设置默认态和选中态的颜色。
要注意的是,样式属性的作用顺序在 LVGL 中有一套优先级逻辑:对象本身的 style 优先级低于 theme,但显式设置的 style 通常会覆盖 theme。如果发现修改颜色不生效,检查是不是被其他样式覆盖。
5.2 为标签页内容添加事件响应
Tabview 本身处理了标签点击事件,你不需要额外去监听标签按钮的LV_EVENT_CLICKED。但可以监听 Tabview 的LV_EVENT_VALUE_CHANGED事件,在页面切换时执行自定义逻辑。
static void tab_changed_event_cb(lv_event_t * e) { lv_obj_t * tabview = lv_event_get_target(e); uint32_t active_id = lv_tabview_get_tab_act(tabview); ESP_LOGI("TAB", "切换到第 %lu 个标签页", (unsigned long)active_id); } /* 在创建 Tabview 后注册事件回调 */ lv_obj_add_event_cb(tabview, tab_changed_event_cb, LV_EVENT_VALUE_CHANGED, NULL);这个回调的典型应用场景是:切换到“数据”页时,触发一次数据刷新;切换到“设置”页时,读取一次配置参数。
注意:示例中使用了ESP_LOGI,如果你在 PC 模拟器里跑,需要把日志宏换成printf或 LVGL 自带的日志输出。
5.3 动态切换选项卡
有时候我们需要在代码里主动跳转到某个标签页,比如用户完成引导流程后,自动跳到“首页”页面。
/* 切换到索引为 0 的标签页,带动画 */ lv_tabview_set_tab_act(tabview, 0, true); /* 切换到索引为 2 的标签页,不带动画 */ lv_tabview_set_tab_act(tabview, 2, false);这里要注意索引越界问题。如果你只添加了 3 个标签页,索引范围是 0 到 2,传入 3 或更大值会导致断言失败或未定义行为。实际项目中最好在调用前用lv_tabview_get_tab_cnt()获取标签总数做一次校验。
6. 中文显示与界面编辑器建议
6.1 LVGL 中文字体加载流程
Label 上直接写中文时,如果你没有加载中文字体,屏幕上会显示一排方块或空白。这是 LVGL 新手最常碰到的问题之一。
LVGL 内部默认的 Montserrat 字体只包含 ASCII 字符。要显示中文,必须准备一份支持中文的字体文件,编译时通过字体转换工具生成 C 数组,然后在代码中引用。
LVGL 官方提供了一个在线字体转换工具,可以导入你想要使用的 .ttf/.woff 字体文件,再勾选需要的字符集或指定字符范围,生成后得到.c和.h文件。
使用步骤:
- 准备一个开源中文字体,比如思源黑体、阿里普惠体,注意文件不能太大。
- 在工具里选择字体大小、字符范围,一般选择“常用中文”或输入你项目中用到的字符。
- 生成 C 文件后,放入
components/lvgl/src/font/或者自定义目录。 - 在代码中声明外部字体变量,并调用
lv_obj_set_style_text_font()应用。
示例代码如下:
/* 声明字体 */ LV_FONT_DECLARE(my_font_16); /* 应用字体 */ lv_obj_t * label = lv_label_create(tab_home); lv_label_set_text(label, "环境监测终端"); lv_obj_set_style_text_font(label, &my_font_16, 0);中文字体体积通常较大,16 号字、常用 3000 字左右的字体数组可能有几百 KB 到几 MB。ESP32 的 Flash 容量一般能放下,但 RAM 不一定能一次性加载。如果内存紧张,注意开启 LVGL 的字体内存管理或使用字体分段加载特性。
6.2 GUI Guider 等界面编辑器的使用建议
除了手写代码,NXP 的 GUI Guider、LVGL 官方的 SquareLine Studio 都支持通过可视化方式创建 Tabview 界面。它们的用法类似:拖一个 Tabview 组件到画布上,在右侧属性面板添加 Tab,往页面里拖入 Label、Button、Chart 等控件,最后导出 C 代码。
如果你的项目对界面要求不高,或者时间紧急,用界面编辑器能明显提速。但编辑导出的代码经常包含大量自动生成的样式和对象命名,代码体积偏大,维护起来没有手写清晰。建议把这些工具当成“原型设计器”使用:先在编辑器里排好布局、确认交互,再在正式工程里手动实现,或者直接基于生成代码做裁剪。
7. 常见问题排查
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 屏幕白屏,无任何显示 | LVGL 未初始化完成,或没有调用lv_task_handler() | 检查lv_init()、屏幕驱动初始化、任务创建顺序 |
| 标签栏显示,但页面点击无反应 | 触摸驱动未注册,或点击事件没有传给 Tabview | 检查输入设备注册与触摸坐标校准 |
| 中文显示为方块 | 未加载中文字体 | 使用中文字体转换工具生成字体数组,应用到对应 Label |
| 切换到某个页面时崩溃 | 页面索引越界,或该页面里的空指针操作 | 用lv_tabview_get_tab_cnt()做边界校验 |
| 页面位置乱跑,按钮条消失 | 手动修改了 Tabview 内部对象的位置/大小 | 不要直接操作 Tabview 内部内容区,改用页面容器布局 |
| 动画卡顿 | LVGL 任务优先级低,或刷新频率不够 | 提高 LVGL 任务优先级,缩短lv_task_handler()调用周期 |
| 点击标签后没反应,但触摸坐标正确 | 标签按钮被其他透明对象遮挡 | 检查页面内容是否越界覆盖到标签栏,用lv_obj_move_foreground()调整层级 |
如果遇到重启循环或类似线程同步的问题,优先怀疑 tick 心跳周期与lv_tick_inc()传入参数不一致。这个问题非常隐蔽,表面现象是动画不执行、事件处理不正常,容易让人误判成 UI 逻辑问题。
8. 最佳实践与工程建议
8.1 页面内容保持独立
每一个 Tab 页的内容,最好用一个独立的函数封装。不要让ui_tabview_create里堆满所有代码。推荐的函数划分方式:
static void create_home_page(lv_obj_t * parent); static void create_data_page(lv_obj_t * parent); static void create_setting_page(lv_obj_t * parent);这样每个页面逻辑清楚,后续增加页面也只需要新增一个函数。
8.2 控制对象数量与刷新频率
LVGL 的动画、样式和布局计算都会耗费 CPU 资源。ESP32 主频虽然不算低,但屏幕分辨率较大时,刷新压力仍然明显。一个 Tab 页里的控件数量不宜过多,尤其是动态更新的 Label,控制刷新频率在 5Hz 到 20Hz 即可,过度刷新会挤压其他任务的运行时间。
8.3 统一使用百分比布局
不同尺寸的屏幕,或者横竖屏切换时,绝对像素布局容易出现内容超出屏幕的问题。推荐在 Tabview 层面和页面内容容器层面都使用lv_pct()来设置尺寸,或者在LV_EVENT_RESIZE回调中动态调整控件位置。
8.4 状态变化及时更新 UI
嵌入式项目中,后台传感器数据变化后需要更新界面。推荐的做法是使用 LVGL 的lv_label_set_text()直接更新文本,并配合lv_obj_invalidate()强制刷新。不要在主循环里用大量延时去等待 UI 绘制完成,LVGL 有自己的刷新机制,阻塞主循环会破坏整个 UI 调度。
8.5 安全与权限提醒
如果你的嵌入式设备后续要接入 Wi-Fi 和网络,注意界面中避免显示敏感信息,并且做好权限隔离。尤其在设备生产环境中,通过 UI 触发配置修改、固件升级等高风险操作时,应增加二次确认机制,并保证操作在测试环境验证通过后再发布。
9. 小结与下一步学习方向
本篇文章的核心内容可以概括为:
- 理解了 Tabview 的整体结构:标签按钮条 + 内容容器 + 多个页面。
- 学会了用
lv_tabview_create()和lv_tabview_add_tab()创建选项卡界面。 - 学会了修改标签栏样式、监听页面切换事件、动态切换标签页。
- 解决了中文显示和常见崩溃问题,并整理了工程化建议。
如果你已经能跑通本文示例,下一步可以去尝试:
- 用两个 Tabview 实现二级菜单界面。
- 在 Tabview 中放入 Chart 图表,做实时数据曲线。
- 把 Tabview 从 PC 模拟器移植到真机,感受触摸交互的差异。
- 研究 LVGL 的样式系统,为 Tabview 做一套风格统一的主题。
嵌入式 UI 开发的核心能力不是记住某个 API,而是理解对象树、样式和事件机制。Tabview 只是其中一块拼图。建议你亲手运行一遍本文代码,然后尝试往页面里加入自己的控件。遇到问题不要慌,优先查 LVGL 官方文档和示例代码,这个库的生态资料已经非常丰富了。
如果这篇文章对你有帮助,可以先收藏备用。后续我会继续更新 ESP32 + LVGL 的表格、图表、弹窗和动画实战,欢迎保持关注。