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;后台线程可用 Handler或runOnUiThread更新 | 严格线程限制 | 
| MFC | UI 和消息循环在主线程;后台线程可通过 PostMessage通信 | 线程安全靠消息机制 |