LVGL 入门第三课:按钮为什么没有文字?一文搞懂 `lv_button`
LVGL 入门第三课:按钮为什么没有文字?一文搞懂 lv_button
前言:按钮不是“创建出来就有文字”
学完 lv_obj 和 lv_label 之后,我们终于可以开始做一个真正能交互的控件了。
最常见的交互控件,就是按钮。
比如:
- 确认
- 取消
- 返回
- 开始
- 保存
- 设置
- 连接设备
在 LVGL 里,按钮对应的控件是:
lv_button
不过刚开始学按钮时,很多人会遇到一个问题:
我明明创建了一个按钮,为什么按钮上没有文字?
原因很简单:
按钮本身只是一个可以点击的对象。
它不会自动帮我们显示文字。
如果想让按钮显示文字,需要在按钮里面再创建一个 lv_label。
所以这一篇会把前两篇的内容串起来:
lv_obj:提供大小、位置、样式、事件等基础能力。lv_label:负责在按钮里面显示文字。lv_button:提供按钮语义和默认按钮行为。
这也是为什么前面一直强调:
LVGL 的核心是对象。
按钮不是孤立的控件。
它本质上还是一个对象,只是这个对象更适合用来处理点击交互。
一、lv_button 是什么?
lv_button 是 LVGL 里的按钮控件。
它本质上也是一个对象。
相比普通的 lv_obj,按钮默认更适合做点击类交互。
比如:
- 默认适合作为可点击控件。
- 可以响应按下、释放、点击、长按等事件。
- 可以通过状态设置按下样式。
- 可以作为可聚焦控件参与输入设备操作。
- 可以配合
LV_STATE_CHECKED做切换按钮。
不过要注意:
lv_button 并没有一套特别复杂的新 API。
它的大部分能力仍然来自 lv_obj。
比如设置大小:
lv_obj_set_size(btn, 120, 50);
设置位置:
lv_obj_center(btn);
设置背景色:
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2F91FF), LV_PART_MAIN);
绑定事件:
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
所以学习按钮时,不要把它看成一个完全陌生的新东西。
可以先这样理解:
lv_button = 一个更适合点击交互的 lv_obj。
二、创建第一个按钮
创建按钮使用这个函数:
lv_obj_t * lv_button_create(lv_obj_t * parent);
参数说明:
| 参数 | 说明 |
|---|---|
parent |
父对象,表示按钮要创建到哪个对象里面 |
返回值说明:
| 返回值 | 说明 |
|---|---|
lv_obj_t * |
新创建的按钮对象 |
最简单的按钮可以这样写:
void init_page(void)
{
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 120, 50);
lv_obj_center(btn);
}
运行后,可以看到一个按钮。
但这个按钮上没有文字。
这是正常的。
因为按钮只是按钮,文字需要我们自己加。
三、给按钮添加文字
按钮里面添加文字,本质上就是在按钮内部创建一个 label。
void init_page(void)
{
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 120, 50);
lv_obj_center(btn);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Button");
lv_obj_center(label);
}

