第5章:常用 Widgets 上手

系统分析:create → 定位 → 设属性,一套套路吃遍绝大多数控件

🏛️

本章导师:狄仁杰

核心方法论:系统分析,把零散控件纳入统一框架

「破案要先画一张嫌疑人关系图,学控件也一样:先把 button、label、checkbox、slider 这些'嫌疑人'的关系摆清楚——它们全是从 lv_obj 这个'祖宗'派生出来的,创建、定位、设属性的套路完全一致。摸清这套规律,剩下的只是查 API 的功夫。现在,我们开始系统分析。」

5.1 Widget 家族总览:一脉相承的对象树

第 3 章讲过,LVGL 的一切都是 lv_obj。这一章的所有"控件"(widget)可以这样理解:它们都是 lv_obj 的派生物——按钮、标签、复选框、滑块……每一个都先是一个普通对象,再被装上专属的行为与外观。因此创建控件的 API 有个铁律一样的规律:lv_xxx_create(parent),返回的仍然是 lv_obj_t *。你在 lv_obj 上学到的所有操作——定位、改尺寸、加事件——对任意控件全部照常生效。

所以"学控件"其实是在学一份档案表:记住每个控件叫什么、创建函数是谁、它擅长表达什么。以 lvgl-master/examples/widgets/ 里的 lv_example_button_basic.c 为例,它展示的"按钮 + 居中标签"结构,恰恰暴露了控件的本质:按钮是一个可点击的基础对象,标签是它的子对象,二者拼出一个完整的交互单元。创建几个常用控件,代码风格惊人地一致。

/* 五个常用控件,创建套路完全一致:lv_xxx_create(parent) */
lv_obj_t * scr = lv_screen_active();

lv_obj_t * btn    = lv_button_create(scr);
lv_obj_t * label  = lv_label_create(scr);
lv_obj_t * cb     = lv_checkbox_create(scr);
lv_obj_t * slider = lv_slider_create(scr);
lv_obj_t * sw     = lv_switch_create(scr);
/* 控件就是 lv_obj:返回类型通用,对象 API 通用 */
lv_obj_t * button = lv_button_create(lv_screen_active());

/* 以下全部是 lv_obj 级 API,对 button 同样有效: */
lv_obj_set_size(button, 160, 48);
lv_obj_center(button);
lv_obj_set_pos(button, 10, 20);
控件创建函数用途
按钮lv_button_create()可点击的交互单元(常内嵌一个 label)
标签lv_label_create()显示文本,支持换行/省略/滚动
复选框lv_checkbox_create()带文本的勾选项(勾选态 = LV_STATE_CHECKED
滑块lv_slider_create()拖拽调节数值(在 bar 基础上加了可拖动的旋钮)
开关lv_switch_create()开/关二态开关(勾选态也走 LV_STATE_CHECKED
进度条lv_bar_create()只读地展示进度/占比
狄仁杰提示

记不住控件没关系,抓住"lv_xxx_create 返回 lv_obj_t *"这条主线:先用对象 API 把它摆对位置,再查它专属的 lv_xxx_set_* 去设专属属性。绝不要试图背全每个控件的 API。

5.2 定位与尺寸:给控件排兵布阵

控件创建出来默认堆在父对象左上角,得先给它"站位"。定位 API 有三兄弟:lv_obj_set_pos() 用绝对坐标摆放;lv_obj_align() 把对象对齐到父对象的某个锚点再偏移;lv_obj_center() 则是"对齐到中心"的快捷方式。对齐锚点来自 lv_align_t 枚举——LV_ALIGN_TOP_MID(顶部居中)、LV_ALIGN_LEFT_MID(左垂直居中)、LV_ALIGN_BOTTOM_MID(底部居中)等,LV_ALIGN_CENTER 就是正中央。

尺寸同样有规律。lv_obj_set_size() 一次设宽高,lv_obj_set_width() / lv_obj_set_height() 分别设。一个容易踩的坑:坐标和尺寸的原点是父对象的"内容区",带边框或内边距时会略有偏移;而且某些控件(如按钮)默认"包着内容自适应"——给按钮设宽高,其实是给它的基础对象设定尺寸,内部标签仍居中对齐。理解了"控件 = 对象 + 专属行为",这些行为就都好解释了。

/* 对齐到父对象锚点,再偏移 */
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);   /* 顶部居中,向下偏 10px */
lv_obj_align(nav,   LV_ALIGN_LEFT_MID, 8, 0);    /* 左侧垂直居中 */
lv_obj_center(logo);                              /* 等价于 LV_ALIGN_CENTER */
/* 尺寸:一次设 vs 分别设 */
lv_obj_set_size(panel, 220, 120);    /* 同时设宽高 */
lv_obj_set_width(slider, 180);         /* 只改宽,高保持默认 */
lv_obj_set_pos(panel, 30, 40);       /* 绝对坐标摆放 */
API作用典型写法
lv_obj_set_pos()用 (x, y) 绝对坐标摆放lv_obj_set_pos(obj, 30, 40)
lv_obj_align()对齐到父对象锚点并偏移lv_obj_align(obj, LV_ALIGN_TOP_MID, 0, 10)
lv_obj_center()对齐到父对象中心lv_obj_center(obj)
lv_obj_set_size()同时设定宽高lv_obj_set_size(obj, 220, 120)
狄仁杰提示

调试定位问题时,最有效的一招是给对象临时加个背景色(lv_obj_set_style_bg_color(obj, lv_color_hex(0xff0000), 0),样式系统第 6 章细讲)——看到它的实际范围,比猜坐标快得多。对齐偏移的 y 参数往屏幕下方是正方向。

5.3 按钮与标签:最小可交互单元

按钮和标签是出场率最高的组合,也最能说明"控件怎么拼"。按钮 lv_button_create() 默认按内容自适应尺寸,所以你通常给它设一个显式尺寸,再在里面创建一个标签并 lv_obj_align 到中心。这个"按钮里嵌标签"的结构,参考 lvgl-master/examples/widgets/button/lv_example_button_basic.c 即可:第二个按钮显式 lv_obj_set_size(button_2, 160, 48),标签再居中,文字就稳稳落在按钮正中央。

标签 lv_label_create() 的核心是 lv_label_set_text() 与它的"长文本模式"。文本太长放不下怎么办?lv_label_set_long_mode() 决定行为:LV_LABEL_LONG_MODE_WRAP 自动换行并撑高、LV_LABEL_LONG_MODE_DOTS 末尾省略号、LV_LABEL_LONG_MODE_SCROLL_CIRCULAR 循环滚动。做状态提示时,还常用 lv_label_set_text_fmt() 格式化拼接,比如 "当前值: %d"——第 8 章事件系统的滑块示例就大量用它实时刷新。

/* 按钮 + 居中标签:最小可交互单元(参考 lv_example_button_basic.c) */
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 160, 48);     /* 显式宽高,不然会包着标签自适应 */
lv_obj_center(btn);

