从零构建嵌入式菜单库(六):导航、动画与消息框
本文介绍了嵌入式菜单库中几个关键交互功能的实现方法: 多级菜单导航系统:通过固定深度的函数指针数组模拟调用栈,实现类似浏览器的前进/后退功能,同时防止循环引用导致的无限堆叠。 动画效果系统: 提供可替换的动画接口,支持自定义效果 默认实现包含展开动画和平滑滚动效果 滚动速度根据选中项位置动态调整 消息框超时机制:基于简单的计时器实现自动关闭功能,不依赖操作系统定时器。 这些功能将基础菜单升级为具有
从零构建嵌入式菜单库(六):导航、动画与消息框
系列定位:这是一套编写教程。
本篇把最后几个"高级交互"模块串起来——子菜单调用链追溯、展开动画、超长文本滚动、消息框。这些功能让菜单从"能用"变成"顺手"。
前言:从"单页菜单"到"多级导航"的鸿沟
原型只有一个菜单页,子菜单靠全局变量手搓——每次新增一个页面就要加一个 void subMenu_xxx() 函数和对应的 if/else 分支。随着页面增加到 5 个、10 个、20 个,维护成本指数级上升。
我们需要的是一种自动追溯调用链的机制——类似浏览器的前进/后退,但深度限制在嵌入式可承受范围内。
知识点预备
1.1 固定深度调用栈模拟
#define TRACE_MAX_DEPTH 16
menuItem_cb trace[TRACE_MAX_DEPTH]; // 函数指针数组
// 推入(进入子菜单)
void push(menuItem_cb item) {
for (int i = 0; i < TRACE_MAX_DEPTH; i++) {
if (trace[i] == item) {
// 要去的地方已在调用链中 → 截断循环引用
trace[i] = NULL;
// 直接跳转
break;
}
if (trace[i]) continue; // 找空位
trace[i] = current; // 保存当前位置
if (i + 1 < TRACE_MAX_DEPTH)
trace[i + 1] = NULL; // 截断后续
break;
}
}
// 弹出(返回父菜单)
void pop() {
int i;
for (i = 0; i < TRACE_MAX_DEPTH && trace[i]; i++);
if (i <= 0) return; // 已经在根页面
menuItem_cb parent = trace[i - 1];
trace[i - 1] = NULL;
// 跳转到 parent
}
这是"用数组模拟栈"的经典模式。固定深度用宏 U8G2_MENU_TRACE_MAX_DEPTH 控制。
1.2 线性追击动画
在第 1 篇原型分析中我们见过:
if (ABS(target - current) > speed)
current += (target > current) ? speed : -speed;
else
current = target;
这个算法用到了滚动动画和行高动画——“永远向目标追,不到就继续追”。
1.3 消息框超时
uint32_t timer_left = timeout;
void onTick(uint16_t ms) {
if (timer_left > ms)
timer_left -= ms;
else
timer_left = 0; // 超时
}
// 当 timer_left == 0 时关闭弹窗
极其简单,但极其可靠——没有依赖操作系统定时器,只依赖用户提供的 Tick 接口。
2. 子菜单追溯系统
2.1 数据结构
struct u8g2_menu_struct {
// ...
menuItem_cb menuItemTrace[U8G2_MENU_TRACE_MAX_DEPTH]; // 16 层
};
menuItemTrace 是 menuItem_cb(函数指针)的定长数组。非 NULL 表示"这个位置记录了一个父页面"。
2.2 推入(Enter)
void u8g2_MenuItemEnter(u8g2_menu_t *u8g2_menu, menuItem_cb menuItem)
{
for (size_t i = 0; i < U8G2_MENU_TRACE_MAX_DEPTH; i++)
{
// 如果目标已在调用链中(循环引用检测)
if (u8g2_menu->menuItemTrace[i] == menuItem) {
u8g2_menu->menuItemTrace[i] = NULL;
u8g2_MenuReplaceItem(u8g2_menu, menuItem);
break;
}
// 找到第一个空位
if (u8g2_menu->menuItemTrace[i]) continue;
u8g2_menu->menuItemTrace[i] = u8g2_menu->menuItem; // 保存当前页
if (i + 1 < U8G2_MENU_TRACE_MAX_DEPTH)
u8g2_menu->menuItemTrace[i + 1] = NULL; // 截断
u8g2_MenuReplaceItem(u8g2_menu, menuItem);
break;
}
}
循环引用检测:如果用户从 A→B→C→A,第三次进入 A 时会发现 A 已在 trace 中,于是截断 C 并直接跳转。防止无限堆叠。
2.3 弹出(Back)
void u8g2_MenuItemBack(u8g2_menu_t *u8g2_menu)
{
size_t i;
for (i = 0; i < U8G2_MENU_TRACE_MAX_DEPTH; i++)
if (u8g2_menu->menuItemTrace[i] == NULL) break;
if (i <= 0) return; // 已在根页面,无法再返回
i -= 1;
u8g2_MenuReplaceItem(u8g2_menu,
u8g2_menu->menuItemTrace[i]); // 恢复到父页面
u8g2_menu->menuItemTrace[i] = NULL;
}
2.4 menu 与 menu_enter 的区别
case MENU_menu:
u8g2_MenuReplaceItem(u8g2_menu,
u8g2_menu->u8g2_menuValue.menu.menuItem);
break;
// → 替换当前页,不保存调用链(像"跳转"按钮)
case MENU_menu_enter:
u8g2_MenuItemEnter(u8g2_menu,
u8g2_menu->u8g2_menuValue.menu.menuItem);
break;
// → 推入调用链,可返回(像"进入"按钮)
menu 是扁平跳转(A→B,不能返回),menu_enter 是层级导航(A→B,可返回 A)。多级菜单应使用 menu_enter。
3. 动画效果器系统
3.1 可替换接口
struct u8g2_menu_effect_struct {
u8g2_int_t (*u8g2_menuEffect_init)(u8g2_menu_t *u8g2_menu); // 初始化
u8g2_int_t (*u8g2_menuEffect_run)(u8g2_menu_t *u8g2_menu); // 逐帧运行
u8g2_int_t _position; // 当前滚动位置
float _rowHeight; // 当前行高比例
};
用户可以写自己的效果器并注册:
u8g2_menu_effect_t myEffect = {myInit, myRun};
u8g2_MenuEffectBind(&menu, &myEffect);
3.2 默认效果:展开动画 + 平滑滚动
// 初始化
u8g2_int_t u8g2_Menu_init(u8g2_menu_t *u8g2_menu)
{
u8g2_menu->menuEffect._rowHeight = ROW_HEIGHT_INCREMENT; // 从 0.2 开始
return 0;
}
// 逐帧运行
u8g2_int_t u8g2_Menu_run(u8g2_menu_t *u8g2_menu)
{
// 1. 行高渐变
if (u8g2_menu->menuEffect._rowHeight < MAX_ROW_HEIGHT)
u8g2_menu->menuEffect._rowHeight += ROW_HEIGHT_INCREMENT;
else
u8g2_menu->menuEffect._rowHeight = MAX_ROW_HEIGHT;
// 2. 滚动追击:计算速度调节量
float spe_adj =
(u8g2_menu->pickItemY >= u8g2_menu->currentHeight + u8g2_menu->currentY)
? (u8g2_menu->pickItemY - u8g2_menu->currentHeight
- u8g2_menu->currentY) / SPE_ADJUSTMENT + 1
: (u8g2_menu->pickItemY - u8g2_menu->pickItemHeight
<= u8g2_menu->currentY)
? (u8g2_menu->pickItemY - u8g2_menu->pickItemHeight
- u8g2_menu->currentY) / SPE_ADJUSTMENT - 1
: 0;
u8g2_menu->menuEffect._position += spe_adj;
u8g2_menu->pickItemY += spe_adj;
return 0;
}
spe_adj 的设计思路:滚动速度与"选中项离可视区域边界的距离"成正比。选中项被遮挡得越多,滚动越快。
可视区 可视区
┌────────┐ ┌────────┐
│ item 1 │ │ item 2 │ ← 部分遮挡 → spe_adj = (遮挡量)/2 + 1
│ item 2 │ ← 选中 │ item 3 │
│ item 3 │ │ item 4 │
└────────┘ └────────┘
选中在可视区内 → spe_adj=0 选中被遮挡 → 加速滚动
3.3 动画帧率控制
// 在 u8g2_DrawMenu 中:
if (!u8g2_menu->timer_effective)
u8g2_menuEffect_run_call(u8g2_menu); // 未启用定时器 → 每帧都跑
if (u8g2_menu->timer_effective && u8g2_menu->timer > U8G2_MENU_DELAY) {
u8g2_menu->timer -= U8G2_MENU_DELAY;
u8g2_menuEffect_run_call(u8g2_menu); // 每 100ms 跑一次
}
timer 由 u8g2_MenuTime_ISR 驱动(定时中断中调用)。如果用户不提供定时器,退化为每帧运行。
4. 超长文本自动滚动
菜单项的文字可能超出屏幕宽度——比如一个 Wi-Fi SSID “My_Really_Long_WiFi_Name_That_Doesnt_Fit”。
4.1 "平移头"概念
float positionOffset; // 目标滚动位置(字符单位)
float _positionOffset; // 实际滚动位置
float positionOffset_spe; // 滚动速度
float positionOffset_strHeaderLen; // 平移头长度(字符数)
"平移头"是一个虚拟的指示器,领先于文本滚动。当它到达屏幕最右侧后,文本才开始向左滚动。这样保证每个字符在屏幕上停留的时间大致均匀。
时刻 t0: [Hello World] 平移头在右侧边界之外
时刻 t1: [Hello Worl|d] 平移头到达边界,文本开始滚动
时刻 t2: [ello Worl|d] 平移头保持在边界
时刻 t3: [lo World |] 平移头领先文本
4.2 核心逻辑(在 u8g2_MenuSelectorCall 中)
if (u8g2_menu->currentDrawItem == u8g2_menu->currentItem)
{
if (u8g2_menu->currentItemWidth > w) // 宽度超屏
{
u8g2_menu->positionOffset -= u8g2_menu->positionOffset_spe;
// 判断是否完成一次滚动(回到起点)
if (u8g2_menu->positionOffset * maxCharWidth
+ u8g2_menu->currentItemWidth
<= u8g2_menu->positionOffset_strHeaderLen)
{
u8g2_menu->positionOffset
= u8g2_menu->positionOffset_strHeaderLen; // 循环
}
// 转换为实际像素偏移
if (u8g2_menu->positionOffset > 0)
u8g2_menu->_positionOffset = 0; // 平移头阶段
else if (...<= w)
u8g2_menu->_positionOffset = (float)(w - itemWidth) / maxCharWidth;
else
u8g2_menu->_positionOffset = u8g2_menu->positionOffset;
}
}
5. 消息框系统
5.1 通用接口
void u8g2_MenuDrawMessageBox(u8g2_menu_t *u8g2_menu,
u8g2_MenuDrawMessageBox_cb drawMessageBox,
void *message,
u8g2_uint_t messageBoxWidth, u8g2_uint_t messageBoxHeight,
uint32_t drawMessageBoxTimer);
消息框内容由回调函数绘制,内容数据通过 void *message 传递——可以是字符串、结构体、图片数据。
5.2 快捷方式
// 文字弹窗
void u8g2_MenuDrawMessageBox_str(u8g2_menu_t *u8g2_menu,
const char *str, uint32_t drawMessageBoxTimer);
// 图片弹窗
void u8g2_MenuDrawMessageBox_xbm(u8g2_menu_t *u8g2_menu,
u8g2_uint_t w, u8g2_uint_t h, const uint8_t *bitmap,
uint32_t drawMessageBoxTimer);
5.3 超时机制
void u8g2_MenuMessageBoxTime_ISR(u8g2_menu_t *u8g2_menu, uint16_t ms)
{
if (u8g2_menu->drawMessageBoxTimer != U8G2_MENU_INFINITE_TIMEOUT) {
if (ms < u8g2_menu->drawMessageBoxTimerLeft)
u8g2_menu->drawMessageBoxTimerLeft -= ms;
else
u8g2_menu->drawMessageBoxTimerLeft = 0;
}
}
当 drawMessageBoxTimerLeft 减到 0 时,消息框自动关闭。U8G2_MENU_INFINITE_TIMEOUT = UINT32_MAX 表示永不超时。
5.4 消息框绘制调度
// 在 u8g2_DrawMenu 末尾
u8g2_menuMessageBoxCall(u8g2_menu);
消息框在菜单内容绘制之后渲染,覆盖在菜单上方。如果设置了超时,时间由 u8g2_MenuTime_ISR 驱动。
6. 记录功能:文本化菜单内容
#define U8G2_MENU_RECORD 1
#define U8G2_MENU_RECORD_SIZE 256
void u8g2_MenuRecordClear(u8g2_menu_t *u8g2_menu) {
u8g2_menu->u8g2_menuRecordLen = 0;
u8g2_menu->u8g2_menuRecord[0] = 0;
}
void u8g2_MenuRecordAdd(u8g2_menu_t *u8g2_menu, const char *text) {
while (*text && u8g2_menu->u8g2_menuRecordLen < U8G2_MENU_RECORD_SIZE - 1)
u8g2_menu->u8g2_menuRecord[u8g2_menu->u8g2_menuRecordLen++] = *text++;
// 超长截断,加 "..."
u8g2_menu->u8g2_menuRecord[u8g2_menu->u8g2_menuRecordLen] = '\0';
}
在 u8g2_DrawMenu 中,每次绘制都会自动记录:
u8g2_MenuRecordClear(u8g2_menu);
u8g2_MenuRecordAdd(u8g2_menu, "\r\n---------------");
u8g2_menu->menuItem(); // 用户回调中的绘制被记录
u8g2_MenuRecordAdd(u8g2_menu, "\r\n");
选择器状态用符号区分:→(不可调)、⇒(可调)、==(正在编辑)。用户可以调用 u8g2_MenuRecord() 获取字符串——比如通过串口输出到 PC。
7. menuEnter/Leave 弱定义回调
WEAK void u8g2_menuItemEnter_weak(u8g2_menu_t *u8g2_menu,
u8g2_uint_t item) { /* 空 */ }
WEAK void u8g2_menuItemLeave_weak(u8g2_menu_t *u8g2_menu,
u8g2_uint_t item) { /* 空 */ }
在 u8g2_DrawMenu 中每次检测到选中项变化时触发:
if (u8g2_menu->currentItemLog != u8g2_menu->currentItem) {
u8g2_menuItemLeave_weak(u8g2_menu, u8g2_menu->currentItemLog);
u8g2_menuItemEnter_weak(u8g2_menu, u8g2_menu->currentItem);
}
典型用途:选中新菜单项时蜂鸣器短鸣、LED 闪烁,或触发传感器采样。
8. 小结
| 系统 | 核心技巧 | 限制 |
|---|---|---|
| 子菜单追溯 | 函数指针数组模拟栈 | 最大 16 层 |
| 效果器 | 可替换回调接口 | 需要用户理解接口契约 |
| 超长文本滚动 | "平移头"追击算法 | 仅水平滚动 |
| 消息框 | 通用回调 + 超时倒计时 | 依赖 ISR 时间基准 |
| 记录 | 文本缓冲 + 超长截断 | 256 字节上限 |
| Enter/Leave | 弱定义钩子 | 用户需自行重写 |
核心教训:这些"高级功能"的代码量都不大(每个模块 100~300 行),但投入产出比极高——因为它们解决的都是"用的时候才发现缺"的问题。16 层追溯在 99% 的场景下够用,但如果做一个文件管理器式的菜单树,就需要评估是否放宽限值。
下一篇——最后一篇——我们将把所有这些模块打包成一个正式的开源项目,处理跨编译器兼容、配置宏系统、开源许可证和工程化。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)