这里最重要的是这一句:
lv_obj_t * label = lv_label_create(btn);
label 的父对象是 btn。
所以后面这句:
lv_obj_center(label);
表示让文字在按钮内部居中。
不是在整个屏幕居中。
按钮和文字之间的对象关系大概是这样:
screen
└── btn
└── label
如果把 label 创建到屏幕上:
lv_obj_t * label = lv_label_create(lv_screen_active());
那它就不是按钮的子对象了。
按钮移动时,文字也不会跟着按钮一起移动。
这就是很多新手做按钮文字时遇到错位的原因。
所以要记住:
按钮里的文字,父对象应该是按钮。
四、LVGL 9 里的 lv_button_create() 和旧教程里的 lv_btn_create()
如果你看一些旧版本教程,可能会看到这种写法:
lv_btn_create(parent);
本文基于 LVGL 9.2 来写,所以这里统一使用:
lv_button_create(parent);
在 LVGL 9 中,很多控件名称变得更完整。
比如:
btn -> button
img -> image
如果旧代码里用了 lv_btn_create(),在一些兼容配置下可能还能编译。
但新代码更建议直接使用:
lv_button_create()
这样后面查文档、读源码、看新版示例时会更一致。
五、按钮点击事件
按钮最核心的功能就是点击。
LVGL 中添加事件回调使用这个函数:
lv_obj_add_event_cb(obj, callback, event_code, user_data);
参数可以先这样理解:
| 参数 | 说明 |
|---|---|
obj |
要绑定事件的对象 |
callback |
事件回调函数 |
event_code |
事件类型 |
user_data |
传给回调函数的用户数据 |
按钮点击事件最常用的是:
LV_EVENT_CLICKED
完整例子如下:
#include <stdio.h>
#include <lvgl.h>
static void btn_event_cb(lv_event_t * e)
{
LV_UNUSED(e);
printf("button clicked\n");
}
void init_page(void)
{
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 120, 50);
lv_obj_center(btn);
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Click");
lv_obj_center(label);
}
点击按钮后,就会进入 btn_event_cb()。
如果当前环境支持串口或控制台输出,就能看到:
button clicked
这一段代码已经包含了按钮最核心的三件事:
创建按钮。
添加文字。
绑定点击事件。
六、事件回调里怎么知道是哪个按钮?
事件回调里可以通过下面这个函数拿到触发事件的对象:
lv_event_get_target(e)
例如:
static void btn_event_cb(lv_event_t * e)
{
lv_obj_t * btn = lv_event_get_target(e);
LV_UNUSED(btn);
printf("button clicked\n");
}
如果一个回调函数绑定给多个按钮,就可以通过 target 判断到底是谁触发了事件。
不过在实际开发中,更常见的是使用 user_data。
比如给不同按钮传不同字符串:
static void btn_event_cb(lv_event_t * e)
{
const char * name = lv_event_get_user_data(e);
printf("%s clicked\n", name);
}
void init_page(void)
{
lv_obj_t * ok_btn = lv_button_create(lv_screen_active());
lv_obj_align(ok_btn, LV_ALIGN_CENTER, 0, -40);
lv_obj_add_event_cb(ok_btn, btn_event_cb, LV_EVENT_CLICKED, "OK");
lv_obj_t * cancel_btn = lv_button_create(lv_screen_active());
lv_obj_align(cancel_btn, LV_ALIGN_CENTER, 0, 40);
lv_obj_add_event_cb(cancel_btn, btn_event_cb, LV_EVENT_CLICKED, "Cancel");
}

这样两个按钮可以共用同一个回调函数。
点击第一个按钮时打印:
OK clicked
点击第二个按钮时打印:
Cancel clicked
这里要注意一点:
传给 user_data 的数据,必须在事件发生时仍然有效。
像 "OK"、"Cancel" 这种字符串常量没有问题。
但如果传的是局部变量地址,就容易出问题。
七、按钮常见事件有哪些?
按钮不只有点击事件。
常见事件如下:
| 事件 | 触发时机 |
|---|---|
LV_EVENT_PRESSED |
按下瞬间 |
LV_EVENT_PRESSING |
按住过程中,会连续触发 |
LV_EVENT_RELEASED |
释放时 |
LV_EVENT_CLICKED |
完成一次点击 |
LV_EVENT_LONG_PRESSED |
长按 |
LV_EVENT_VALUE_CHANGED |
可切换按钮状态变化 |
刚开始学按钮,最常用的是:
LV_EVENT_CLICKED
如果想做长按功能,可以使用:
LV_EVENT_LONG_PRESSED
如果想做按住连续调节,比如长按加音量,可以考虑:
LV_EVENT_PRESSING
也可以监听所有事件,然后在回调里判断具体事件类型:
static void btn_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
printf("clicked\n");
}
}
void init_page(void)
{
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_center(btn);
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_ALL, NULL);
}
这里绑定的是:
LV_EVENT_ALL
表示所有事件都会进入回调。
所以回调里必须使用:
lv_event_get_code(e)
判断具体事件类型。
如果只关心点击,更建议直接绑定:
LV_EVENT_CLICKED
这样代码更清楚。
八、做一个点击计数按钮
现在做一个稍微实用一点的例子。
目标是:
- 创建一个按钮。
- 按钮里显示点击次数。
- 每点击一次,数字加 1。
代码如下:
#include <stdio.h>
#include <lvgl.h>
static int32_t click_count = 0;
static void count_btn_event_cb(lv_event_t * e)
{
lv_obj_t * label = lv_event_get_user_data(e);
click_count++;
lv_label_set_text_fmt(label, "Count: %" LV_PRId32, click_count);
}
void init_page(void)
{
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 150, 55);
lv_obj_center(btn);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Count: 0");
lv_obj_center(label);
lv_obj_add_event_cb(btn, count_btn_event_cb, LV_EVENT_CLICKED, label);
}

