4. C语言与数据结构强化

好,咱们今天来啃 LVGL 的基础控件。说实话,这部分是嵌入式 GUI 的「砖瓦」—— 你后面所有复杂的界面,都是靠这些基础控件搭起来的。我个人习惯先把 Label、Button、Checkbox、Slider 这四个玩透,再去看别的。

为什么?因为它们覆盖了三种最核心的交互:显示文本、触发动作、状态切换与数值调节。你想想看,一个设置界面,是不是就这几样东西来回组合?

本章核心脉络:先掌握每个控件的创建与基本用法,再理解样式如何「叠加」到控件上,最后搞懂事件回调是怎么被触发的。这三层打通了,LVGL 的控件体系你就入门了。

LVGL 基础控件知识体系 LVGL 基础控件知识体系 基础控件 Label(标签) Button(按钮) Checkbox(复选框) Slider(滑块) 控件属性与样式设置 事件回调机制 样式状态管理 💡 学习路径:先创建控件 → 设置属性 → 配置样式 → 绑定事件 每个控件都遵循「创建 → 配置 → 注册回调」三步走

4.1 Label(标签)—— 最基础的显示单元

Label 说白了就是用来显示文字的。但你别小看它,我在项目里见过有人把 Label 当「调试输出窗口」用,结果内存爆了 —— 因为没限制文本长度。

创建 Label 很简单:

/* 创建一个标签 */
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello, LVGL!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

嗯,这里要注意几个关键属性:

  • 文本设置:lv_label_set_text() 会复制字符串,频繁更新时建议用 lv_label_set_text_fmt() 格式化。
  • 长文本处理:默认不换行。需要调用 lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP) 开启自动换行。
  • 文本颜色:通过样式设置,后面会细讲。

我的经验:调试时我喜欢在 Label 里显示系统运行时间或帧率,但记得加个最大长度限制,否则字符串拼接会慢慢吃掉堆内存。

4.2 Button(按钮)—— 触发交互的入口

Button 是用户最常「点」的东西。创建按钮本身不难,难的是让它「好看」且「反馈及时」。

/* 创建按钮 */
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 120, 50);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);

/* 在按钮上添加文本 */
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "点击我");
lv_obj_center(label);

Button 有几个状态:LV_STATE_DEFAULT、LV_STATE_PRESSED、LV_STATE_DISABLED 等。每个状态都可以单独设置样式,这就是后面要讲的「样式状态管理」。

避坑指南:我曾经在按钮上直接设置 lv_obj_set_style_bg_color(btn, lv_color_hex(0xFF0000), LV_STATE_DEFAULT),结果发现按下时颜色没变 —— 因为没给 LV_STATE_PRESSED 单独设置样式。记住:每个状态都要单独配样式,否则会继承默认状态。

4.3 Checkbox(复选框)—— 状态切换的利器

Checkbox 比 Button 多了一个「选中状态」。它内部其实是一个小方块 + 一个 Label 的组合。

/* 创建复选框 */
lv_obj_t *cb = lv_checkbox_create(lv_scr_act());
lv_checkbox_set_text(cb, "启用自动调节");
lv_obj_align(cb, LV_ALIGN_CENTER, 0, 0);

/* 设置初始状态为选中 */
lv_obj_add_state(cb, LV_STATE_CHECKED);

Checkbox 的状态判断也很直接:

/* 判断是否选中 */
bool is_checked = lv_obj_has_state(cb, LV_STATE_CHECKED);

我个人习惯把 Checkbox 用于「设置页」的开关选项,比如「开启夜间模式」、「启用日志输出」这类二值选择。

4.4 Slider(滑块)—— 连续数值调节

Slider 是调节数值的直观控件,比如亮度、音量、进度条。它本质上是一个可拖动的条。

/* 创建滑块 */
lv_obj_t *slider = lv_slider_create(lv_scr_act());
lv_obj_set_size(slider, 200, 20);
lv_obj_align(slider, LV_ALIGN_CENTER, 0, 0);

/* 设置范围 */
lv_slider_set_range(slider, 0, 100);
lv_slider_set_value(slider, 50, LV_ANIM_ON);

Slider 有几个关键属性:

