- 智能硬件
- 嵌入式
- 硬件开发
【免费下载链接】X-TRACK
A GPS bicycle speedometer that supports offline maps and track recording
LVGL(Light and Versatile Graphics Library)的lv_table是一个专为嵌入式 GUI 设计的高效表格控件:它只存储文本、不创建任何真实子对象,单元格全部在绘制时即时渲染,因此内存占用极轻,非常适合在资源受限的嵌入式设备上展示结构化数据。本文以 X-TRACK(离线地图 GPS 自行车码表)仓库中内置的 LVGL 源码与文档为研究对象,系统讲解lv_table的样式划分、单元格读写、行列管理、合并、滚动、事件与按键交互,并结合 lv_table.h 与 lv_table.c 给出源码级原理支撑,以及 lv_example_table_1.c / lv_example_table_2.c 两个可直接复用的实战示例。读完本文,你将能够独立完成一个可滚动、可合并、可按键选中单元格的 LVGL 表格。
一、控件概述:为什么说 Table 是"最轻量"的表格
lv_table的定位很明确:表格由行、列、存放文本的单元格构成,控件本身极其轻量,因为只有文本被存储,单元格不会创建任何真实对象,而是在绘制时按需画出来。这意味着你在创建一张 200 行的表格时,LVGL 并不会实例化 200 个 label 对象,而是维护一张紧凑的文本数据表,绘制阶段统一渲染。
从源码结构看,这一点得到了充分印证。lv_table_t的数据结构(lv_table.h)非常简单:
typedef struct { lv_obj_t obj; /* 继承自 Base Object */ uint16_t col_cnt; /* 列数 */ uint16_t row_cnt; /* 行数 */ char ** cell_data; /* 单元格文本指针数组(每格头部 1 字节存控制位) */ lv_coord_t * row_h; /* 每行高度 */ lv_coord_t * col_w; /* 每列宽度 */ uint16_t col_act; /* 当前激活列 */ uint16_t row_act; /* 当前激活行 */ } lv_table_t;整个控件的数据全部由这几个字段承载,没有子对象树。此外,lv_table_class中声明了editable = LV_OBJ_CLASS_EDITABLE_TRUE和group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE(见 lv_table.c),说明 Table 是可编辑对象,且默认加入控件组(group)——这正是它能通过编码器(encoder)方向键逐格选中单元格的前提。
启用条件
lv_table依赖 Label 控件,且受LV_USE_TABLE宏开关控制:
- 若
LV_USE_LABEL == 0,编译会直接报错"lv_table: lv_label is required. Enable it in lv_conf.h (LV_USE_LABEL 1)"(见 lv_table.h); - 默认配置模板中
LV_USE_TABLE为 1(见 lv_conf_template.h),本仓库 Simulator 的 lv_conf.h 同样开启了该功能;Kconfig 菜单中也提供了对应配置项(见 lvgl/Kconfig)。
二、Parts 与样式:Part 机制下的表格美化
lv_table只有两个样式部分(Part):
LV_PART_MAIN:表格的背景,支持全部典型背景样式属性(背景色、边框、圆角、内边距等);LV_PART_ITEMS:单元格区域,同样支持背景样式属性,外加文本样式属性(字体、文字颜色、对齐方式、行间距等)。
默认主题(default theme)专门为单元格准备了table_cell样式(见 lv_theme_default.c),其核心是给每个单元格绘制 1px 的上下边框(LV_BORDER_SIDE_TOP | LV_BORDER_SIDE_BOTTOM),从而形成典型的表格分隔线外观。
一个实用的样式技巧:表格默认会为被按下的单元格显示"按下"状态,如果你希望自己接管选中态的绘制(例如下面示例 2 中要自定义开关图形),可以移除该状态样式:
lv_obj_remove_style(table, NULL, LV_PART_ITEMS | LV_STATE_PRESSED);三、单元格操作:写入、格式化与读取
设置单元格文本
单元格只能存储文本,因此数值必须先转换为字符串再写入:
void lv_table_set_cell_value(lv_obj_t * obj, uint16_t row, uint16_t col, const char * txt);要点(对应文档说明):
- 文本会被 Table拷贝保存,因此可以传入局部变量,函数返回后无需担心指针失效;
- 文本支持换行符,例如
"Value\n60.3"会在单元格内渲染成两行; - 行列会自动扩展:如果指定的
row/col超过了当前行列数,Table 会自动增加行列。
从实现看(lv_table.c),lv_table_set_cell_value内部首先做自动扩展:
if(col >= table->col_cnt) lv_table_set_col_cnt(obj, col + 1); if(row >= table->row_cnt) lv_table_set_row_cnt(obj, row + 1);随后用lv_mem_realloc重新分配该格内存并拷贝文本,最后调用refr_cell_size触发该格尺寸刷新。值得注意的实现细节:每个单元格内存块的第 0 字节保存控制位(control byte),真正文本从第 1 字节开始,读取时通过&table->cell_data[cell][1]跳过格式字节(见 lv_table.c)。
printf 风格格式化写入
LVGL 还提供了printf风格的便捷接口,尤其适合批量生成带编号的文本:
void lv_table_set_cell_value_fmt(lv_obj_t * obj, uint16_t row, uint16_t col, const char * fmt, ...);其实现先通过lv_vsnprintf(NULL, 0, fmt, ap)计算格式化后的长度,再按需 realloc 并写入(见 lv_table.c)。注意该实现还处理了阿拉伯/波斯语文本整形(LV_USE_ARABIC_PERSIAN_CHARS)的兼容路径。示例用法:
lv_table_set_cell_value_fmt(table, i, 0, "Item %"LV_PRIu32, i + 1);其中LV_PRIu32是 LVGL 提供的跨平台 printf 格式宏,避免在 32/64 位环境下%u/%lu混用导致的格式警告。
读取单元格
const char * lv_table_get_cell_value(lv_obj_t * obj, uint16_t row, uint16_t col);如果row/col越界或单元格为空,返回空字符串""而非空指针(见 lv_table.c),调用方可安全使用。
四、行列管理:行数、列数与列宽
显式设置行列数使用:
void lv_table_set_row_cnt(lv_obj_t * obj, uint16_t row_cnt); void lv_table_set_col_cnt(lv_obj_t * obj, uint16_t col_cnt);对应的读取接口为lv_table_get_row_cnt/lv_table_get_col_cnt。
从源码看,改变行数时会 reallocrow_h与cell_data数组,缩小表格时释放多余的单元格内存,扩大时清零新增区域(见 lv_table.c);改变列数时由于列主序存储布局变化,需要整体搬迁cell_data并重新映射,新增列默认宽度取LV_DPI_DEF(每英寸点数换算的默认像素值,见 lv_table.c)。
设置列宽使用:
void lv_table_set_col_width(lv_obj_t * obj, uint16_t col_id, lv_coord_t w);- 表格整体宽度会自动等于所有列宽之和;
- 表格高度则由单元格样式(字体、内边距等)与行数自动计算。
实际项目中,推荐先设行列数、再逐格填充:先lv_table_set_row_cnt+lv_table_set_col_cnt可以一次性分配好整个数据数组,避免set_cell_value时反复自动扩展引发大量 realloc(示例 2 的注释就明确写着 "Not required but avoids a lot of memory reallocation")。
五、单元格合并:水平合并单元格
lv_table支持水平方向的单元格合并:
void lv_table_add_cell_ctrl(lv_obj_t * obj, uint16_t row, uint16_t col, lv_table_cell_ctrl_t ctrl); // 合并标志: // lv_table_add_cell_ctrl(table, row, col, LV_TABLE_CELL_CTRL_MERGE_RIGHT);要合并多个相邻单元格,需要对每个目标单元格分别调用一次该函数。对应清除合并标志的接口是lv_table_clear_cell_ctrl,查询状态用lv_table_has_cell_ctrl。
控制位定义完整列表见 lv_table.h:
| 控制位 | 值 | 用途 |
|---|---|---|
LV_TABLE_CELL_CTRL_MERGE_RIGHT | 1 << 0 | 与右侧单元格合并 |
LV_TABLE_CELL_CTRL_TEXT_CROP | 1 << 1 | 裁剪超宽文本 |
LV_TABLE_CELL_CTRL_CUSTOM_1 | 1 << 4 | 用户自定义位 1 |
LV_TABLE_CELL_CTRL_CUSTOM_2 | 1 << 5 | 用户自定义位 2 |
LV_TABLE_CELL_CTRL_CUSTOM_3 | 1 << 6 | 用户自定义位 3 |
LV_TABLE_CELL_CTRL_CUSTOM_4 | 1 << 7 | 用户自定义位 4 |
CUSTOM_1~CUSTOM_4四个自定义控制位完全交由应用层使用,lv_table_has_cell_ctrl可查询"是否全部置位"。源码中控制位就存放在每个单元格内存块的第 0 字节(格式字节),这也是每个格子在 realloc 文本前先备份ctrl的原因(见 lv_table.c)。
六、尺寸与滚动:LV_SIZE_CONTENT 与可滚动表格
lv_table默认宽度与高度均为LV_SIZE_CONTENT(见类定义width_def/height_def,lv_table.c),因此:
- 若宽或高设为
LV_SIZE_CONTENT,Table 会按内容展开,显示全部行列,例如:lv_obj_set_size(table, LV_SIZE_CONTENT, LV_SIZE_CONTENT);会自动调整表格尺寸以容纳所有行列;
- 若宽或高小于内容固有尺寸,Table 会变成可滚动(LVGL 所有对象都带滚动机制)。
实战中最常见的是"固定高度 + 内容宽度"的组合,示例 1 正是这样构造可滚动表格的:
lv_obj_t * table = lv_table_create(lv_scr_act()); /* 填充单元格…… */ lv_obj_set_height(table, 200); /* 高度 200px,超过部分可滚动 */ lv_obj_center(table);七、事件系统:绘制回调与值变更
LV_EVENT_VALUE_CHANGED
当通过按键(编码器)选中一个新单元格时,Table 发送LV_EVENT_VALUE_CHANGED。典型用途是配合lv_table_get_selected_cell实现"选中即操作",例如示例 2 的开关列表:选中某行后切换该行CUSTOM_1控制位,从而翻转开关状态:
static void change_event_cb(lv_event_t * e) { lv_obj_t * obj = lv_event_get_target(e); uint16_t row, col; lv_table_get_selected_cell(obj, &row, &col); bool chk = lv_table_has_cell_ctrl(obj, row, 0, LV_TABLE_CELL_CTRL_CUSTOM_1); if(chk) lv_table_clear_cell_ctrl(obj, row, 0, LV_TABLE_CELL_CTRL_CUSTOM_1); else lv_table_add_cell_ctrl(obj, row, 0, LV_TABLE_CELL_CTRL_CUSTOM_1); }LV_EVENT_DRAW_PART_BEGIN / DRAW_PART_END
这两个绘制事件对单元格逐个触发,事件类型为LV_TABLE_DRAW_PART_CELL,事件参数lv_obj_draw_part_dsc_t中与表格相关的字段:
part:LV_PART_ITEMS;draw_area:单元格绘制区域;rect_dsc/label_dsc:矩形与文本绘制描述符,可在此修改背景色、对齐方式等;id:单元格线性索引,等于当前行 × 总列数 + 当前列。
示例 1 用该回调实现了三种典型美化:
if(dsc->part == LV_PART_ITEMS) { uint32_t row = dsc->id / lv_table_get_col_cnt(obj); uint32_t col = dsc->id - row * lv_table_get_col_cnt(obj); if(row == 0) { /* 表头居中、浅蓝背景 */ dsc->label_dsc->align = LV_TEXT_ALIGN_CENTER; dsc->rect_dsc->bg_color = lv_color_mix(lv_palette_main(LV_PALETTE_BLUE), dsc->rect_dsc->bg_color, LV_OPA_20); dsc->rect_dsc->bg_opa = LV_OPA_COVER; } else if(col == 0) { /* 第一列右对齐 */ dsc->label_dsc->align = LV_TEXT_ALIGN_RIGHT; } if((row != 0 && row % 2) == 0) { /* 偶数行灰色条纹(斑马纹) */ dsc->rect_dsc->bg_color = lv_color_mix(lv_palette_main(LV_PALETTE_GREY), dsc->rect_dsc->bg_color, LV_OPA_10); dsc->rect_dsc->bg_opa = LV_OPA_COVER; } }由此可以看出:表格样式"整列/整行统一"这种需求,不必逐格设置样式,在绘制回调里按行号/列号分支处理即可,这也是 Table 控件推荐的美化模式。若在LV_EVENT_DRAW_PART_END中回调,则可像示例 2 那样用dsc->draw_ctx在单元格上额外绘制自定义图形(开关滑块)。
另外,LV_EVENT_DRAW_PART_BEGIN/END之外,Table 作为 Base Object 的子类,还继承 Base Object 的全部事件;事件机制的通用说明可参考 事件总览。
八、按键与编码器交互:方向键选格
lv_table是一个可编辑对象且默认加入 group,因此通过方向键/编码器即可逐格移动选区:
LV_KEY_RIGHT / LEFT / UP / DOWN:移动选中单元格;LV_KEY_ENTER:按惯例被翻译为LV_EVENT_PRESSED / PRESSING / RELEASED等事件,Table 不单独处理 Enter 键。
查询当前选中的单元格:
uint16_t row, col; lv_table_get_selected_cell(table, &row, &col); // 无选中时 row/col 会被置为 LV_TABLE_CELL_NONE (0xFFFF)LV_TABLE_CELL_NONE定义为0XFFFF(见 lv_table.h),用于表示"没有单元格被选中"。按键机制的通用说明可参考 输入设备总览。
九、实战示例:从示例代码到完整表格
示例 1:带斑马纹与滚动的水果价格表
完整代码见 lv_example_table_1.c,核心步骤:
lv_table_create(lv_scr_act())创建表格;- 逐格写入两列数据(第 0 列为 "Name",第 1 列为 "Price",8 行数据);
lv_obj_set_height(table, 200)制造垂直滚动;- 注册
LV_EVENT_DRAW_PART_BEGIN回调实现表头居中高亮、首列右对齐、偶数行灰底。
这是"查看型表格"的通用写法,适合数据浏览、日志列表等场景。
示例 2:用 Table 实现超轻量列表(含开关)
完整代码见 lv_example_table_2.c,它演示了 Table 作为列表容器的经典用法:
- 预分配 200 行 × 1 列(
lv_table_set_row_cnt(table, ITEM_CNT)+lv_table_set_col_cnt(table, 1)),避免自动扩展带来的反复 realloc; - 用
lv_table_set_cell_value_fmt循环生成"Item N"文本; - 移除单元格按下态样式,改由
LV_EVENT_DRAW_PART_END在每行右侧绘制一个圆形开关滑块,开关状态由CUSTOM_1控制位记忆; - 在
LV_EVENT_VALUE_CHANGED中翻转控制位,实现"选中即切换"。
该示例运行结束后会在屏幕上报告200 项仅用数毫秒创建、占用极少内存,直观展示了lv_table"只存文本、按需绘制"的轻量特性。这种"表格当列表用"的模式非常适合嵌入式菜单、设置项列表等交互界面。
十、API 速查表
| 功能 | 函数 | 说明 |
|---|---|---|
| 创建 | lv_table_create(parent) | 创建 Table 对象 |
| 写单元格 | lv_table_set_cell_value(table, row, col, txt) | 文本拷贝保存,行列自动扩展 |
| 格式化写 | lv_table_set_cell_value_fmt(table, row, col, fmt, ...) | printf 风格 |
| 读单元格 | lv_table_get_cell_value(table, row, col) | 空/越界返回"" |
| 行数 | lv_table_set_row_cnt/lv_table_get_row_cnt | 显式设定行数 |
| 列数 | lv_table_set_col_cnt/lv_table_get_col_cnt | 显式设定列数 |
| 列宽 | lv_table_set_col_width/lv_table_get_col_width | 整体宽度 = 各列宽之和 |
| 合并 | lv_table_add_cell_ctrl(table, r, c, LV_TABLE_CELL_CTRL_MERGE_RIGHT) | 水平合并 |
| 控制位 | lv_table_add_cell_ctrl/lv_table_clear_cell_ctrl/lv_table_has_cell_ctrl | 合并、裁剪、自定义位 |
| 选中格 | lv_table_get_selected_cell(table, &row, &col) | 无选中时为LV_TABLE_CELL_NONE |
| 滚动 | lv_obj_set_size(table, LV_SIZE_CONTENT, 高度/宽度) | 小于固有尺寸即可滚动 |
十一、在 X-TRACK 工程中使用 Table 的注意事项
X-TRACK 是一个基于 LVGL 的嵌入式 GPS 码表项目,Simulator 与 MDK-ARM 各平台固件共用同一套 LVGL 源码树(Simulator/LVGL.Simulator/lvgl)。在资源敏感的嵌入式目标上使用lv_table时建议:
- 确认目标工程的
lv_conf.h中LV_USE_TABLE已开启(否则头文件中的#if LV_USE_TABLE != 0会让整个控件被编译排除); - 大数据量表格先设行列数再填充,减少自动扩展期间的动态内存 realloc 碎片;
- 文本内容采用"存指针/复用字符串"策略,善用
lv_table_set_cell_value_fmt避免手写snprintf转换数值; - 用绘制回调而非逐格建样式对象的方式实现斑马纹、表头高亮等视觉效果,保持控件"无子对象"的轻量特性。
综上,lv_table以"轻量存储 + 即时绘制"的设计在嵌入式 GUI 中兼顾了表现力与性能,配合其按键选格、绘制回调与自定义控制位机制,足以覆盖数据展示、设置列表、日志浏览等绝大多数表格场景。
- 智能硬件
- 嵌入式
- 硬件开发
【免费下载链接】X-TRACK
A GPS bicycle speedometer that supports offline maps and track recording
相关推荐
Vant Cell 单元格组件完全指南:从基础用法到源码级原理解析
Vant Cell 单元格组件完全指南:从基础用法到源码级原理解析 Cell (单元格)是 Vant 移动端 UI 库中最基础也最高频的列表展示组件,常用于信息
前端UI组件Vant Weapp SwipeCell 滑动单元格组件完全指南:从基础用法到源码级原理解析
Vant Weapp SwipeCell 滑动单元格组件完全指南:从基础用法到源码级原理解析 导读 SwipeCell(滑动单元格)是 Vant Weapp 小
前端小程序UI组件移动开发ThingsBoard 表格组件单元格内容函数(Cell content function)完全指南:从配置到源码级实现
ThingsBoard 表格组件单元格内容函数(Cell content function)完全指南:从配置到源码级实现 导读 单元格内容函数(Cell con
物联网后端数据可视化消息队列
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考