这里有一个关键点:
lv_obj_add_event_cb(btn, count_btn_event_cb, LV_EVENT_CLICKED, label);
最后一个参数传的是 label。
所以在回调里可以拿到这个 label:
lv_obj_t * label = lv_event_get_user_data(e);
然后更新文字:
lv_label_set_text_fmt(label, "Count: %" LV_PRId32, click_count);
这就是事件回调和 label 配合的一个典型用法。
按钮负责接收点击。
label 负责显示数字。
事件回调负责把两者连接起来。
九、按钮样式:默认、按下、禁用
按钮既然是对象,就可以使用 lv_obj_set_style_*() 设置样式。
比如默认状态下设置蓝色背景:
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2F91FF), LV_PART_MAIN);
设置圆角:
lv_obj_set_style_radius(btn, 8, LV_PART_MAIN);
设置文字颜色:
lv_obj_set_style_text_color(btn, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
这里的文字颜色设置到按钮上,按钮里面的 label 通常可以继承这个文字颜色。
如果想设置按下状态,可以组合 LV_STATE_PRESSED:
lv_obj_set_style_bg_color(btn,
lv_color_hex(0x1769C2),
LV_PART_MAIN | LV_STATE_PRESSED);
这句代码的意思是:
按钮主体在按下状态时,背景变成深蓝色。
完整例子:
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 130, 50);
lv_obj_center(btn);
lv_obj_set_style_radius(btn, 8, LV_PART_MAIN);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2F91FF), LV_PART_MAIN);
lv_obj_set_style_bg_color(btn,
lv_color_hex(0x1769C2),
LV_PART_MAIN | LV_STATE_PRESSED);
lv_obj_set_style_text_color(btn, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Save");
lv_obj_center(label);
运行时,按钮默认是亮蓝色,按下时变成深蓝色。
这就是 LVGL 状态样式的典型用法。
如果想让按钮进入禁用状态,可以使用:
lv_obj_add_state(btn, LV_STATE_DISABLED);
取消禁用状态则可以使用:
lv_obj_clear_state(btn, LV_STATE_DISABLED);
禁用状态也可以单独设置样式:
lv_obj_set_style_bg_color(btn,
lv_color_hex(0x6B7280),
LV_PART_MAIN | LV_STATE_DISABLED);
这样就可以让按钮在不可用时显示成灰色。
十、按钮为什么设置的是 LV_PART_MAIN?
设置按钮样式时,经常会看到:
LV_PART_MAIN
按钮的主要 part 就是:
| part | 说明 |
|---|---|
LV_PART_MAIN |
按钮主体背景 |
按钮不像滑条、开关那样有 LV_PART_INDICATOR、LV_PART_KNOB 这类明显的子区域。
所以按钮的大部分样式都设置在:
LV_PART_MAIN
比如:
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2F91FF), LV_PART_MAIN);
lv_obj_set_style_radius(btn, 8, LV_PART_MAIN);
lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(btn, 12, LV_PART_MAIN);
后面学滑条、开关时,再对比它们的 part,就会更容易理解。
可以简单记成:
按钮主要改 MAIN。
滑条、开关要区分 MAIN、INDICATOR、KNOB。
十一、可切换按钮:像开关一样的按钮
普通按钮点击一下就结束了。
但有时我们希望按钮有“选中/未选中”状态。
比如:
- Wi-Fi 开启/关闭
- 自动模式开启/关闭
- 静音开启/关闭
- 某个功能被选中/取消
这时可以给按钮添加:
LV_OBJ_FLAG_CHECKABLE
示例:
static void toggle_btn_event_cb(lv_event_t * e)
{
lv_obj_t * btn = lv_event_get_target(e);
lv_obj_t * label = lv_event_get_user_data(e);
if(lv_obj_has_state(btn, LV_STATE_CHECKED)) {
lv_label_set_text(label, "ON");
} else {
lv_label_set_text(label, "OFF");
}
}
void init_page(void)
{
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 120, 50);
lv_obj_center(btn);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CHECKABLE);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "OFF");
lv_obj_center(label);
lv_obj_add_event_cb(btn, toggle_btn_event_cb, LV_EVENT_VALUE_CHANGED, label);
}


