LVGL4(一个物联网界面)

1 界面设计

一直说多学学LVGL,所以打算试着做一个简单的物联网的界面UI。分为两个部分,一部分是控制,一部分是显示。

模拟使用2寸以内的LCD。包含控制和显示两个部分。控制包括启动,停止,传感器选择(下拉选择),采样间隔(秒)。显示包括当前数值,列表显示最近6个时间周期的,然后一个折线图,也是显示最近6个

2 代码

简单按照设计用了一下,因为我以前对MFC比较熟悉,所以这些GUI的流程还是知道一点。LVGL目前基本是直接设置像素距离,虽然有类似响应式布局,但是好像也用处不大。

因为到了LVGL这一层资源很受限的UI,基本就是性能比较弱的低成本终端,一个型号对应一个界面,就算换屏估计一般分辨率也不会换。如果资源更充足,直接就上Android了。适配多种屏也有很完善的方案。

就算不同分辨率的屏,调一下LVGL的坐标也是一天两天的事情。。。

代码:

cpp 复制代码
static lv_obj_t *label_current;
static lv_obj_t *table_history;
static lv_obj_t *chart;
static lv_chart_series_t *ser;
static bool running = false;
#define MAX_POINTS 3
static float data_points[MAX_POINTS] = {0};

static void start_cb(lv_event_t *e)
{
    running = true;
}

static void stop_cb(lv_event_t *e)
{
    running = false;
}

static int sample_interval = 2000; // 默认2秒

static void sensor_changed_cb(lv_event_t *e)
{
  char buf[64];
  lv_dropdown_get_selected_str(lv_event_get_target(e), buf, sizeof(buf));
  //printf("Sensor: %s\n", buf);
}

static void interval_changed_cb(lv_event_t *e)
{
    int id = lv_dropdown_get_selected(lv_event_get_target(e));
    int intervals[] = {1000, 2000, 5000, 10000};
    sample_interval = intervals[id];
    //printf("采样间隔: %d ms\n", sample_interval);
}

static void update_timer_cb(lv_timer_t *timer)
{
    if (!running) return;

    time_t now = time(NULL);
    struct tm *t = localtime(&now);
    char time_buf[16];
    strftime(time_buf, sizeof(time_buf), "%H:%M:%S", t);

    float val = (float)(rand() % 1000) / 10.0f; // 模拟随机值

    // 移动数据
    for (int i = 0; i < MAX_POINTS - 1; i++)
        data_points[i] = data_points[i + 1];
    data_points[MAX_POINTS - 1] = val;

    // 更新显示
    lv_label_set_text_fmt(label_current, "Current: %.1f", val);

    for (int i = 0; i < MAX_POINTS; i++) {
        char vbuf[16];
        snprintf(vbuf, sizeof(vbuf), "%.1f", data_points[i]);
        lv_table_set_cell_value_fmt(table_history, i + 1, 0, "%d", i + 1);
        lv_table_set_cell_value(table_history, i + 1, 1, vbuf);
        lv_chart_set_next_value(chart, ser, data_points[i]);
    }

    // 更新图表
    //lv_chart_set_all_value(chart, ser, data_points);
}

