☰
LVGL 表格控件 lv_table 全面解析:从单元格设置、滚动交互到源码级原理
2026/10/5 2:06:33 网站建设 项目流程
  • 智能硬件
  • 嵌入式
  • 硬件开发

【免费下载链接】X-TRACK

A GPS bicycle speedometer that supports offline maps and track recording

项目地址:https://gitcode.com/gh_mirrors/xt/X-TRACK
点击查看免费下载

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_RIGHT1 << 0与右侧单元格合并
LV_TABLE_CELL_CTRL_TEXT_CROP1 << 1裁剪超宽文本
LV_TABLE_CELL_CTRL_CUSTOM_11 << 4用户自定义位 1
LV_TABLE_CELL_CTRL_CUSTOM_21 << 5用户自定义位 2
LV_TABLE_CELL_CTRL_CUSTOM_31 << 6用户自定义位 3
LV_TABLE_CELL_CTRL_CUSTOM_41 << 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,核心步骤:

  1. lv_table_create(lv_scr_act())创建表格;
  2. 逐格写入两列数据(第 0 列为 "Name",第 1 列为 "Price",8 行数据);
  3. lv_obj_set_height(table, 200)制造垂直滚动;
  4. 注册LV_EVENT_DRAW_PART_BEGIN回调实现表头居中高亮、首列右对齐、偶数行灰底。

这是"查看型表格"的通用写法,适合数据浏览、日志列表等场景。

示例 2:用 Table 实现超轻量列表(含开关)

完整代码见 lv_example_table_2.c,它演示了 Table 作为列表容器的经典用法:

  1. 预分配 200 行 × 1 列(lv_table_set_row_cnt(table, ITEM_CNT)+lv_table_set_col_cnt(table, 1)),避免自动扩展带来的反复 realloc;
  2. 用lv_table_set_cell_value_fmt循环生成"Item N"文本;
  3. 移除单元格按下态样式,改由LV_EVENT_DRAW_PART_END在每行右侧绘制一个圆形开关滑块,开关状态由CUSTOM_1控制位记忆;
  4. 在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

项目地址:https://gitcode.com/gh_mirrors/xt/X-TRACK
点击查看免费下载

相关推荐

上一篇:如何永久备份微信聊天记录?本地免费工具WeChatMsg终极指南
下一篇:AutoSubs终极指南:3步实现本地AI字幕生成,告别繁琐手动编辑

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询