这里有几个重点。
第一句:
lv_obj_add_flag(btn, LV_OBJ_FLAG_CHECKABLE);
表示按钮可以切换选中状态。
第二句:
LV_EVENT_VALUE_CHANGED
表示按钮状态发生变化。
第三句:
lv_obj_has_state(btn, LV_STATE_CHECKED)
表示判断按钮当前是否处于选中状态。
这样按钮就不只是“点击一下”,而是有了类似开关的状态。
十二、给 checked 状态设置样式
可切换按钮最常见的需求是:
未选中:灰色
选中:绿色
可以这样写:
lv_obj_set_style_bg_color(btn,
lv_color_hex(0x3A4B61),
LV_PART_MAIN);
lv_obj_set_style_bg_color(btn,
lv_color_hex(0x22C55E),
LV_PART_MAIN | LV_STATE_CHECKED);
完整例子:
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_set_size(btn, 120, 50);
lv_obj_center(btn);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CHECKABLE);
lv_obj_set_style_radius(btn, 8, LV_PART_MAIN);
lv_obj_set_style_bg_color(btn,
lv_color_hex(0x3A4B61),
LV_PART_MAIN);
lv_obj_set_style_bg_color(btn,
lv_color_hex(0x22C55E),
LV_PART_MAIN | LV_STATE_CHECKED);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Toggle");
lv_obj_center(label);
这个写法和前面按下状态非常像。
只是状态从:
LV_STATE_PRESSED
换成了:
LV_STATE_CHECKED
可以简单理解成:
PRESSED:正在按下。CHECKED:已经选中。
十三、按钮里的 label 为什么有时不居中?
理论上,下面这句代码就能让 label 在按钮里面居中:
lv_obj_center(label);
但有时看起来还是不居中。
常见原因有三个。
1. label 不是按钮的子对象
错误写法:
lv_obj_t * label = lv_label_create(lv_screen_active());
正确写法:
lv_obj_t * label = lv_label_create(btn);
只有 label 是按钮的子对象,lv_obj_center(label) 才表示在按钮内部居中。
2. 按钮使用了布局
如果按钮被设置了 Flex 或 Grid 布局,子对象的位置可能会由布局系统管理。
这时就不能只依赖:
lv_obj_center(label);
而要按照布局规则来设置。
3. 按钮 padding 不为 0
按钮默认可能有 padding。
如果想完全按自己的方式摆放,可以设置:
lv_obj_set_style_pad_all(btn, 0, LV_PART_MAIN);
不过普通按钮不建议一上来就去掉所有 padding。
因为 padding 可以让按钮文字和边缘之间保留合适距离,看起来不会太挤。
十四、用 lv_obj 做按钮可以吗?
可以。
前面第一篇其实已经讲过:
只要一个对象可以点击,它就可以做成按钮外观。
例如:
lv_obj_t * card_btn = lv_obj_create(lv_screen_active());
lv_obj_set_size(card_btn, 160, 70);
lv_obj_center(card_btn);
lv_obj_add_flag(card_btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(card_btn, btn_event_cb, LV_EVENT_CLICKED, NULL);
然后再设置背景、圆角、文字,就能做一个自定义卡片按钮。
那什么时候用 lv_button,什么时候用 lv_obj?
可以简单这样判断:
| 场景 | 建议 |
|---|---|
| 普通按钮、确认取消、表单按钮 | 用 lv_button |
| 卡片式入口、复杂组合控件、特殊外观 | 用 lv_obj 自定义 |
如果只是一个普通按钮,优先用 lv_button。
如果按钮其实是一个复杂卡片,比如:
左边图标 + 中间两行文字 + 右边箭头
这种用 lv_obj 自己组合也很合理。
很多嵌入式界面的按钮,并不一定长得像传统按钮。
它们可能更像卡片、列表项、功能入口。
这时用基础对象自己组合,反而更灵活。
十五、完整示例:两个按钮,一个普通点击,一个切换状态
最后写一个完整示例。
目标是:
- 第一个按钮:点击后计数加 1。
- 第二个按钮:点击后在 ON/OFF 之间切换。
- 两个按钮都带文字。
- 使用不同状态样式。
代码如下:
#include <stdio.h>
#include <lvgl.h>
static int32_t click_count = 0;
static void count_btn_event_cb(lv_event_t * e)
{
lv_obj_t * label = lv_event_get_user_data(e);
click_count++;
lv_label_set_text_fmt(label, "Count: %" LV_PRId32, click_count);
}
static void toggle_btn_event_cb(lv_event_t * e)
{
lv_obj_t * btn = lv_event_get_target(e);
lv_obj_t * label = lv_event_get_user_data(e);
if(lv_obj_has_state(btn, LV_STATE_CHECKED)) {
lv_label_set_text(label, "ON");
} else {
lv_label_set_text(label, "OFF");
}
}
static lv_obj_t * create_text_button(lv_obj_t * parent,
const char * text,
lv_obj_t ** label_out)
{
lv_obj_t * btn = lv_button_create(parent);
lv_obj_set_size(btn, 150, 55);
lv_obj_set_style_radius(btn, 8, LV_PART_MAIN);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2F91FF), LV_PART_MAIN);
lv_obj_set_style_bg_color(btn,
lv_color_hex(0x1769C2),
LV_PART_MAIN | LV_STATE_PRESSED);
lv_obj_set_style_text_color(btn, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, text);
lv_obj_center(label);
if(label_out != NULL) {
*label_out = label;
}
return btn;
}
void init_page(void)
{
lv_obj_t * label1 = NULL;
lv_obj_t * btn1 = create_text_button(lv_screen_active(),
"Count: 0",
&label1);
lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -45);
lv_obj_add_event_cb(btn1, count_btn_event_cb, LV_EVENT_CLICKED, label1);
lv_obj_t * label2 = NULL;
lv_obj_t * btn2 = create_text_button(lv_screen_active(),
"OFF",
&label2);
lv_obj_align(btn2, LV_ALIGN_CENTER, 0, 45);
lv_obj_add_flag(btn2, LV_OBJ_FLAG_CHECKABLE);
lv_obj_set_style_bg_color(btn2,
lv_color_hex(0x3A4B61),
LV_PART_MAIN);
lv_obj_set_style_bg_color(btn2,
lv_color_hex(0x22C55E),
LV_PART_MAIN | LV_STATE_CHECKED);
lv_obj_add_event_cb(btn2, toggle_btn_event_cb, LV_EVENT_VALUE_CHANGED, label2);
}

