LVGL作为EmWin的一个开源替代,在做国产替代项目经常用到,结合国产的MCU可以说是绝配。

这里汇总LVGL里面常用的一些设置和组件的使用。

一般用lv_imgbtn作为最常用的组件,结合坐标可以实现界面的完全自定义。

使用LVGL做大型的自定义界面,配置方面最好多带一块外置的Flash例如:

华邦(Winbond) 25Q128JVSIQ 128Mbit(16MB) 存储芯片 如果界面比16MB还要大,还可以用32MB的存储芯片

华邦(Winbond) W25Q256JVSIQ 256Mbit (32MB) 存储芯片

有了大的存储空间,自定义的UI就可以任意发挥了。

本篇把做自定义界面时最常用到的调用整理成速查手册,按“工具 → 全局设置 → 控件基础 → 图片按钮 → 图片资源 → 事件”分组,每个片段都注明用途与易错点。

工具软件

  • Lvgl_image_convert_tool

离线的图片转换工具,可以转换图片为C文件或者是bin文件

  • LvglFontTool_V0.4 LVGL字库制作工具, 当要显示中文的时候,这个是必备工具, 把项目中要用到的文字制作成C文件嵌入到项目中来,就能显示中文。

常用的LVGL代码

调试日志输出

1
  LV_LOG_USER("Clicked date: %02d.%02d.%d", date.day, date.month, date.year); //打印日历信息

日志输出尽量不要用printf()函数,后期作为正式产品输出的时候不方便禁用。LV_LOG_* 系列的好处是可分级(TRACE/INFO/WARN/ERROR)且集中管理:开发期开 TRACE,量产把级别调到 WARN 就安静了,散落在各处的 printf 做不到这一点。使用前需在 lv_conf.h 打开 LV_USE_LOG,并用 lv_log_register_print_cb 注册输出函数(或定义 LV_LOG_PRINTF 直接走 printf)。

禁止屏幕滑动和出现滚动条

1
lv_obj_clear_flag(lv_scr_act(),LV_OBJ_FLAG_SCROLLABLE);

一般主屏幕设置成禁止滚动会比较好。原因:自定义界面都是绝对坐标摆位的,控件一旦比屏幕大(或加了边距),屏幕对象就获得滚动能力,误触拖动时所有控件会整体跑偏。LV_OBJ_FLAG_SCROLLABLE 是对象级标志,哪个对象内容溢出哪个才会滚。

位置,文本,事件绑定

1
2
3
4
5
6
lv_obj_set_pos(btn1, 20, 20);//设置坐标
lv_obj_set_size(btn1, 150, 50);//设置大小
lv_obj_set_event_cb(btn1, btn_event_cb);//设置回调函数

lv_obj_t* label = lv_label_create(btn1, NULL);//给 btn1 添加 label 子对象
lv_label_set_text(label, "Click me");//设置文本

注意版本差异:lv_obj_set_event_cb 是 LVGL v7 的接口,v8 里改为 lv_obj_add_event_cb(btn1, btn_event_cb, LV_EVENT_ALL, NULL)——v8 一个对象可以挂多个回调、并按事件过滤。label 以 btn1 为父对象创建,按钮动它跟着动,这是父子坐标系的用法。

创建图片按钮

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static lv_obj_t* create_menu_button(lv_obj_t *parent, const void *press_icon,
		const void *relese_icon, lv_coord_t x, lv_coord_t y) {
	lv_obj_t *icon = lv_imgbtn_create(parent);
	lv_imgbtn_set_src(icon, LV_IMGBTN_STATE_CHECKED_RELEASED, relese_icon, NULL,
	NULL);
	lv_imgbtn_set_src(icon, LV_IMGBTN_STATE_CHECKED_PRESSED, press_icon,
	NULL,
	NULL);
	lv_imgbtn_set_state(icon, LV_IMGBTN_STATE_CHECKED_RELEASED);
	lv_obj_set_pos(icon, x, y);
	lv_img_set_antialias(parent, false);
	return icon;
}

