第4部分:进阶应用与项目实战(上)
文章目录
- **第4部分:进阶应用与项目实战(上)**
-
- [**第11章:数据展示三剑客 - 图表、列表、滚轮**](#第11章:数据展示三剑客 - 图表、列表、滚轮)
-
- [**11.1 图表:数据的可视化艺术**](#11.1 图表:数据的可视化艺术)
-
- [**11.1.1 图表的基本数学模型**](#11.1.1 图表的基本数学模型)
- [**11.1.2 创建实时数据监控图表**](#11.1.2 创建实时数据监控图表)
- [**11.1.3 高级图表特性**](#11.1.3 高级图表特性)
- [**11.2 列表:结构化数据的展示**](#11.2 列表:结构化数据的展示)
-
- [**11.2.1 列表的数学模型**](#11.2.1 列表的数学模型)
- [**11.2.2 创建功能完整的消息列表**](#11.2.2 创建功能完整的消息列表)
- [**11.3 滚轮:优雅的选择器**](#11.3 滚轮:优雅的选择器)
-
- [**11.3.1 滚轮的物理模型**](#11.3.1 滚轮的物理模型)
- [**11.3.2 创建日期时间选择器**](#11.3.2 创建日期时间选择器)
- **第12章:图片与字体**
-
- [**12.1 图片:视觉元素的载体**](#12.1 图片:视觉元素的载体)
-
- [**12.1.1 图片的数学表示**](#12.1.1 图片的数学表示)
- [**12.1.2 图片存储格式对比**](#12.1.2 图片存储格式对比)
- [**12.1.3 实战:创建图片库浏览器**](#12.1.3 实战:创建图片库浏览器)
- [**12.2 字体:文字渲染的艺术**](#12.2 字体:文字渲染的艺术)
-
- [**12.2.1 字体的数学基础**](#12.2.1 字体的数学基础)
- [**12.2.2 使用外部字体**](#12.2.2 使用外部字体)
- [**12.2.3 自定义字体生成**](#12.2.3 自定义字体生成)
- **本章总结与挑战**
第11章:数据展示三剑客 - 图表、列表、滚轮
11.1 图表:数据的可视化艺术
在现代GUI应用中,数据可视化至关重要。LVGL提供了强大的图表控件,能够将抽象数据转化为直观的图形。
11.1.1 图表的基本数学模型
图表的核心是将数据点映射到屏幕坐标:
设数据序列 D = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x n , y n ) } 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),值域为 [ y m i n , y m a x ] [y_{min}, y_{max}] [ymin,ymax]。
则每个数据点的屏幕坐标 ( X i , Y i ) (X_i, Y_i) (Xi,Yi) 计算为:
X i = x i − x m i n x m a x − x m i n × W X_i = \frac{x_i - x_{min}}{x_{max} - x_{min}} \times W Xi=xmax−xminxi−xmin×W
Y i = H − y i − y m i n y m a x − y m i n × H Y_i = H - \frac{y_i - y_{min}}{y_{max} - y_{min}} \times H Yi=H−ymax−yminyi−ymin×H
11.1.2 创建实时数据监控图表
让我们创建一个传感器数据实时监控界面:
cpp
#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轴支持:
cpp
// 创建次要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);
事件交互:
cpp
// 启用光标
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 = [ i 1 , i 2 , . . . , i n ] L = [i_1, i_2, ..., i_n] L=[i1,i2,...,in]
其中每个列表项 i k i_k ik 包含:
- 文本内容 t k t_k tk
- 图标 i c o n k icon_k iconk(可选)
- 元数据 m e t a k meta_k metak(可选)
列表的渲染高度计算:
H t o t a l = ∑ k = 1 n h k + ( n − 1 ) × s p a c i n g H_{total} = \sum_{k=1}^n h_k + (n-1) \times spacing Htotal=k=1∑nhk+(n−1)×spacing
11.2.2 创建功能完整的消息列表
cpp
#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 ω,转动惯量为 I I I,阻尼系数为 β \beta β,则运动方程为:
I d ω d t + β ω = τ e x t I \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 创建日期时间选择器
cpp
#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 = [ p i j ] m × n I = [p_{ij}]_{m \times n} I=[pij]m×n
其中 p i j p_{ij} pij 表示位置 ( i , j ) (i, j) (i,j) 的像素值,对于RGB格式:
p i j = ( R i j , G i j , B i j ) p_{ij} = (R_{ij}, G_{ij}, B_{ij}) pij=(Rij,Gij,Bij)
图片缩放变换:
I s c a l e d ( x ′ , y ′ ) = I ( x ′ s x , y ′ s y ) I_{scaled}(x', y') = I\left(\frac{x'}{s_x}, \frac{y'}{s_y}\right) Iscaled(x′,y′)=I(sxx′,syy′)
其中 ( s x , s y ) (s_x, s_y) (sx,sy) 是缩放因子。
12.1.2 图片存储格式对比
LVGL支持多种图片存储格式:
图片存储格式 C数组 文件系统 外部存储器 编译时嵌入 快速访问 占用Flash 运行时加载 需要文件系统 灵活更新 专用解码器 大图片支持 复杂实现
12.1.3 实战:创建图片库浏览器
cpp
#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 字体的数学基础
字体渲染涉及复杂的几何计算:
字符边界框:
每个字符 c c c 可以定义边界框 B ( c ) = ( x m i n , y m i n , x m a x , y m a x ) B(c) = (x_{min}, y_{min}, x_{max}, y_{max}) B(c)=(xmin,ymin,xmax,ymax)
基线对齐:
字符在基线上下分布:
- 上升高度: a = y m a x − b a s e l i n e a = y_{max} - baseline a=ymax−baseline
- 下降高度: d = b a s e l i n e − y m i n d = baseline - y_{min} d=baseline−ymin
- 行高: h = a + d + l e a d i n g h = a + d + leading h=a+d+leading
12.2.2 使用外部字体
LVGL支持从外部文件加载字体,让我们集成中文字体:
cpp
#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 自定义字体生成
在实际项目中,你可能需要生成自定义字体:
bash
# 使用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
然后在代码中使用:
cpp
LV_FONT_DECLARE(my_font_16);
// 应用自定义字体
lv_obj_set_style_text_font(label, &my_font_16, 0);
本章总结与挑战
恭喜!你已经掌握了LVGL进阶数据展示和资源管理的核心技术。现在你能够:
- 创建专业的数据图表,实现实时数据可视化
- 构建功能完整的列表界面,展示结构化数据
- 实现优雅的选择器,提供优秀的用户体验
- 管理和使用图片资源,丰富界面视觉效果
- 集成和使用自定义字体,提升文本显示质量
综合挑战:
-
智能家居仪表盘升级:扩展之前的项目,添加:
- 实时环境数据图表
- 设备状态历史列表
- 多语言支持界面
-
媒体播放器界面:创建一个完整的媒体播放器:
- 专辑封面图片展示
- 播放列表(使用高级列表)
- 进度控制(使用改进的滑块和动画)
-
天气预报应用:实现一个天气预报应用:
- 多日温度曲线图表
- 天气图标字体
- 位置选择器(使用滚轮)
在下一部分,我们将进入深入原理与移植优化,学习LVGL的内部机制和性能优化技术,让你从使用者成长为LVGL专家!
研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)