从零开始玩转LVGL画布:让嵌入式UI拥有“自由绘图”的灵魂
你有没有遇到过这样的场景?
想在智能手表上画一个渐变色的圆形表盘,却发现标准控件只能填充单一颜色;
想实时显示一段音频频谱,但系统里根本没有“波形图”这种组件;
甚至只是想给按钮加个带阴影的自定义图标,也得提前用图像工具做好PNG贴上去……
传统GUI库就像一套预制积木——整齐、规范,但一旦你想搭出图纸外的造型,立刻就卡住了。
而LVGL(Light and Versatile Graphics Library)之所以能在嵌入式领域迅速崛起,正是因为它不仅提供了丰富的标准控件,还留了一扇“后门”:Canvas画布。它不给你现成的图形,而是直接递给你一支画笔和一块画布,说:“来吧,你想画什么,自己动手。”
今天,我们就从零开始,带你走进LVGL的Canvas世界,彻底搞懂这个让无数工程师拍手叫绝的功能——如何在资源有限的MCU上,实现像素级的自由创作。
为什么你需要Canvas?不只是“多一个控件”那么简单
在讲技术细节之前,先问一个问题:我们真的需要一个能画画的模块吗?
毕竟,LVGL已经有按钮、滑块、标签、图表……看起来什么都有了。
但现实是:这些控件再丰富,也只是“别人设计好的东西”。当你面对以下需求时,它们往往束手无策:
- 实时绘制温度变化曲线(非固定数据点)
- 动态生成二维码或条形码
- 创建可变色的矢量图标(比如电池电量不同颜色不同)
- 做一个模拟仪表盘,指针要平滑旋转
- 实现简单的动画背景,比如流动的粒子效果
这时候,你就不再是在“使用控件”,而是在“创造视觉元素”。
而Canvas的本质,就是一块内存中的“离屏缓冲区”——你可以把它理解为一张空白的画纸,LVGL允许你在上面任意涂鸦,画完后再贴到屏幕上。整个过程完全由你控制,不受任何控件逻辑限制。
更重要的是,它不是裸写帧缓冲!LVGL已经为你封装了抗锯齿、裁剪、Alpha混合、颜色格式转换等底层细节。你只需要调用高级API,就能写出既高效又美观的绘图代码。
Canvas是怎么工作的?三步走清逻辑链
别被“画布”这个词迷惑了,它的背后其实是一套严谨的内存与渲染机制。我们可以把它的运作拆成三个阶段:
第一步:创建画布对象 + 分配缓冲区
lv_obj_t *canvas = lv_canvas_create(lv_scr_act());这行代码会在当前屏幕上创建一个Canvas对象。但它此时还“没有内容”,因为你还没告诉它:你的画布有多大?用什么颜色格式?内存从哪来?
所以紧接着要绑定缓冲区:
static lv_color_t canvas_buffer[CANVAS_WIDTH * CANVAS_HEIGHT]; lv_canvas_set_buffer(canvas, canvas_buffer, CANVAS_WIDTH, CANVAS_HEIGHT, LV_IMG_CF_TRUE_COLOR);这里的关键点是:
- 缓冲区必须是你自己预先分配的(推荐静态分配,避免堆碎片)
- 尺寸必须匹配宽高
- 颜色格式决定每像素占多少字节(如RGB888=3字节,ARGB8888=4字节)
⚠️ 常见坑点:忘记初始化缓冲区会导致画面花屏。建议每次使用前先清屏。
第二步:调用绘图API开始“作画”
Canvas本身不会自动画任何东西,一切都要靠你主动调用绘图函数。LVGL提供了一系列以lv_canvas_draw_*开头的API:
| 函数 | 功能 |
|---|---|
lv_canvas_draw_pixel() | 画单个像素 |
lv_canvas_draw_line() | 画直线(支持抗锯齿) |
lv_canvas_draw_rect() | 画矩形(可填充/描边/圆角) |
lv_canvas_draw_arc() | 画圆弧或扇形 |
lv_canvas_draw_text() | 渲染文本(支持字体、对齐) |
lv_canvas_draw_img() | 绘制图像片段 |
这些函数都基于一个统一的设计理念:描述+执行。
什么意思?你看下面这段代码:
lv_draw_rect_dsc_t rect_dsc; lv_draw_rect_dsc_init(&rect_dsc); rect_dsc.bg_color = lv_color_green(); rect_dsc.border_color = lv_color_red(); rect_dsc.border_width = 2; lv_canvas_draw_rect(canvas, 10, 10, 80, 60, &rect_dsc);我们并没有直接传颜色、线宽进去,而是先构造一个“绘图描述符”(_dsc结构体),设置好样式,再交给绘图函数去执行。这种方式的好处是:
- 样式可复用(比如多个矩形共用同一个边框风格)
- 扩展性强(未来加新属性不影响接口)
- 更符合面向对象思维
第三步:触发刷新,让画面动起来
所有绘图操作都在内存中完成,屏幕并不会立即更新。只有当LVGL检测到Canvas内容发生变化时,才会将其标记为“脏区域”(dirty area),并在下一帧重绘时将这部分合成到主画面中。
这意味着:
- 你可以批量绘制多个图形,只触发一次刷新
- 若内容不变,则不会重复渲染,节省性能
- 支持局部刷新(partial update),特别适合低功耗设备
如果你修改了画布内容并希望立即看到结果,可以手动调用:
lv_obj_invalidate(canvas); // 强制重绘或者更优雅地,结合定时器做动态更新:
lv_timer_create(update_canvas_cb, 100, canvas); // 每100ms刷新一次实战演示:动手画一个复合图形
下面我们来写一段完整的代码,展示如何在一个Canvas上组合多种元素——红色边框矩形、绿色实心圆、蓝色文字。
#include "lvgl.h" #define CANVAS_WIDTH 150 #define CANVAS_HEIGHT 100 // 静态分配缓冲区(注意:lv_color_t 默认为 RGB565 或 RGB888,取决于配置) static lv_color_t canvas_buffer[CANVAS_WIDTH * CANVAS_HEIGHT]; void demo_canvas_render(void) { // 1. 创建Canvas对象 lv_obj_t * canvas = lv_canvas_create(lv_scr_act()); lv_obj_align(canvas, LV_ALIGN_CENTER, 0, 0); // 居中显示 lv_canvas_set_buffer(canvas, canvas_buffer, CANVAS_WIDTH, CANVAS_HEIGHT, LV_IMG_CF_TRUE_COLOR); // 2. 清屏:设置背景为白色 lv_canvas_fill_bg(canvas, lv_color_white(), LV_OPA_COVER); // 3. 绘制红色矩形边框 lv_draw_rect_dsc_t rect_dsc; lv_draw_rect_dsc_init(&rect_dsc); rect_dsc.border_color = lv_color_red(); rect_dsc.border_width = 2; rect_dsc.bg_opa = LV_OPA_TRANSP; // 背景透明 lv_canvas_draw_rect(canvas, 10, 10, 80, 60, &rect_dsc); // 4. 绘制绿色实心圆(利用圆角矩形模拟) lv_draw_rect_dsc_init(&rect_dsc); rect_dsc.bg_color = lv_color_green(); rect_dsc.radius = LV_RADIUS_CIRCLE; // 设为最大圆角,即圆形 lv_canvas_draw_rect(canvas, 50, 30, 40, 40, &rect_dsc); // 5. 绘制蓝色文本 lv_draw_label_dsc_t label_dsc; lv_draw_label_dsc_init(&label_dsc); label_dsc.color = lv_color_blue(); label_dsc.font = &lv_font_montserrat_16; lv_canvas_draw_text(canvas, 20, 75, 110, &label_dsc, "Hello LVGL", LV_LABEL_ALIGN_LEFT); }📌关键说明:
- 所有坐标都是相对于画布左上角的偏移量
-lv_canvas_fill_bg()是最基础的清屏操作,相当于“铺底色”
- 圆形是通过设置radius = LV_RADIUS_CIRCLE的矩形实现的(宽度和高度相等时自动变为圆)
- 文本绘制支持自动换行和对齐,第三个参数是最大宽度
运行效果大致如下:
+----------------------------+ | | | ┌─────────────┐ | | │ │ | | │ ● | | │ | | │ Hello LVGL | | └─────────────┘ | | | +----------------------------+是不是有点像一个简易的状态面板?你可以轻松扩展它:加个电池图标、画个进度条、甚至做个小动画。
内存怎么管?颜色格式选哪个才合适?
这是初学者最容易踩坑的地方。
先算一笔账:内存占用
假设你要画一个100x100的画布:
| 格式 | 每像素字节数 | 总内存 |
|---|---|---|
LV_IMG_CF_TRUE_COLOR(RGB888) | 3 | 30,000 bytes ≈ 30KB |
LV_IMG_CF_TRUE_COLOR_ALPHA(ARGB8888) | 4 | 40,000 bytes ≈ 40KB |
LV_IMG_CF_GRAY_8 | 1 | 10,000 bytes ≈ 10KB |
听起来不多?但在STM32F4这类仅有128KB RAM的芯片上,40KB已经是不可忽视的开销了。
如何选择颜色格式?
| 场景 | 推荐格式 | 理由 |
|---|---|---|
| 彩色图形、照片合成 | LV_IMG_CF_TRUE_COLOR_ALPHA | 支持透明通道,适合叠加 |
| 单色图标、状态指示 | LV_IMG_CF_TRUE_COLOR | 节省25%内存,无需透明 |
| 极端资源受限 | LV_IMG_CF_INDEXED_8_BIT+ 调色板 | 只需1字节/像素,调色板仅256色 |
💡 小技巧:如果只是画简单图形(如线条、文本),完全可以使用
LV_IMG_CF_TRUE_COLOR,除非你明确需要半透明效果。
最佳实践建议
- 优先静态分配缓冲区,避免频繁malloc/free导致内存碎片
- 尽量复用Canvas对象,不要每次重绘都新建
- 大画布考虑分块绘制,配合局部刷新降低带宽压力
- 调试时可导出buffer为BMP文件,验证绘图是否正确
它能做什么?这些应用场景你一定用得上
别以为Canvas只是“炫技工具”,它在实际项目中有大量高价值用途:
✅ 实时数据可视化
比如工业HMI中的趋势图,每秒采集一次温度,你可以:
- 在Canvas上维护一条历史曲线数组
- 每次新增数据点后,重新绘制整条折线
- 利用抗锯齿让曲线更平滑
✅ 动态图标生成
传统做法是准备多张PNG图切换,而用Canvas可以:
- 根据电量动态绘制电池图标(空/1/2/3格)
- 根据信号强度画天线柱状图
- 图标颜色随主题变化而实时重绘
✅ 二维码/条形码生成
无需额外库,直接根据算法:
- 计算黑白矩阵
- 用lv_canvas_draw_rect()逐块填充像素
- 生成后作为图像控件显示
✅ 自定义动画背景
比如做一个呼吸灯效果:
- 在Canvas上绘制渐变圆
- 用定时器周期性改变中心颜色
- 实现柔和的明暗变化
✅ 图像合成与滤镜
虽然LVGL不是Photoshop,但基础处理完全可以:
- 将两张图片按Alpha混合
- 对区域进行灰度化处理
- 添加简单阴影效果(偏移+半透明填充)
高手才知道的几个“秘籍”
秘籍一:如何提升绘图效率?
- 只重绘变化区域:不要每次都全刷,可以用
lv_area_t指定更新范围 - 缓存常用图形:比如固定背景图只画一次,前景动态叠加
- 关闭抗锯齿(若不需要):某些直线/矩形可提速
秘籍二:如何实现“擦除”效果?
Canvas没有“撤销”功能,但你可以:
- 保存一份原始背景副本
- 每次重绘前先恢复背景
- 再绘制新内容
或者更聪明地:只清除局部区域后重画那一部分。
秘籍三:如何与其他控件交互?
Canvas本质仍是lv_obj_t子类,因此你可以:
- 添加点击事件:lv_obj_add_event_cb(canvas, on_click, LV_EVENT_CLICKED, NULL);
- 设置动画:比如让画布整体淡入淡出
- 应用样式:加边框、阴影、圆角等
这意味着它不仅能“画”,还能“动”、能“响应”。
写在最后:Canvas不止是功能,更是一种思维方式
掌握Canvas的意义,远不止学会几个API那么简单。
它代表了一种从“使用控件”到“创造界面”的思维跃迁。当你不再依赖预制组件,而是能够亲手绘制每一个像素时,你就真正掌握了LVGL的灵魂。
对于嵌入式开发者而言,这尤为重要。我们面对的往往是资源紧张、需求多变的场景。与其苦苦寻找“有没有现成控件”,不如学会“我自己能不能画出来”。
而LVGL的Canvas,正是那支让你放手创作的画笔。
如果你正在做以下类型的项目,不妨试试加入Canvas:
- 智能穿戴设备的个性化表盘
- 工业仪表的实时趋势图
- 教学类产品的互动绘图板
- 开源硬件的调试可视化界面
你会发现,原来在MCU上做出“媲美手机UI”的效果,并没有那么遥远。
👉 下一步你可以尝试:用Canvas实现一个秒针转动的模拟时钟,或者画一个随音量跳动的柱状图。欢迎在评论区分享你的作品!