[LVGL] 从0开始,学LVGL:进阶应用与项目实战(上)
第4部分:进阶应用与项目实战(上)
文章目录
- **第4部分:进阶应用与项目实战(上)**
- **第11章:数据展示三剑客 - 图表、列表、滚轮**
- **11.1 图表:数据的可视化艺术**
- **11.1.1 图表的基本数学模型**
- **11.1.2 创建实时数据监控图表**
- **11.1.3 高级图表特性**
- **11.2 列表:结构化数据的展示**
- **11.2.1 列表的数学模型**
- **11.2.2 创建功能完整的消息列表**
- **11.3 滚轮:优雅的选择器**
- **11.3.1 滚轮的物理模型**
- **11.3.2 创建日期时间选择器**
- **第12章:图片与字体**
- **12.1 图片:视觉元素的载体**
- **12.1.1 图片的数学表示**
- **12.1.2 图片存储格式对比**
- **12.1.3 实战:创建图片库浏览器**
- **12.2 字体:文字渲染的艺术**
- **12.2.1 字体的数学基础**
- **12.2.2 使用外部字体**
- **12.2.3 自定义字体生成**
- **本章总结与挑战**
第11章:数据展示三剑客 - 图表、列表、滚轮
11.1 图表:数据的可视化艺术
在现代GUI应用中,数据可视化至关重要。LVGL提供了强大的图表控件,能够将抽象数据转化为直观的图形。
11.1.1 图表的基本数学模型
图表的核心是将数据点映射到屏幕坐标:
设数据序列 D={(x1,y1),(x2,y2),...,(xn,yn)}D = \{(x_1, y_1), (x_2, y_2), ..., (x_n, y_n)\}D={(x1,y1),(x2,y2),...,(xn,yn)},图表区域尺寸为 (W,H)(W, H)(W,H),值域为 [ymin,ymax][y_{min}, y_{max}][ymin,ymax]。
则每个数据点的屏幕坐标 (Xi,Yi)(X_i, Y_i)(Xi,Yi) 计算为:
Xi=xi−xminxmax−xmin×WX_i = \frac{x_i - x_{min}}{x_{max} - x_{min}} \times WXi=xmax−xminxi−xmin×W
Yi=H−yi−yminymax−ymin×HY_i = H - \frac{y_i - y_{min}}{y_{max} - y_{min}} \times HYi=H−ymax−yminyi−ymin×H
11.1.2 创建实时数据监控图表
让我们创建一个传感器数据实时监控界面:
#include <lvgl.h>
#include <stdlib.h>
#include <time.h>// 图表数据结构
typedef struct {lv_obj_t * chart;lv_chart_series_t * temp_series;lv_chart_series_t * humi_series;lv_chart_series_t * press_series;uint32_t data_count;
} chart_manager_t;static chart_manager_t chart_mgr;/*** 创建多系列图表*/
void create_multi_series_chart() {lv_obj_t * parent = lv_scr_act();// 创建图表容器lv_obj_t * container = lv_obj_create(parent);lv_obj_set_size(container, 450, 300);lv_obj_align(container, LV_ALIGN_TOP_LEFT, 20, 20);lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);lv_obj_set_style_pad_all(container, 15, 0);// 标题lv_obj_t * title = lv_label_create(container);lv_label_set_text(title, "📊 环境传感器数据");lv_obj_set_style_text_font(title, &lv_font_montserrat_18, 0);// 创建图表对象chart_mgr.chart = lv_chart_create(container);lv_obj_set_size(chart_mgr.chart, LV_PCT(100), LV_PCT(80));// 配置图表基本属性lv_chart_set_type(chart_mgr.chart, LV_CHART_TYPE_LINE);lv_chart_set_range(chart_mgr.chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);lv_chart_set_point_count(chart_mgr.chart, 50); // 显示50个数据点lv_chart_set_div_line_count(chart_mgr.chart, 5, 5); // 网格线// 启用滚动模式 - 新数据从右侧进入lv_chart_set_zoom_x(chart_mgr.chart, 256); // 无缩放lv_chart_set_update_mode(chart_mgr.chart, LV_CHART_UPDATE_MODE_SHIFT);// 添加坐标轴标签lv_obj_t * x_axis_label = lv_label_create(container);lv_label_set_text(x_axis_label, "时间序列");lv_obj_set_style_text_align(x_axis_label, LV_TEXT_ALIGN_CENTER, 0);lv_obj_set_width(x_axis_label, LV_PCT(100));// 创建数据系列chart_mgr.temp_series = lv_chart_add_series(chart_mgr.chart, lv_color_hex(0xFF6B6B), LV_CHART_AXIS_PRIMARY_Y);chart_mgr.humi_series = lv_chart_add_series(chart_mgr.chart, lv_color_hex(0x4ECDC4), LV_CHART_AXIS_PRIMARY_Y);chart_mgr.press_series = lv_chart_add_series(chart_mgr.chart, lv_color_hex(0x45B7D1), LV_CHART_AXIS_PRIMARY_Y);// 初始化一些随机数据srand(time(NULL));for(int i = 0; i < 10; i++) {lv_chart_set_next_value(chart_mgr.chart, chart_mgr.temp_series, 20 + rand() % 10);lv_chart_set_next_value(chart_mgr.chart, chart_mgr.humi_series, 40 + rand() % 30);lv_chart_set_next_value(chart_mgr.chart, chart_mgr.press_series, 60 + rand() % 20);chart_mgr.data_count++;}// 创建图例create_chart_legend(container);// 启动数据更新定时器lv_timer_t * update_timer = lv_timer_create(update_chart_data, 1000, NULL);lv_timer_set_repeat_count(update_timer, 0); // 无限重复
}/*** 创建图表图例*/
void create_chart_legend(lv_obj_t * parent) {lv_obj_t * legend = lv_obj_create(parent);lv_obj_set_size(legend, LV_PCT(100), 30);lv_obj_set_style_bg_opa(legend, LV_OPA_0, 0);lv_obj_set_style_border_width(legend, 0, 0);lv_obj_set_flex_flow(legend, LV_FLEX_FLOW_ROW);lv_obj_set_flex_align(legend, LV_FLEX_ALIGN_SPACE_AROUND, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);// 温度图例lv_obj_t * temp_legend = create_legend_item(legend, "温度", lv_color_hex(0xFF6B6B));lv_obj_t * humi_legend = create_legend_item(legend, "湿度", lv_color_hex(0x4ECDC4));lv_obj_t * press_legend = create_legend_item(legend, "压力", lv_color_hex(0x45B7D1));
}/*** 创建单个图例项*/
lv_obj_t * create_legend_item(lv_obj_t * parent, const char * text, lv_color_t color) {lv_obj_t * container = lv_obj_create(parent);lv_obj_set_size(container, LV_SIZE_CONTENT, LV_SIZE_CONTENT);lv_obj_set_style_bg_opa(container, LV_OPA_0, 0);lv_obj_set_style_border_width(container, 0, 0);lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);lv_obj_set_flex_align(container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);// 颜色标识lv_obj_t * color_indicator = lv_obj_create(container);lv_obj_set_size(color_indicator, 12, 12);lv_obj_set_style_bg_color(color_indicator, color, 0);lv_obj_set_style_radius(color_indicator, 6, 0);// 文本标签lv_obj_t * label = lv_label_create(container);lv_label_set_text(label, text);lv_obj_set_style_text_font(label, &lv_font_montserrat_12, 0);return container;
}/*** 更新图表数据 - 模拟传感器数据*/
void update_chart_data(lv_timer_t * timer) {// 模拟传感器读数int temp = 20 + rand() % 10 + (rand() % 10 - 5) * 0.1; // 20-30°C,带小数波动int humi = 40 + rand() % 30 + (rand() % 10 - 5) * 0.1; // 40-70%int press = 60 + rand() % 20 + (rand() % 10 - 5) * 0.1; // 60-80%// 添加新数据点lv_chart_set_next_value(chart_mgr.chart, chart_mgr.temp_series, temp);lv_chart_set_next_value(chart_mgr.chart, chart_mgr.humi_series, humi);lv_chart_set_next_value(chart_mgr.chart, chart_mgr.press_series, press);chart_mgr.data_count++;// 每100个数据点清除一次旧数据(可选)if(chart_mgr.data_count > 100) {lv_chart_refresh(chart_mgr.chart);chart_mgr.data_count = 50;}
}
11.1.3 高级图表特性
多Y轴支持:
// 创建次要Y轴
lv_chart_set_axis_tick(chart, LV_CHART_AXIS_PRIMARY_Y, 5, 2, 6, 2, true, 40);
lv_chart_set_axis_tick(chart, LV_CHART_AXIS_SECONDARY_Y, 5, 2, 6, 2, true, 40);// 为系列指定不同的Y轴
lv_chart_series_t * series = lv_chart_add_series(chart, color, LV_CHART_AXIS_SECONDARY_Y);
事件交互:
// 启用光标
lv_chart_set_cursor_point(chart, cursor, &point);// 添加点击事件
lv_obj_add_event_cb(chart, [](lv_event_t * e) {lv_obj_t * chart = lv_event_get_target(e);lv_chart_series_t * ser = lv_chart_get_series_next(chart, NULL);// 获取点击位置对应的数据uint16_t id;lv_chart_get_pressed_point(chart, &id);if(id != LV_CHART_POINT_NONE) {lv_coord_t * y_array = lv_chart_get_y_array(chart, ser);printf("点击的数据点 %d: 值=%d\n", id, y_array[id]);}
}, LV_EVENT_CLICKED, NULL);
11.2 列表:结构化数据的展示
列表控件非常适合展示结构化数据,如消息、设置项、文件等。
11.2.1 列表的数学模型
列表可以看作一个线性序列:
L=[i1,i2,...,in]L = [i_1, i_2, ..., i_n]L=[i1,i2,...,in]
其中每个列表项 iki_kik 包含:
- 文本内容 tkt_ktk
- 图标 iconkicon_kiconk(可选)
- 元数据 metakmeta_kmetak(可选)
列表的渲染高度计算:
Htotal=∑k=1nhk+(n−1)×spacingH_{total} = \sum_{k=1}^n h_k + (n-1) \times spacingHtotal=k=1∑nhk+(n−1)×spacing
11.2.2 创建功能完整的消息列表
#include <lvgl.h>// 消息数据结构
typedef struct {const char * sender;const char * preview;const char * time;bool unread;lv_color_t color;
} message_t;// 模拟消息数据
static message_t messages[] = {{"张三", "你好,项目进展如何?", "10:30", true, LV_COLOR_MAKE(0x34, 0x98, 0xDB)},{"李四", "会议改到下午3点", "09:15", true, LV_COLOR_MAKE(0x2E, 0xCC, 0x71)},{"王五", "文档已更新,请查看", "昨天", false, LV_COLOR_MAKE(0xE7, 0x4C, 0x3C)},{"赵六", "周末一起吃饭?", "昨天", false, LV_COLOR_MAKE(0xF3, 0x9C, 0x12)},{"钱七", "报销单已提交", "周三", false, LV_COLOR_MAKE(0x9B, 0x59, 0xB6)},{"孙八", "新的需求文档", "周二", false, LV_COLOR_MAKE(0x1A, 0xBC, 0x9C)},
};/*** 创建消息列表*/
void create_message_list() {lv_obj_t * parent = lv_scr_act();// 创建列表容器lv_obj_t * container = lv_obj_create(parent);lv_obj_set_size(container, 350, 400);lv_obj_align(container, LV_ALIGN_TOP_RIGHT, -20, 20);lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);lv_obj_set_style_pad_all(container, 0, 0);// 标题栏lv_obj_t * header = lv_obj_create(container);lv_obj_set_size(header, LV_PCT(100), 50);lv_obj_set_style_bg_color(header, lv_color_hex(0x3498DB), 0);lv_obj_set_style_radius(header, 0, 0);lv_obj_set_flex_flow(header, LV_FLEX_FLOW_ROW);lv_obj_set_flex_align(header, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);lv_obj_set_style_pad_hor(header, 15, 0);lv_obj_t * title = lv_label_create(header);lv_label_set_text(title, "💬 消息");lv_obj_set_style_text_color(title, lv_color_white(), 0);lv_obj_set_style_text_font(title, &lv_font_montserrat_18, 0);lv_obj_t * count_label = lv_label_create(header);lv_label_set_text(count_label, "2条未读");lv_obj_set_style_text_color(count_label, lv_color_white(), 0);// 创建列表对象lv_obj_t * list = lv_list_create(container);lv_obj_set_size(list, LV_PCT(100), LV_PCT(100));lv_obj_set_style_bg_opa(list, LV_OPA_0, 0);lv_obj_set_style_pad_all(list, 0, 0);lv_obj_set_style_radius(list, 0, 0);// 设置列表样式lv_obj_set_style_bg_opa(list, LV_OPA_0, LV_PART_MAIN);lv_obj_set_style_border_width(list, 0, LV_PART_MAIN);// 添加消息项for(int i = 0; i < sizeof(messages) / sizeof(messages[0]); i++) {add_message_to_list(list, &messages[i]);}
}/*** 添加消息到列表*/
void add_message_to_list(lv_obj_t * list, message_t * msg) {// 创建列表项lv_obj_t * list_btn = lv_list_add_btn(list, NULL, NULL); // 不使用内置图标// 设置列表项样式lv_obj_set_style_bg_opa(list_btn, LV_OPA_0, 0);lv_obj_set_style_pad_all(list_btn, 15, 0);lv_obj_set_style_border_side(list_btn, LV_BORDER_SIDE_BOTTOM, 0);lv_obj_set_style_border_color(list_btn, lv_color_hex(0xECF0F1), 0);lv_obj_set_style_border_width(list_btn, 1, 0);// 如果有未读消息,添加背景色if(msg->unread) {lv_obj_set_style_bg_opa(list_btn, LV_OPA_10, 0);lv_obj_set_style_bg_color(list_btn, lv_color_hex(0x3498DB), 0);}// 创建Flex容器来组织内容lv_obj_t * content = lv_obj_create(list_btn);lv_obj_set_size(content, LV_PCT(100), LV_SIZE_CONTENT);lv_obj_set_style_bg_opa(content, LV_OPA_0, 0);lv_obj_set_style_border_width(content, 0, 0);lv_obj_set_flex_flow(content, LV_FLEX_FLOW_ROW);lv_obj_set_flex_align(content, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER);// 左侧:头像/图标lv_obj_t * avatar = lv_obj_create(content);lv_obj_set_size(avatar, 40, 40);lv_obj_set_style_bg_color(avatar, msg->color, 0);lv_obj_set_style_radius(avatar, 20, 0);lv_obj_set_style_shadow_width(avatar, 5, 0);lv_obj_set_style_shadow_color(avatar, msg->color, 0);lv_obj_set_style_shadow_opa(avatar, LV_OPA_30, 0);// 头像文字(首字符)char avatar_text[3];snprintf(avatar_text, sizeof(avatar_text), "%c", msg->sender[0]);lv_obj_t * avatar_label = lv_label_create(avatar);lv_label_set_text(avatar_label, avatar_text);lv_obj_set_style_text_color(avatar_label, lv_color_white(), 0);lv_obj_set_style_text_font(avatar_label, &lv_font_montserrat_14, 0);lv_obj_center(avatar_label);// 中间:消息内容lv_obj_t * text_container = lv_obj_create(content);lv_obj_set_size(text_container, LV_PCT(70), LV_SIZE_CONTENT);lv_obj_set_style_bg_opa(text_container, LV_OPA_0, 0);lv_obj_set_style_border_width(text_container, 0, 0);lv_obj_set_flex_flow(text_container, LV_FLEX_FLOW_COLUMN);lv_obj_set_flex_align(text_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);lv_obj_set_style_pad_left(text_container, 10, 0);// 发件人lv_obj_t * sender_label = lv_label_create(text_container);lv_label_set_text(sender_label, msg->sender);lv_obj_set_style_text_font(sender_label, &lv_font_montserrat_14, 0);lv_obj_set_style_text_color(sender_label, lv_color_black(), 0);if(msg->unread) {lv_obj_set_style_text_color(sender_label, lv_color_hex(0x2C3E50), 0);}// 消息预览lv_obj_t * preview_label = lv_label_create(text_container);lv_label_set_text(preview_label, msg->preview);lv_obj_set_style_text_color(preview_label, lv_color_hex(0x7F8C8D), 0);lv_obj_set_style_text_font(preview_label, &lv_font_montserrat_12, 0);// 右侧:时间和其他信息lv_obj_t * right_container = lv_obj_create(content);lv_obj_set_size(right_container, LV_PCT(20), LV_SIZE_CONTENT);lv_obj_set_style_bg_opa(right_container, LV_OPA_0, 0);lv_obj_set_style_border_width(right_container, 0, 0);lv_obj_set_flex_flow(right_container, LV_FLEX_FLOW_COLUMN);lv_obj_set_flex_align(right_container, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER);// 时间lv_obj_t * time_label = lv_label_create(right_container);lv_label_set_text(time_label, msg->time);lv_obj_set_style_text_color(time_label, lv_color_hex(0x95A5A6), 0);lv_obj_set_style_text_font(time_label, &lv_font_montserrat_12, 0);// 未读标识if(msg->unread) {lv_obj_t * unread_indicator = lv_obj_create(right_container);lv_obj_set_size(unread_indicator, 8, 8);lv_obj_set_style_bg_color(unread_indicator, lv_color_hex(0xE74C3C), 0);lv_obj_set_style_radius(unread_indicator, 4, 0);}// 添加点击事件lv_obj_add_event_cb(list_btn, [](lv_event_t * e) {lv_obj_t * btn = lv_event_get_target(e);message_t * msg_data = (message_t *)lv_event_get_user_data(e);printf("打开消息来自: %s\n", msg_data->sender);printf("内容: %s\n", msg_data->preview);// 标记为已读if(msg_data->unread) {msg_data->unread = false;lv_obj_set_style_bg_opa(btn, LV_OPA_0, 0);// 移除未读标识lv_obj_t * right_container = lv_obj_get_child(btn, 0);right_container = lv_obj_get_child(right_container, 2); // 获取右侧容器if(lv_obj_get_child_cnt(right_container) > 1) {lv_obj_del(lv_obj_get_child(right_container, 1)); // 删除未读红点}}}, LV_EVENT_CLICKED, msg);
}
11.3 滚轮:优雅的选择器
滚轮控件提供了一种直观的方式来选择值,特别适合日期、时间等连续或离散值的选择。
11.3.1 滚轮的物理模型
滚轮的选择行为可以模拟物理滚动:
设滚轮角速度为 ω\omegaω,转动惯量为 III,阻尼系数为 β\betaβ,则运动方程为:
Idωdt+βω=τextI \frac{d\omega}{dt} + \beta \omega = \tau_{ext}Idtdω+βω=τext
在离散时间步长 Δt\Delta tΔt 下:
ωt+1=ωt⋅e−βIΔt\omega_{t+1} = \omega_t \cdot e^{-\frac{\beta}{I} \Delta t}ωt+1=ωt⋅e−IβΔt
11.3.2 创建日期时间选择器
#include <lvgl.h>/*** 创建日期时间选择器*/
void create_datetime_picker() {lv_obj_t * parent = lv_scr_act();// 创建选择器容器lv_obj_t * container = lv_obj_create(parent);lv_obj_set_size(container, 380, 280);lv_obj_center(container);lv_obj_set_style_bg_color(container, lv_color_hex(0x2C3E50), 0);lv_obj_set_style_radius(container, 20, 0);lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);lv_obj_set_style_pad_all(container, 20, 0);// 标题lv_obj_t * title = lv_label_create(container);lv_label_set_text(title, "📅 选择日期和时间");lv_obj_set_style_text_color(title, lv_color_white(), 0);lv_obj_set_style_text_font(title, &lv_font_montserrat_18, 0);lv_obj_set_align(title, LV_ALIGN_TOP_MID);// 滚轮容器lv_obj_t * wheel_container = lv_obj_create(container);lv_obj_set_size(wheel_container, LV_PCT(100), 180);lv_obj_set_style_bg_opa(wheel_container, LV_OPA_0, 0);lv_obj_set_style_border_width(wheel_container, 0, 0);lv_obj_set_flex_flow(wheel_container, LV_FLEX_FLOW_ROW);lv_obj_set_flex_align(wheel_container, LV_FLEX_ALIGN_SPACE_AROUND, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);// 创建年份滚轮 (2020-2030)lv_obj_t * year_wheel = lv_roller_create(wheel_container);lv_obj_set_size(year_wheel, 80, 160);// 构建年份选项char years[128] = "";for(int year = 2020; year <= 2030; year++) {char year_str[8];snprintf(year_str, sizeof(year_str), "%d\n", year);strcat(years, year_str);}lv_roller_set_options(year_wheel, years, LV_ROLLER_MODE_INFINITE);// 创建月份滚轮lv_obj_t * month_wheel = lv_roller_create(wheel_container);lv_obj_set_size(month_wheel, 80, 160);lv_roller_set_options(month_wheel, "1月\n2月\n3月\n4月\n5月\n6月\n7月\n8月\n9月\n10月\n11月\n12月", LV_ROLLER_MODE_INFINITE);// 创建日期滚轮 (1-31)lv_obj_t * day_wheel = lv_roller_create(wheel_container);lv_obj_set_size(day_wheel, 60, 160);char days[256] = "";for(int day = 1; day <= 31; day++) {char day_str[8];snprintf(day_str, sizeof(day_str), "%d日\n", day);strcat(days, day_str);}lv_roller_set_options(day_wheel, days, LV_ROLLER_MODE_INFINITE);// 创建时间滚轮容器lv_obj_t * time_container = lv_obj_create(wheel_container);lv_obj_set_size(time_container, 100, 160);lv_obj_set_style_bg_opa(time_container, LV_OPA_0, 0);lv_obj_set_style_border_width(time_container, 0, 0);lv_obj_set_flex_flow(time_container, LV_FLEX_FLOW_ROW);lv_obj_set_flex_align(time_container, LV_FLEX_ALIGN_SPACE_AROUND, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);// 小时滚轮lv_obj_t * hour_wheel = lv_roller_create(time_container);lv_obj_set_size(hour_wheel, 45, 160);char hours[256] = "";for(int hour = 0; hour < 24; hour++) {char hour_str[8];snprintf(hour_str, sizeof(hour_str), "%02d\n", hour);strcat(hours, hour_str);}lv_roller_set_options(hour_wheel, hours, LV_ROLLER_MODE_INFINITE);// 分隔符lv_obj_t * separator = lv_label_create(time_container);lv_label_set_text(separator, ":");lv_obj_set_style_text_color(separator, lv_color_white(), 0);lv_obj_set_style_text_font(separator, &lv_font_montserrat_16, 0);// 分钟滚轮lv_obj_t * minute_wheel = lv_roller_create(time_container);lv_obj_set_size(minute_wheel, 45, 160);char minutes[512] = "";for(int minute = 0; minute < 60; minute++) {char minute_str[8];snprintf(minute_str, sizeof(minute_str), "%02d\n", minute);strcat(minutes, minute_str);}lv_roller_set_options(minute_wheel, minutes, LV_ROLLER_MODE_INFINITE);// 设置样式lv_obj_t * wheels[] = {year_wheel, month_wheel, day_wheel, hour_wheel, minute_wheel};for(int i = 0; i < 5; i++) {lv_obj_set_style_text_color(wheels[i], lv_color_white(), LV_PART_MAIN);lv_obj_set_style_text_color(wheels[i], lv_color_hex(0x3498DB), LV_PART_SELECTED);lv_obj_set_style_text_font(wheels[i], &lv_font_montserrat_16, LV_PART_MAIN);lv_obj_set_style_text_font(wheels[i], &lv_font_montserrat_18, LV_PART_SELECTED);lv_obj_set_style_bg_color(wheels[i], lv_color_hex(0x34495E), LV_PART_MAIN);lv_obj_set_style_bg_color(wheels[i], lv_color_hex(0x2C3E50), LV_PART_SELECTED);}// 设置当前日期时间set_current_datetime(year_wheel, month_wheel, day_wheel, hour_wheel, minute_wheel);// 底部按钮lv_obj_t * btn_container = lv_obj_create(container);lv_obj_set_size(btn_container, LV_PCT(100), 50);lv_obj_set_style_bg_opa(btn_container, LV_OPA_0, 0);lv_obj_set_style_border_width(btn_container, 0, 0);lv_obj_set_flex_flow(btn_container, LV_FLEX_FLOW_ROW);lv_obj_set_flex_align(btn_container, LV_FLEX_ALIGN_SPACE_AROUND, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);// 取消按钮lv_obj_t * cancel_btn = lv_btn_create(btn_container);lv_obj_set_size(cancel_btn, 100, 40);lv_obj_set_style_bg_color(cancel_btn, lv_color_hex(0x7F8C8D), 0);lv_obj_set_style_radius(cancel_btn, 10, 0);lv_obj_t * cancel_label = lv_label_create(cancel_btn);lv_label_set_text(cancel_label, "取消");lv_obj_center(cancel_label);// 确定按钮lv_obj_t * confirm_btn = lv_btn_create(btn_container);lv_obj_set_size(confirm_btn, 100, 40);lv_obj_set_style_bg_color(confirm_btn, lv_color_hex(0x3498DB), 0);lv_obj_set_style_radius(confirm_btn, 10, 0);lv_obj_t * confirm_label = lv_label_create(confirm_btn);lv_label_set_text(confirm_label, "确定");lv_obj_center(confirm_label);// 事件处理lv_obj_add_event_cb(confirm_btn, [](lv_event_t * e) {lv_obj_t * container = lv_obj_get_parent(lv_obj_get_parent((lv_obj_t *)e->user_data));lv_obj_t * wheel_container = lv_obj_get_child(container, 1);// 获取所有滚轮lv_obj_t * year_wheel = lv_obj_get_child(wheel_container, 0);lv_obj_t * month_wheel = lv_obj_get_child(wheel_container, 1);lv_obj_t * day_wheel = lv_obj_get_child(wheel_container, 2);lv_obj_t * time_container = lv_obj_get_child(wheel_container, 3);lv_obj_t * hour_wheel = lv_obj_get_child(time_container, 0);lv_obj_t * minute_wheel = lv_obj_get_child(time_container, 2);// 获取选中的值uint16_t year_sel = lv_roller_get_selected(year_wheel);uint16_t month_sel = lv_roller_get_selected(month_wheel);uint16_t day_sel = lv_roller_get_selected(day_wheel);uint16_t hour_sel = lv_roller_get_selected(hour_wheel);uint16_t minute_sel = lv_roller_get_selected(minute_wheel);printf("选择的日期时间: %d年%d月%d日 %02d:%02d\n", 2020 + year_sel, month_sel + 1, day_sel + 1, hour_sel, minute_sel);// 关闭选择器lv_obj_del(container);}, LV_EVENT_CLICKED, wheel_container);lv_obj_add_event_cb(cancel_btn, [](lv_event_t * e) {lv_obj_t * container = lv_obj_get_parent(lv_obj_get_parent((lv_obj_t *)e->user_data));lv_obj_del(container);}, LV_EVENT_CLICKED, wheel_container);
}/*** 设置当前日期时间*/
void set_current_datetime(lv_obj_t * year_wheel, lv_obj_t * month_wheel, lv_obj_t * day_wheel, lv_obj_t * hour_wheel, lv_obj_t * minute_wheel) {// 这里应该获取系统时间,这里使用固定值演示int current_year = 2024;int current_month = 5; // 6月 (0-based)int current_day = 15; // 16日 (0-based)int current_hour = 14;int current_minute = 30;lv_roller_set_selected(year_wheel, current_year - 2020, LV_ANIM_OFF);lv_roller_set_selected(month_wheel, current_month, LV_ANIM_OFF);lv_roller_set_selected(day_wheel, current_day, LV_ANIM_OFF);lv_roller_set_selected(hour_wheel, current_hour, LV_ANIM_OFF);lv_roller_set_selected(minute_wheel, current_minute, LV_ANIM_OFF);
}
第12章:图片与字体
12.1 图片:视觉元素的载体
图片是GUI设计中不可或缺的元素。LVGL支持多种图片格式和存储方式。
12.1.1 图片的数学表示
数字图片可以表示为像素矩阵:
I=[pij]m×nI = [p_{ij}]_{m \times n}I=[pij]m×n
其中 pijp_{ij}pij 表示位置 (i,j)(i, j)(i,j) 的像素值,对于RGB格式:
pij=(Rij,Gij,Bij)p_{ij} = (R_{ij}, G_{ij}, B_{ij})pij=(Rij,Gij,Bij)
图片缩放变换:
Iscaled(x′,y′)=I(x′sx,y′sy)I_{scaled}(x', y') = I\left(\frac{x'}{s_x}, \frac{y'}{s_y}\right)Iscaled(x′,y′)=I(sxx′,syy′)
其中 (sx,sy)(s_x, s_y)(sx,sy) 是缩放因子。
12.1.2 图片存储格式对比
LVGL支持多种图片存储格式:
12.1.3 实战:创建图片库浏览器
#include <lvgl.h>// 图片数据(实际项目中从文件加载)
// 这里使用内置符号模拟图片
static const char * image_symbols[] = {LV_SYMBOL_AUDIO,LV_SYMBOL_VIDEO,LV_SYMBOL_LIST,LV_SYMBOL_OK,LV_SYMBOL_CLOSE,LV_SYMBOL_POWER,LV_SYMBOL_SETTINGS,LV_SYMBOL_HOME,LV_SYMBOL_DOWNLOAD,LV_SYMBOL_DRIVE,LV_SYMBOL_REFRESH,LV_SYMBOL_MUTE,LV_SYMBOL_VOLUME_MID,LV_SYMBOL_BATTERY_3,LV_SYMBOL_BATTERY_2,LV_SYMBOL_BATTERY_1,
};/*** 创建图片库浏览器*/
void create_image_gallery() {lv_obj_t * parent = lv_scr_act();// 创建图片库容器lv_obj_t * gallery = lv_obj_create(parent);lv_obj_set_size(gallery, 400, 450);lv_obj_center(gallery);lv_obj_set_style_bg_color(gallery, lv_color_hex(0x34495E), 0);lv_obj_set_style_radius(gallery, 15, 0);lv_obj_set_flex_flow(gallery, LV_FLEX_FLOW_COLUMN);lv_obj_set_style_pad_all(gallery, 15, 0);// 标题lv_obj_t * title = lv_label_create(gallery);lv_label_set_text(title, "🖼️ 图片库");lv_obj_set_style_text_color(title, lv_color_white(), 0);lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);// 创建网格容器lv_obj_t * grid = lv_obj_create(gallery);lv_obj_set_size(grid, LV_PCT(100), LV_PCT(85));lv_obj_set_style_bg_opa(grid, LV_OPA_0, 0);lv_obj_set_style_border_width(grid, 0, 0);lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);lv_obj_set_style_pad_gap(grid, 10, 0);// 添加图片项for(int i = 0; i < sizeof(image_symbols) / sizeof(image_symbols[0]); i++) {create_image_item(grid, image_symbols[i], i);}
}/*** 创建单个图片项*/
void create_image_item(lv_obj_t * parent, const char * symbol, int index) {// 创建图片容器lv_obj_t * item = lv_obj_create(parent);lv_obj_set_size(item, 80, 100);lv_obj_set_style_bg_color(item, lv_color_hex(0x4A6572), 0);lv_obj_set_style_radius(item, 12, 0);lv_obj_set_style_shadow_width(item, 8, 0);lv_obj_set_style_shadow_color(item, lv_color_hex(0x2C3E50), 0);lv_obj_set_flex_flow(item, LV_FLEX_FLOW_COLUMN);lv_obj_set_flex_align(item, LV_FLEX_ALIGN_SPACE_AROUND, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);// 创建"图片"(使用符号字体)lv_obj_t * img = lv_label_create(item);lv_label_set_text(img, symbol);lv_obj_set_style_text_color(img, lv_color_white(), 0);lv_obj_set_style_text_font(img, &lv_font_montserrat_24, 0);// 图片标题char title[16];snprintf(title, sizeof(title), "图片 %d", index + 1);lv_obj_t * label = lv_label_create(item);lv_label_set_text(label, title);lv_obj_set_style_text_color(label, lv_color_white(), 0);lv_obj_set_style_text_font(label, &lv_font_montserrat_12, 0);// 添加点击事件lv_obj_add_event_cb(item, [](lv_event_t * e) {lv_obj_t * item = lv_event_get_target(e);lv_obj_t * label = lv_obj_get_child(item, 1);const char * title = lv_label_get_text(label);printf("点击了: %s\n", title);// 添加点击动画lv_anim_t a;lv_anim_init(&a);lv_anim_set_var(&a, item);lv_anim_set_values(&a, 255, 200, 255);lv_anim_set_time(&a, 300);lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_style_opa);lv_anim_start(&a);}, LV_EVENT_CLICKED, NULL);
}/*** 从文件系统加载图片示例*/
void load_image_from_filesystem() {// 注意:此功能需要配置LVGL文件系统支持lv_obj_t * img = lv_img_create(lv_scr_act());// 从文件加载图片lv_img_set_src(img, "S:/images/picture.jpg"); // "S:" 是文件系统驱动器字母// 设置图片属性lv_obj_set_size(img, 200, 150);lv_obj_center(img);// 设置图片样式lv_obj_set_style_radius(img, 10, 0);lv_obj_set_style_shadow_width(img, 15, 0);lv_obj_set_style_shadow_color(img, lv_color_hex(0x2C3E50), 0);
}
12.2 字体:文字渲染的艺术
字体是UI设计的灵魂,合适的字体能极大提升用户体验。
12.2.1 字体的数学基础
字体渲染涉及复杂的几何计算:
字符边界框:
每个字符 ccc 可以定义边界框 B(c)=(xmin,ymin,xmax,ymax)B(c) = (x_{min}, y_{min}, x_{max}, y_{max})B(c)=(xmin,ymin,xmax,ymax)
基线对齐:
字符在基线上下分布:
- 上升高度:a=ymax−baselinea = y_{max} - baselinea=ymax−baseline
- 下降高度:d=baseline−ymind = baseline - y_{min}d=baseline−ymin
- 行高:h=a+d+leadingh = a + d + leadingh=a+d+leading
12.2.2 使用外部字体
LVGL支持从外部文件加载字体,让我们集成中文字体:
#include <lvgl.h>// 字体声明(实际项目中从文件加载)
LV_FONT_DECLARE(lv_font_montserrat_16);
LV_FONT_DECLARE(lv_font_montserrat_24);
LV_FONT_DECLARE(lv_font_montserrat_32);/*** 创建字体展示界面*/
void create_font_showcase() {lv_obj_t * parent = lv_scr_act();// 创建字体展示容器lv_obj_t * container = lv_obj_create(parent);lv_obj_set_size(container, 380, 500);lv_obj_center(container);lv_obj_set_style_bg_color(container, lv_color_hex(0x2C3E50), 0);lv_obj_set_style_radius(container, 15, 0);lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);lv_obj_set_style_pad_all(container, 20, 0);lv_obj_set_style_pad_gap(container, 15, 0);// 标题lv_obj_t * title = lv_label_create(container);lv_label_set_text(title, "🔤 字体展示");lv_obj_set_style_text_color(title, lv_color_white(), 0);lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0);// 字体示例区域create_font_example(container, "小字体 (16px)", &lv_font_montserrat_16, "The quick brown fox jumps over the lazy dog");create_font_example(container, "中等字体 (24px)", &lv_font_montserrat_24, "The quick brown fox jumps over the lazy dog");create_font_example(container, "大字体 (32px)", &lv_font_montserrat_32, "The quick brown fox");// 多语言支持示例create_multilingual_example(container);
}/*** 创建字体示例*/
void create_font_example(lv_obj_t * parent, const char * font_name, const lv_font_t * font, const char * sample_text) {// 字体示例容器lv_obj_t * example = lv_obj_create(parent);lv_obj_set_size(example, LV_PCT(100), LV_SIZE_CONTENT);lv_obj_set_style_bg_color(example, lv_color_hex(0x4A6572), 0);lv_obj_set_style_radius(example, 10, 0);lv_obj_set_flex_flow(example, LV_FLEX_FLOW_COLUMN);lv_obj_set_style_pad_all(example, 15, 0);// 字体名称lv_obj_t * name_label = lv_label_create(example);lv_label_set_text(name_label, font_name);lv_obj_set_style_text_color(name_label, lv_color_hex(0xBDC3C7), 0);lv_obj_set_style_text_font(name_label, &lv_font_montserrat_14, 0);// 示例文本lv_obj_t * text_label = lv_label_create(example);lv_label_set_text(text_label, sample_text);lv_obj_set_style_text_color(text_label, lv_color_white(), 0);lv_obj_set_style_text_font(text_label, font, 0);lv_obj_set_style_text_align(text_label, LV_TEXT_ALIGN_LEFT, 0);lv_obj_set_width(text_label, LV_PCT(100));
}/*** 创建多语言示例*/
void create_multilingual_example(lv_obj_t * parent) {lv_obj_t * example = lv_obj_create(parent);lv_obj_set_size(example, LV_PCT(100), LV_SIZE_CONTENT);lv_obj_set_style_bg_color(example, lv_color_hex(0x4A6572), 0);lv_obj_set_style_radius(example, 10, 0);lv_obj_set_flex_flow(example, LV_FLEX_FLOW_COLUMN);lv_obj_set_style_pad_all(example, 15, 0);// 标题lv_obj_t * title = lv_label_create(example);lv_label_set_text(title, "🌍 多语言支持");lv_obj_set_style_text_color(title, lv_color_hex(0xBDC3C7), 0);lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);// 多语言文本示例const char * multilingual_text = "English: Hello World!\n""中文: 你好,世界!\n""Español: ¡Hola Mundo!\n""Français: Bonjour le monde!\n""日本語: こんにちは世界!";lv_obj_t * text_label = lv_label_create(example);lv_label_set_text(text_label, multilingual_text);lv_obj_set_style_text_color(text_label, lv_color_white(), 0);lv_obj_set_style_text_font(text_label, &lv_font_montserrat_16, 0);lv_obj_set_width(text_label, LV_PCT(100));
}/*** 动态字体切换示例*/
void create_font_switcher() {lv_obj_t * parent = lv_scr_act();lv_obj_t * container = lv_obj_create(parent);lv_obj_set_size(container, 300, 200);lv_obj_align(container, LV_ALIGN_BOTTOM_RIGHT, -20, -20);lv_obj_set_style_bg_color(container, lv_color_hex(0x34495E), 0);lv_obj_set_style_radius(container, 15, 0);lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);lv_obj_set_style_pad_all(container, 15, 0);// 标题lv_obj_t * title = lv_label_create(container);lv_label_set_text(title, "字体切换器");lv_obj_set_style_text_color(title, lv_color_white(), 0);// 动态文本lv_obj_t * dynamic_text = lv_label_create(container);lv_label_set_text(dynamic_text, "这段文字会改变字体!");lv_obj_set_style_text_color(dynamic_text, lv_color_white(), 0);lv_obj_set_style_text_font(dynamic_text, &lv_font_montserrat_16, 0);// 字体选择按钮lv_obj_t * btn_container = lv_obj_create(container);lv_obj_set_size(btn_container, LV_PCT(100), LV_SIZE_CONTENT);lv_obj_set_style_bg_opa(btn_container, LV_OPA_0, 0);lv_obj_set_flex_flow(btn_container, LV_FLEX_FLOW_ROW_WRAP);lv_obj_set_flex_align(btn_container, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);const char * font_names[] = {"小字体", "中字体", "大字体"};const lv_font_t * fonts[] = {&lv_font_montserrat_16,&lv_font_montserrat_24, &lv_font_montserrat_32};for(int i = 0; i < 3; i++) {lv_obj_t * btn = lv_btn_create(btn_container);lv_obj_set_size(btn, 80, 35);lv_obj_set_style_bg_color(btn, lv_color_hex(0x3498DB), 0);lv_obj_t * label = lv_label_create(btn);lv_label_set_text(label, font_names[i]);lv_obj_center(label);// 字体切换事件lv_obj_add_event_cb(btn, [](lv_event_t * e) {int font_index = (int)e->user_data;lv_obj_t * text_label = (lv_obj_t *)lv_event_get_user_data(e);lv_obj_set_style_text_font(text_label, fonts[font_index], 0);printf("切换到字体: %s\n", font_names[font_index]);}, LV_EVENT_CLICKED, dynamic_text);// 存储字体索引lv_obj_set_user_data(btn, (void*)(intptr_t)i);}
}
12.2.3 自定义字体生成
在实际项目中,你可能需要生成自定义字体:
# 使用LVGL字体转换工具
python lv_font_conv.py \--font MyFont.ttf \--range 0x20-0x7F,0x4E00-0x9FFF \--size 16 \--format lvgl \--bpp 4 \--no-compress \-o my_font_16.c
然后在代码中使用:
LV_FONT_DECLARE(my_font_16);// 应用自定义字体
lv_obj_set_style_text_font(label, &my_font_16, 0);
本章总结与挑战
恭喜!你已经掌握了LVGL进阶数据展示和资源管理的核心技术。现在你能够:
- 创建专业的数据图表,实现实时数据可视化
- 构建功能完整的列表界面,展示结构化数据
- 实现优雅的选择器,提供优秀的用户体验
- 管理和使用图片资源,丰富界面视觉效果
- 集成和使用自定义字体,提升文本显示质量
综合挑战:
-
智能家居仪表盘升级:扩展之前的项目,添加:
- 实时环境数据图表
- 设备状态历史列表
- 多语言支持界面
-
媒体播放器界面:创建一个完整的媒体播放器:
- 专辑封面图片展示
- 播放列表(使用高级列表)
- 进度控制(使用改进的滑块和动画)
-
天气预报应用:实现一个天气预报应用:
- 多日温度曲线图表
- 天气图标字体
- 位置选择器(使用滚轮)
在下一部分,我们将进入深入原理与移植优化,学习LVGL的内部机制和性能优化技术,让你从使用者成长为LVGL专家!
研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)