lv_obj_t * btn_label = lv_label_create(btn);   /* 标签是按钮的子对象 */
lv_obj_set_align(btn_label, LV_ALIGN_CENTER);
lv_label_set_text(btn_label, "Click me");
/* 标签长文本模式:三种典型处理 */
lv_obj_set_width(lbl_wrap, 120);
lv_label_set_long_mode(lbl_wrap, LV_LABEL_LONG_MODE_WRAP);        /* 换行撑高 */

lv_obj_set_width(lbl_dots, 120);
lv_label_set_long_mode(lbl_dots, LV_LABEL_LONG_MODE_DOTS);         /* 太长打省略号 */

lv_obj_set_width(lbl_scroll, 120);
lv_label_set_long_mode(lbl_scroll, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); /* 循环滚动 */

/* 格式化文本:实时刷新状态提示的利器 */
lv_label_set_text_fmt(status, "Value: %d", value);
长文本模式行为
LV_LABEL_LONG_MODE_WRAP保持宽度,超长换行并撑高对象
LV_LABEL_LONG_MODE_DOTS保持尺寸,末尾显示省略号
LV_LABEL_LONG_MODE_SCROLL保持尺寸,文本来回滚动
LV_LABEL_LONG_MODE_SCROLL_CIRCULAR保持尺寸,文本循环滚动
LV_LABEL_LONG_MODE_CLIP保持尺寸,直接裁切
狄仁杰提示

改完 lv_label_set_long_mode() 后文本没反应?先确认对象有明确宽度(lv_obj_set_width)——长文本模式都是"尺寸定死、处理超出的文字",没有定宽的模式等于没有约束,自然看不出效果。

5.4 复选框、开关与滑块:状态与取值

复选框与开关都是"二态"控件,它们的"选中"不靠专用 API,而是对象的一个状态位:LV_STATE_CHECKED。设置用 lv_obj_add_state(),清除用 lv_obj_remove_state(),读取用 lv_obj_has_state()。用户点击时控件自己会翻转这个状态,你只需在回调里读取它。复选框还能直接带文本——lv_checkbox_create() 之后调 lv_checkbox_set_text(),文字就会画在勾选框旁边。