图片按钮的核心是“一套状态对应一套图”:为 CHECKED_RELEASED(选中且松开)、CHECKED_PRESSED(选中且按下)分别指定贴图,之后用 lv_imgbtn_set_state 切状态,贴图自动跟着换。后两个 NULL 是三段式贴图(左/中/右)布局,普通整图按钮留空即可。事件处理函数里按 lv_event_get_code 的返回值分流:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * 手动点击接水事件
 * @param event
 */
void menu_start_event_cb(lv_event_t *event) {
	lv_event_code_t code = lv_event_get_code(event); //获取事件code
	if (code == LV_EVENT_PRESSED) {
		/* 对象被长按 */
		//按下 水机开始工作
		water_machine_start_work();
		//显示警告消息
		show_warn_msg(WARN_CODE_CUP_HOT);
	} else if (code == LV_EVENT_LONG_PRESSED) {
		/* 对象被长按 */
		//长按 水机开始工作
		water_machine_start_work();
		//显示警告消息
		show_warn_msg(WARN_CODE_CUP_HOT);
	} else if (code == LV_EVENT_LONG_PRESSED_REPEAT) {
		/* 对象被重复长按 */
		//长按 水机开始工作
		water_machine_start_work();
		//显示警告消息
		show_warn_msg(WARN_CODE_CUP_HOT);
	} else if (code == LV_EVENT_RELEASED) {
		/* 对象被释放 */
		//释放,停止出水
		water_machine_stop_work();
		//隐藏警告消息
		show_warn_msg(WARN_CODE_HIDDEN);

	}
	if (code == LV_EVENT_VALUE_CHANGED) { //值改变事件处理
		//lv_obj_t* obj = lv_event_get_current_target(e); //获取事件产生的对象
	}
}

调用

1
2
3
4
5
//定义对象
lv_obj_t *menu_start;
//创建按钮
menu_start = create_menu_button(lv_scr_act(), &menu_tea_p,
			&menu_tea_r, 621, 50);

使得图片按钮可以点击

1
lv_obj_add_flag(menu_start, LV_OBJ_FLAG_CLICKABLE);

禁止某个组件的点击功能

与上面相反,把 CLICKABLE 标志清掉即可:

1
lv_obj_clear_flag(menu_start, LV_OBJ_FLAG_CLICKABLE);

清掉后控件不再响应按下事件,点击会“穿透”给下层对象,适合做纯装饰性图片。

隐藏图片按钮

1
lv_imgbtn_set_state(menu_start, LV_IMGBTN_STATE_CHECKED_DISABLED);

修改图片状态为普通未按下状态

1
lv_imgbtn_set_state(menu_start, LV_IMGBTN_STATE_CHECKED_RELEASED);

修改图片状态为按下状态

1
lv_imgbtn_set_state(menu_start, LV_IMGBTN_STATE_CHECKED_PRESSED);

显示/隐藏控件

1
lv_obj_clear_flag(img_warn_msg, LV_OBJ_FLAG_HIDDEN);

LV_OBJ_FLAG_HIDDEN 是所有控件通用的隐藏标志:置上即不渲染(对象还在,坐标和父子关系不变),清掉即显示。比起反复创建/删除对象,切标志没有内存分配开销,弹窗类 UI 推荐这种做法;隐藏时记得连子对象一起考虑(隐藏父对象会连子对象一起藏)。

修改图片内容

同一个 img 对象运行中换图,两种来源写法不同:C 数组方式(资源编进固件,速度快、占内部 Flash)用指针;bin 方式(资源在外部存储,走文件系统)用 “盘符:路径” 字符串。换图后尺寸变了记得 lv_obj_set_size 或让 LVGL 自动按新图重算。将图片转换成C文件方式

1
2
3
4
//定义图片
LV_IMG_DECLARE(warn_cup_block);
//更改图片src
lv_img_set_src(img_warn_msg, &warn_cup_block);

将图片转换成bin文件方式:

1
2
//需要配合FatFS和提前将bin文件放到SD卡上
lv_img_set_src(img_warn_msg, "S:images/warn/warn_cup_block.bin");

手动触发事件