void lv_example_iot_1(void)
{
  static lv_style_t style_title;
  lv_style_init(&style_title);

  // 2. 设置字体(使用LVGL内置的大字体,或自定义字体)
  // 内置字体选项:LV_FONT_DEFAULT(默认)、LV_FONT_MONTSERRAT_16/20/24/32等(数字越大字体越大)
  lv_style_set_text_font(&style_title, &lv_font_montserrat_24); // 例如使用24号字体

  // 3. 创建标题标签并应用样式
  lv_obj_t *label = lv_label_create(lv_scr_act());
  lv_label_set_text(label, "IO Monitor");
  lv_obj_add_style(label, &style_title, 0); // 应用样式到标签

  // 4. 对齐到屏幕上方中间
  lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 10); // 下移10像素避免贴边


  //lv_obj_t *btn1 = lv_obj_create(lv_scr_act());
  lv_obj_t * btn1 = lv_button_create(lv_screen_active());
  lv_obj_set_pos(btn1, 40, 40);
  lv_obj_set_size(btn1, 90, 52);
  lv_obj_t *btn1_label = lv_label_create(btn1);
  lv_label_set_text(btn1_label, "START");
  lv_obj_align(btn1_label, LV_ALIGN_CENTER, 0, 0);
  lv_obj_add_event_cb(btn1, start_cb, LV_EVENT_CLICKED, NULL);


  //lv_obj_t *btn2 = lv_obj_create(lv_scr_act());
  lv_obj_t * btn2 = lv_button_create(lv_screen_active());
  lv_obj_set_style_bg_color(btn2, lv_color_hex(0xFF0000), LV_PART_MAIN);
  lv_obj_set_pos(btn2, 150, 40);
  lv_obj_set_size(btn2, 90, 52);
  lv_obj_t *btn2_label = lv_label_create(btn2);
  lv_label_set_text(btn2_label, "STOP");
  lv_obj_align(btn2_label, LV_ALIGN_CENTER, 0, 0);
  lv_obj_add_event_cb(btn2, stop_cb, LV_EVENT_CLICKED, NULL);

    // 传感器选择
    lv_obj_t * sensor_label = lv_label_create(lv_scr_act());
    lv_obj_set_pos(sensor_label, 380, 60);
    lv_label_set_text(sensor_label, "Sensor:");

    lv_obj_t *dd_sensor = lv_dropdown_create(lv_scr_act());
    lv_obj_set_pos(dd_sensor, 460, 50);
    lv_dropdown_set_options(dd_sensor, "Temperature\nHumidity\nAtmoPressure");
    lv_obj_add_event_cb(dd_sensor, sensor_changed_cb, LV_EVENT_VALUE_CHANGED, NULL);

    // 采样间隔选择
    lv_obj_t * interval_label = lv_label_create(lv_scr_act());
    lv_obj_set_pos(interval_label, 380, 110);
    lv_label_set_text(interval_label, "Interval:");

    lv_obj_t *dd_interval = lv_dropdown_create(lv_scr_act());
    lv_obj_set_pos(dd_interval, 460, 100);
    lv_dropdown_set_options(dd_interval, "1 Sec\n2 sec\n5 sec\n10 sec");
    lv_obj_add_event_cb(dd_interval, interval_changed_cb, LV_EVENT_VALUE_CHANGED, NULL);


    // 当前值
    label_current = lv_label_create(lv_scr_act());
    lv_obj_set_pos(label_current, 40, 180);
    lv_label_set_text(label_current, "Current Value: --");

    // 历史表格
    table_history = lv_table_create(lv_scr_act());
    lv_obj_set_pos(table_history, 40, 200);
    lv_table_set_col_cnt(table_history, 2);
    lv_table_set_row_cnt(table_history, 3 + 1);
    lv_table_set_cell_value(table_history, 0, 0, "Time");
    lv_table_set_cell_value(table_history, 0, 1, "Value");

    // 折线图
    chart = lv_chart_create(lv_scr_act());
    lv_obj_set_pos(chart, 340, 200);
    lv_obj_set_size(chart, LV_PCT(40), 200);
    lv_chart_set_type(chart, LV_CHART_TYPE_LINE);
    ser = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_BLUE), LV_CHART_AXIS_PRIMARY_Y);

    lv_timer_create(update_timer_cb, sample_interval, NULL);
}

最后的效果。看着还行还行。。。

代码

#include "lvgl.h"

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

static lv_obj_t *label_current;

static lv_obj_t *table_history;

static lv_obj_t *chart;

static lv_chart_series_t *ser;

static int sample_interval = 2000; // 默认2秒

static bool running = false;

#define MAX_POINTS 6

static float data_points[MAX_POINTS] = {0};

static void start_cb(lv_event_t *e);

static void stop_cb(lv_event_t *e);