属性 说明 常用函数
范围 最小值到最大值 lv_slider_set_range()
当前值 滑块所在位置的值 lv_slider_set_value()
步进 拖动时的最小变化单位 lv_slider_set_step()
动画 设置值时是否平滑移动 第三个参数传 LV_ANIM_ON/OFF

我的经验:Slider 的步进值别设太大,否则用户拖动时会感觉「一跳一跳」的。我一般设 1 或 5,具体看你的精度需求。

4.5 控件属性与样式设置

样式是 LVGL 的灵魂。每个控件都可以通过 lv_obj_set_style_* 系列函数来设置外观。

样式设置的核心逻辑是:属性 + 状态 + 优先级。

/* 设置按钮默认背景色为蓝色 */
lv_obj_set_style_bg_color(btn, lv_color_hex(0x3498db), LV_STATE_DEFAULT);

/* 设置按钮按下时背景色为深蓝色 */
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2980b9), LV_STATE_PRESSED);

/* 设置圆角 */
lv_obj_set_style_radius(btn, 8, LV_STATE_DEFAULT);

/* 设置边框 */
lv_obj_set_style_border_width(btn, 2, LV_STATE_DEFAULT);
lv_obj_set_style_border_color(btn, lv_color_hex(0x2c3e50), LV_STATE_DEFAULT);

常用的样式属性包括:

  • 背景:bg_color、bg_opa(透明度)、bg_grad_color(渐变色)
  • 边框:border_width、border_color、border_side(哪一侧有边框)
  • 文本:text_color、text_font、text_align
  • 尺寸与位置:width、height、pad_all(内边距)

核心原则:样式是「叠加」的。子控件会继承父控件的样式,但子控件自己的样式优先级更高。如果你发现某个控件的颜色不对,先检查它有没有被父控件的样式「覆盖」了。

4.6 事件回调机制

事件是用户交互的「桥梁」。没有事件,控件就是一堆静态图形。

LVGL 的事件机制是 冒泡式 的:事件从触发控件开始,逐级向上传递,直到被处理或到达根对象。

/* 定义事件回调函数 */
static void btn_event_cb(lv_event_t *e) {
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t *target = lv_event_get_target(e);

    if (code == LV_EVENT_CLICKED) {
        LV_LOG_USER("按钮被点击了!");
        /* 在这里处理点击逻辑 */
    } else if (code == LV_EVENT_PRESSING) {
        /* 按住不放时持续触发 */
    } else if (code == LV_EVENT_VALUE_CHANGED) {
        /* 值改变时触发(Slider、Checkbox 常用) */
    }
}

/* 绑定事件 */
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);

常用的事件类型:

事件代码 触发时机 适用控件
LV_EVENT_CLICKED 点击后释放 Button
LV_EVENT_PRESSED 按下瞬间 Button、Slider
LV_EVENT_VALUE_CHANGED 值发生变化 Slider、Checkbox
LV_EVENT_LONG_PRESSED 长按超过阈值 Button

避坑指南:我曾经在事件回调里直接调用 lv_obj_del() 删除触发事件的控件,结果程序直接崩溃。为什么?因为事件处理完后 LVGL 还要访问那个对象。正确的做法是用 lv_event_send() 延迟删除,或者用 lv_async_call() 把删除操作放到下一帧执行。

对于 Slider 和 Checkbox,事件回调里获取当前值的方法:

static void slider_event_cb(lv_event_t *e) {
    lv_obj_t *slider = lv_event_get_target(e);
    int32_t value = lv_slider_get_value(slider);
    LV_LOG_USER("当前滑块值: %d", value);
}

static void checkbox_event_cb(lv_event_t *e) {
    lv_obj_t *cb = lv_event_get_target(e);
    bool checked = lv_obj_has_state(cb, LV_STATE_CHECKED);
    LV_LOG_USER("复选框状态: %s", checked ? "选中" : "未选中");
}

嗯,到这里基础控件的基本用法就讲完了。你可能会问:「这些控件怎么组合成一个完整的界面?」别急,下一节我们会讲布局和容器,到时候你就知道怎么把它们「装」在一起了。

最后送你一句话:控件是死的,样式是活的,事件是灵魂。把这三点吃透,你就能做出任何想要的界面。