滑块 lv_slider_create() 是"取值型"控件的代表。取值范围默认是 0–100,可用 lv_slider_set_range() 改成任意区间,lv_slider_set_value() 设置当前值(第三个参数 LV_ANIM_ON/OFF 决定是否带动画),lv_slider_get_value() 读取。注意滑块本质是"能拖动的进度条",它和 5.5 的进度条共用一套数值模型——这也是"系统分析"要看到的规律:bar 提供数值与外观,slider 在 bar 之上加了交互

/* 复选框与开关:都是 LV_STATE_CHECKED 状态位 */
lv_obj_t * cb = lv_checkbox_create(scr);
lv_checkbox_set_text(cb, "Enable feature");
lv_obj_add_state(cb, LV_STATE_CHECKED);   /* 预置为勾选 */

lv_obj_t * sw = lv_switch_create(scr);
lv_obj_add_state(sw, LV_STATE_CHECKED);   /* 开关默认打开 */

/* 读取:bool 判断当前是否勾选/打开 */
if(lv_obj_has_state(cb, LV_STATE_CHECKED)) { /* 已勾选 */ }
/* 滑块:范围 + 取值 + 读取(参考 lv_example_slider_event.c) */
lv_obj_t * slider = lv_slider_create(scr);
lv_obj_set_width(slider, 200);
lv_slider_set_range(slider, 0, 100);
lv_slider_set_value(slider, 50, LV_ANIM_OFF);

/* 回调里读取当前值(事件系统第 8 章深入,这里先用 LV_EVENT_VALUE_CHANGED) */
static void slider_event_cb(lv_event_t * e)
{
    lv_obj_t * obj = lv_event_get_target_obj(e);
    int32_t v = lv_slider_get_value(obj);
    lv_label_set_text_fmt(status_label, "Value: %d", (int)v);
}

lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
控件状态/数值模型关键 API
复选框勾选态 = LV_STATE_CHECKEDlv_checkbox_set_text()
开关开/关 = LV_STATE_CHECKED无专属取值,走 lv_obj_add_state
滑块区间内整数lv_slider_set_range/set_value/get_value
狄仁杰提示

复选框和开关用同一个状态位,这是刻意的设计:你的代码里"要不要某功能"的判断,对两者写法完全一样(lv_obj_has_state(..., LV_STATE_CHECKED))。换控件不用改逻辑——系统分析的回报就在这种地方。

5.5 进度条与综合面板:把控件拼成仪表盘

进度条 lv_bar_create() 是"只读的数值展示"。它的 API 与滑块几乎对称:lv_bar_set_range() 设范围、lv_bar_set_value() 设当前值(同样带 LV_ANIM_ON/OFF)、lv_bar_set_mode() 选模式(LV_BAR_MODE_NORMAL 普通、LV_BAR_MODE_SYMMETRICAL 对称、LV_BAR_MODE_RANGE 区间)。做仪表盘时,bar 的典型用途是把另一个控件的值"可视化"——比如让滑块的值实时反映到一根进度条上。

现在把前面所有技巧合成一个完整的小面板:顶部一个标题、中间一根滑块、下面一根进度条、最底一个状态标签。全部用 lv_obj_align 锚定到顶部居中的不同垂直位置,代码量不到 20 行——这就是"控件 = 对象 + 专属属性"在实战里的样子。参考 lvgl-master/examples/widgets/bar/examples/widgets/slider/,可以找到大量同类组合。

/* 进度条:范围 + 值(参考 lv_example_bar_value_range.c 思路) */
lv_obj_t * bar = lv_bar_create(scr);
lv_obj_set_size(bar, 200, 16);
lv_bar_set_range(bar, 0, 100);
lv_bar_set_value(bar, 70, LV_ANIM_ON);   /* 带动画填充到 70% */
/* 综合面板:标题 + 滑块 + 进度条 + 状态标签,纯 align 排布 */
lv_obj_t * title  = lv_label_create(scr);
lv_label_set_text(title, "Fan Speed");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16);

lv_obj_t * slider = lv_slider_create(scr);
lv_obj_set_size(slider, 200, 16);
lv_slider_set_range(slider, 0, 100);
lv_obj_align(slider, LV_ALIGN_TOP_MID, 0, 60);

lv_obj_t * bar = lv_bar_create(scr);
lv_obj_set_size(bar, 200, 16);
lv_bar_set_range(bar, 0, 100);
lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 100);

lv_obj_t * status = lv_label_create(scr);
lv_label_set_text(status, "Speed: 0%");
lv_obj_align(status, LV_ALIGN_TOP_MID, 0, 140);