static void update_timer_cb(lv_timer_t *timer);

static void sensor_changed_cb(lv_event_t *e);

static void interval_changed_cb(lv_event_t *e);

void ui_create_main_screen(void)

{

lv_obj_t *scr = lv_obj_create(NULL);

lv_scr_load(scr);

lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);

lv_obj_set_size(scr, LV_HOR_RES, LV_VER_RES);

/*** 控制区 ***/

lv_obj_t *ctrl = lv_obj_create(scr);

lv_obj_set_size(ctrl, LV_PCT(100), LV_SIZE_CONTENT);

lv_obj_set_flex_flow(ctrl, LV_FLEX_FLOW_ROW);

lv_obj_set_style_pad_all(ctrl, 5, 0);

// 启动按钮

lv_obj_t *btn_start = lv_btn_create(ctrl);

lv_obj_t *lbl1 = lv_label_create(btn_start);

lv_label_set_text(lbl1, "启动");

lv_obj_center(lbl1);

lv_obj_add_event_cb(btn_start, start_cb, LV_EVENT_CLICKED, NULL);

// 停止按钮

lv_obj_t *btn_stop = lv_btn_create(ctrl);

lv_obj_t *lbl2 = lv_label_create(btn_stop);

lv_label_set_text(lbl2, "停止");

lv_obj_center(lbl2);

lv_obj_add_event_cb(btn_stop, stop_cb, LV_EVENT_CLICKED, NULL);

// 传感器选择

lv_obj_t *dd_sensor = lv_dropdown_create(ctrl);

lv_dropdown_set_options(dd_sensor, "温度\n湿度\n气压");

lv_obj_add_event_cb(dd_sensor, sensor_changed_cb, LV_EVENT_VALUE_CHANGED, NULL);

// 采样间隔选择

lv_obj_t *dd_interval = lv_dropdown_create(ctrl);

lv_dropdown_set_options(dd_interval, "1秒\n2秒\n5秒\n10秒");

lv_obj_add_event_cb(dd_interval, interval_changed_cb, LV_EVENT_VALUE_CHANGED, NULL);

/*** 显示区 ***/

lv_obj_t *disp = lv_obj_create(scr);

lv_obj_set_size(disp, LV_PCT(100), LV_SIZE_CONTENT);

lv_obj_set_flex_flow(disp, LV_FLEX_FLOW_COLUMN);

lv_obj_set_style_pad_all(disp, 5, 0);

// 当前值

label_current = lv_label_create(disp);

lv_label_set_text(label_current, "当前值: --");

// 历史表格

table_history = lv_table_create(disp);

lv_table_set_col_cnt(table_history, 2);

lv_table_set_row_cnt(table_history, MAX_POINTS + 1);

lv_table_set_cell_value(table_history, 0, 0, "时间");

lv_table_set_cell_value(table_history, 0, 1, "数值");

// 折线图

chart = lv_chart_create(scr);

lv_obj_set_size(chart, LV_PCT(100), 120);

lv_chart_set_type(chart, LV_CHART_TYPE_LINE);

ser = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_BLUE), LV_CHART_AXIS_PRIMARY_Y);

lv_timer_create(update_timer_cb, sample_interval, NULL);

}

static void start_cb(lv_event_t *e)

{

running = true;

}

static void stop_cb(lv_event_t *e)

{

running = false;

}

static void sensor_changed_cb(lv_event_t *e)

{

const char *txt = lv_dropdown_get_selected_str(lv_event_get_target(e));

printf("选择传感器: %s\n", txt);

}

static void interval_changed_cb(lv_event_t *e)

{

int id = lv_dropdown_get_selected(lv_event_get_target(e));

int intervals[] = {1000, 2000, 5000, 10000};

sample_interval = intervals[id];

printf("采样间隔: %d ms\n", sample_interval);

}

static void update_timer_cb(lv_timer_t *timer)

