LVGL 入门第三课:按钮为什么没有文字?一文搞懂 lv_button

前言:按钮不是“创建出来就有文字”

学完 lv_objlv_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_INDICATORLV_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 指针传出来。

这样代码更明确,也能避免后面按钮里子对象变多时取错对象。


十六、这一篇先记住什么?

这一篇把按钮的基础用法讲完了。

先记住下面几句话:

  1. lv_button 本质上也是对象。
  2. 创建按钮用 lv_button_create()
  3. 按钮本身不显示文字,需要在按钮里面创建 lv_label
  4. 按钮文字的父对象应该是按钮,而不是屏幕。
  5. 点击事件常用 LV_EVENT_CLICKED
  6. 可切换按钮需要添加 LV_OBJ_FLAG_CHECKABLE
  7. 切换状态判断用 LV_STATE_CHECKED
  8. 按下样式用 LV_STATE_PRESSED
  9. 按钮大部分样式设置在 LV_PART_MAIN
  10. 普通按钮用 lv_button,复杂卡片按钮可以用 lv_obj 自定义。

到这里,lv_objlv_labellv_button 三个最基础的内容已经连起来了。

后面学习开关、滑条、圆弧时,本质上还是这几件事:

创建对象。
设置样式。
添加子对象。
绑定事件。
判断状态。

下一篇继续讲 lv_switch

也就是 LVGL 里的开关控件。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