这段代码稍微长一点,但它把按钮常用知识点都串起来了:
- 创建按钮。
- 给按钮添加 label。
- 设置默认样式。
- 设置按下样式。
- 设置 checked 样式。
- 绑定点击事件。
- 使用 user data 传递 label。
- 用
LV_OBJ_FLAG_CHECKABLE做切换按钮。
这里没有直接通过 lv_obj_get_child(btn, 0) 获取 label。
而是在创建按钮时,通过 label_out 把 label 指针传出来。
这样代码更明确,也能避免后面按钮里子对象变多时取错对象。
十六、这一篇先记住什么?
这一篇把按钮的基础用法讲完了。
先记住下面几句话:
lv_button本质上也是对象。- 创建按钮用
lv_button_create()。 - 按钮本身不显示文字,需要在按钮里面创建
lv_label。 - 按钮文字的父对象应该是按钮,而不是屏幕。
- 点击事件常用
LV_EVENT_CLICKED。 - 可切换按钮需要添加
LV_OBJ_FLAG_CHECKABLE。 - 切换状态判断用
LV_STATE_CHECKED。 - 按下样式用
LV_STATE_PRESSED。 - 按钮大部分样式设置在
LV_PART_MAIN。 - 普通按钮用
lv_button,复杂卡片按钮可以用lv_obj自定义。
到这里,lv_obj、lv_label、lv_button 三个最基础的内容已经连起来了。
后面学习开关、滑条、圆弧时,本质上还是这几件事:
创建对象。
设置样式。
添加子对象。
绑定事件。
判断状态。
下一篇继续讲 lv_switch。
也就是 LVGL 里的开关控件。
更多推荐

所有评论(0)