{

if (!running) return;

time_t now = time(NULL);

struct tm *t = localtime(&now);

char time_buf[16];

strftime(time_buf, sizeof(time_buf), "%H:%M:%S", t);

float val = (float)(rand() % 1000) / 10.0f; // 模拟随机值

// 移动数据

for (int i = 0; i < MAX_POINTS - 1; i++)

data_points[i] = data_points[i + 1];

data_points[MAX_POINTS - 1] = val;

// 更新显示

lv_label_set_text_fmt(label_current, "当前值: %.1f", val);

for (int i = 0; i < MAX_POINTS; i++) {

char vbuf[16];

snprintf(vbuf, sizeof(vbuf), "%.1f", data_points[i]);

lv_table_set_cell_value_fmt(table_history, i + 1, 0, "%d", i + 1);

lv_table_set_cell_value(table_history, i + 1, 1, vbuf);

}

// 更新图表

lv_chart_set_all_value(chart, ser, data_points);

}

3 LVGL初步小结

整体:

项目 LVGL Android MFC
主要语言 C Java / Kotlin / C++ (NDK) C++
框架设计思想 手动创建对象(面向过程 + 简单对象封装) 面向对象 + 组件化 + XML布局 面向对象 + 事件驱动
编程方式 直接调用API创建控件,如 lv_button_create() XML定义UI + Java逻辑 Visual Studio拖拽控件 + C++绑定消息
学习难度 ⭐⭐(偏底层) ⭐⭐⭐⭐(体系庞大) ⭐⭐⭐(较老旧)

界面布局:

项目 LVGL Android MFC
布局方式 手动指定坐标 / Flex 布局 ConstraintLayout / LinearLayout 等自动布局 绝对坐标或对话框布局
自适应能力 中等(可通过百分比或Flex布局实现) 很强(多分辨率自动适配) 弱(需要手动调整窗口大小)

事件处理:

项目 LVGL Android MFC
事件模型 统一事件系统(按键、触摸、值变化等) 事件回调 + 消息分发(Listener / ViewModel) 消息映射宏(ON_COMMAND、WM_XXX)
写法举例 lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, NULL) button.setOnClickListener(new View.OnClickListener {...}) ON_BN_CLICKED(IDC_BUTTON1, OnButtonClick)
简洁度 ⭐⭐⭐⭐(统一接口) ⭐⭐⭐(面向对象但冗长) ⭐⭐(宏多且笨重)

线程机制:

框架 UI线程与事件线程 调用规则
LVGL 同一个线程(主循环) 所有 UI 操作必须在该线程中执行
Android 主线程(UI Thread)处理所有 UI;后台线程可用 HandlerrunOnUiThread 更新 严格线程限制
MFC UI 和消息循环在主线程;后台线程可通过 PostMessage 通信 线程安全靠消息机制
相关推荐
Hare_bai14 小时前
WPF的MVVM模式核心架构与实现细节
ui·架构·c#·wpf·交互·xaml·mvvm
张人玉15 小时前
WPF 静态样式与动态样式的定义及使用详解
ui·c#·wpf
charlie11451419116 小时前
从模仿到掌握:尝试一下Native CSS手写一个好看的按钮
前端·css·学习·ui
CodeCraft Studio16 小时前
MPP文件处理组件Aspose.Tasks教程:使用Python在Excel中打开MPP文件
python·ui·excel·csv·mpp·aspose·ms project
东木君_19 小时前
芯外拾遗第二篇:编译、工具链、烧录,你真的搞懂了吗?
linux·单片机·操作系统·嵌入式
咚璟19 小时前
博客内容目录
嵌入式
Larry_Yanan20 小时前
QML学习笔记(四十七)QML与C++交互:上下文对象
c++·笔记·qt·学习·ui
芝麻开门-新起点21 小时前
Flutter 网络通信协议:从原理到实战,选对协议让 APP 飞起来
flutter·ui·性能优化
十里-1 天前
在 Vue2 中为 Element-UI 的 el-dialog 添加拖拽功能
前端·vue.js·ui