/* 让滑块驱动进度条与状态文本(事件在第 8 章深入) */
lv_obj_add_event_cb(slider, fan_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
控件创建函数典型用法
进度条lv_bar_create()只读展示进度,lv_bar_set_value()
滑块lv_slider_create()可拖拽取值,lv_slider_get_value()
状态标签lv_label_create()lv_label_set_text_fmt() 实时刷新
狄仁杰提示

做一个多控件面板的通用流程:先规划布局(用 align 锚点 + 垂直偏移排成列/行),再逐个创建控件,最后用一个 LV_EVENT_VALUE_CHANGED 回调把"输入控件"连到"输出控件"。先摆得对,再连得上——顺序别反。

章末练习

练习 1:创建并摆放一个按钮 入门

在活跃屏幕上创建一个 160×48 的按钮,内部放一个居中的标签,文本为 "Press",整个按钮对齐到屏幕正中央。写出完整代码。

提示

回忆 5.3 的"按钮 + 居中标签"结构:lv_button_createlv_obj_set_sizelv_obj_center,再 lv_label_create 到按钮上并对齐。

参考答案

lv_obj_t * btn = lv_button_create(lv_screen_active()); lv_obj_set_size(btn, 160, 48); lv_obj_center(btn); lv_obj_t * lbl = lv_label_create(btn); lv_obj_set_align(lbl, LV_ALIGN_CENTER); lv_label_set_text(lbl, "Press");

练习 2:滑块与进度条联动 进阶

写一个小程序:一根滑块(范围 0–100)和一根进度条,拖动滑块时进度条实时同步到滑块的值,并用一个标签显示当前值。需要用到哪些 API?回调里如何拿到滑块当前值?

提示

参考 5.4 与 5.5 的代码:lv_slider_get_value 读值,lv_bar_set_value 设进度,lv_label_set_text_fmt 刷文本;事件用 LV_EVENT_VALUE_CHANGED

参考答案

回调 static void cb(lv_event_t * e) { lv_obj_t * s = lv_event_get_target_obj(e); int32_t v = lv_slider_get_value(s); lv_bar_set_value(bar, v, LV_ANIM_OFF); lv_label_set_text_fmt(status, "Value: %d", (int)v); },再 lv_obj_add_event_cb(slider, cb, LV_EVENT_VALUE_CHANGED, NULL)。注意 slider/bar/status 需定义为文件级变量供回调访问。

练习 3:复选框与开关组成设置面板 进阶

做一个两行的小设置面板:一行是复选框"启用通知"(默认勾选),一行是开关"夜间模式"(默认打开)。请写出创建代码,并说明:读取这两个控件当前状态分别该用什么 API?为什么它们的写法一致?

提示

勾选/打开都是 LV_STATE_CHECKED 状态位:设置用 lv_obj_add_state,读取用 lv_obj_has_state。复选框记得 lv_checkbox_set_text

参考答案

lv_obj_t * cb = lv_checkbox_create(scr); lv_checkbox_set_text(cb, "启用通知"); lv_obj_add_state(cb, LV_STATE_CHECKED);lv_obj_t * sw = lv_switch_create(scr); lv_obj_add_state(sw, LV_STATE_CHECKED);。读取统一用 lv_obj_has_state(x, LV_STATE_CHECKED)。因为二者都复用 LV_STATE_CHECKED 状态位,逻辑代码与控件类型解耦——这正是 5.4 强调的"换控件不改逻辑"。

练习 4:系统分析——三兄弟的异同 挑战

用本章的"系统分析"方法,对比 button、switch、checkbox 三个控件:它们各自在"被点击后改变什么"?哪些依赖 LV_STATE_CHECKED?为什么按钮通常不依赖它?参考 lvgl-master/examples/widgets/button/lv_example_button_states.cexamples/widgets/switch/,试着从源码层面找依据。

提示

思考三者的"点击语义":按钮是"瞬时动作",开关/复选框是"保持状态"。去 examples 里看 LV_STATE_CHECKED 被谁用到;button 例子里的 checked 态是怎么来的(通常要靠事件去 lv_obj_add_state,不是自带的)。

参考答案

开关与复选框天生"toggle":点一下翻转 LV_STATE_CHECKED,状态自持、可再点回来,所以它们内部就依赖这个状态位。按钮的语义是"瞬时触发"——按下释放一次完成动作,无需记住被按下;按钮出现"选中/保持"外观时(如工具栏 toggle 按钮),通常是开发者在 LV_EVENT_CLICKED 里自己调 lv_obj_add_state(btn, LV_STATE_CHECKED),而非内置。所以依赖 LV_STATE_CHECKED 的是"有持久状态"的控件;区分"瞬时动作 vs 持久状态",正是选择控件时的第一判断。