事件不只来自输入设备,代码里也能主动发。自定义事件(如 USER_EVENT_1)自己定义枚举,从 _LV_EVENT_LAST 之后编号,就不会和系统事件撞号:

1
2
3
//下面2个函数手动触发事件
lv_event_send(btn1, USER_EVENT_1, &test_data);//内部调用lv_event_send_func
lv_event_send_func(btn_event_cb, btn1, LV_EVENT_REFRESH, &test_data1);

事件清单

这份枚举就是 lv_event_get_code 的全部返回值,按三组记:按下类(PRESSED/PRESSING/CLICKED/RELEASED/长按系列)、拖动与手势类、状态变化类(VALUE_CHANGED/FOCUSED/DELETE 等)。写回调时先想清楚要的是“边沿”还是“电平”:CLICKED 是完整的一次点按,PRESSED 是按下瞬间,LONG_PRESSED 要按住超过 LV_INDEV_LONG_PRESS_TIME 才来。以你手上版本的 lv_event.h 为准,不同小版本偶有增减:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum {
    LV_EVENT_PRESSED,             /**< The object has been pressed*/
    LV_EVENT_PRESSING,            /**< The object is being pressed (called continuously while pressing)*/
    LV_EVENT_PRESS_LOST,          /**< User is still pressing but slid cursor/finger off of the object */
    LV_EVENT_SHORT_CLICKED,       /**< User pressed object for a short period of time, then released it. Not called if dragged. */
    LV_EVENT_LONG_PRESSED,        /**< Object has been pressed for at least `LV_INDEV_LONG_PRESS_TIME`.  Not called if dragged.*/
    LV_EVENT_LONG_PRESSED_REPEAT, /**< Called after `LV_INDEV_LONG_PRESS_TIME` in every
                                       `LV_INDEV_LONG_PRESS_REP_TIME` ms.  Not called if dragged.*/
    LV_EVENT_CLICKED,             /**< Called on release if not dragged (regardless to long press)*/
    LV_EVENT_RELEASED,            /**< Called in every cases when the object has been released*/
    LV_EVENT_DRAG_BEGIN,
    LV_EVENT_DRAG_END,
    LV_EVENT_DRAG_THROW_BEGIN,
    LV_EVENT_GESTURE,           /**< The object has been gesture*/
    LV_EVENT_KEY,
    LV_EVENT_FOCUSED,
    LV_EVENT_DEFOCUSED,
    LV_EVENT_LEAVE,
    LV_EVENT_VALUE_CHANGED,      /**< The object's value has changed (i.e. slider moved) */
    LV_EVENT_INSERT,
    LV_EVENT_REFRESH,
    LV_EVENT_APPLY,  /**< "Ok", "Apply" or similar specific button has clicked*/
    LV_EVENT_CANCEL, /**< "Close", "Cancel" or similar specific button has clicked*/
    LV_EVENT_DELETE, /**< Object is being deleted */
    _LV_EVENT_LAST /** Number of events*/
};

踩坑记录与注意事项

  • v7/v8 接口混用:set_event_cb(v7)与 add_event_cb(v8)、v8.2 前后 imgbtn 接口都有差异,从旧工程拷代码先对一遍当前版本的 lv_event.h / lv_imgbtn.h;
  • 图片资源两种形式按需选:C 数组嵌入固件、读取快但占 Flash 且换图要重编;bin 放外部存储、换图只拷文件但要做文件系统。大 UI 推荐后者(见文首的外挂 Flash 方案);
  • LV_LOG 要先使能:lv_conf.h 里不开 LV_USE_LOG,LV_LOG_USER 什么都不输出;
  • 禁了滚动还想要局部滚动:SCROLLABLE 是对象级的,主屏禁掉、列表容器单独保留即可,别全局一刀切;
  • 事件回调里别做重活:回调在 lv_timer_handler 的上下文里执行,长循环会拖死整个 GUI 刷新(包括喂狗),耗时操作发个标志位出去做;
  • 贴图状态没配全:只设了 PRESSED 没设 RELEASED,切状态时会显示空白;imgbtn 的每个用到的状态都要有图。