/* *********************************************************
* 多级菜单v2.0
* 作者:Adam
*
* 移植方法
*
* 1. 配置菜单宏定义, 兼容不同显示器及布局, 例如:
#define MENU_WIDTH 128 // 菜单宽度
#define MENU_HEIGHT 64 // 菜单高度
#define MENU_LINE_H 20 // 行高
* 2. 实现 menu_command_callback() 对应的指令功能以完成移植, 有些指令是有参数的, 参数已经提取好, 按需使用参数即可,
* 详情可看优快云博客:https://blog.youkuaiyun.com/AdminAdam/article/details/138232161
* 使用方法
*
* 1. 创建选项列表,并直接初始化, 每个选项对应其名字和功能(功能为函数指针, 直接填写函数名), 例如:
static MENU_OptionTypeDef MENU_OptionList[] = {
{"<<<", NULL}, // 固定格式, 用于退出
{"Tools", MENU_RunToolsMenu}, // 工具
{"Games", MENU_RunGamesMenu}, // 游戏
{"Setting", MENU_RunSettingMenu}, // 设置
{"Information", MENU_Information}, // 信息
{"..", NULL}, // 固定格式, 用于计算选项列表长度和退出
};
* 2. 创建菜单句柄 并把菜单句柄内的选项列表指针指向 第1 步创建的选项列表, 例如
static MENU_HandleTypeDef MENU = {.OptionList = MENU_OptionList};
* 3. 调用 MENU_RunMenu() 运行菜单, 参数为菜单句柄
MENU_RunMenu(&MENU);
*
* 4. 为了实现多级菜单, 可使用一个函数来封装 第 1 2 3 步, 封装好的函数可作为功能被其他菜单调用, 以此实现不限层级多级菜单, 此文件底部提供了示例代码
* 例如 void MENU_RunToolsMenu(void) 被选项 {"Tools", MENU_RunToolsMenu} 调用;
*
*
* 视频教程:https://www.bilibili.com/video/BV1Y94y1g7mu?p=2
* 优快云博客:https://blog.youkuaiyun.com/AdminAdam/article/details/138232161
*
* 下载链接
* 百度网盘:https://pan.baidu.com/s/1bZPWCKaiNbb-l1gpAv6QNg?pwd=KYWS
* Gitee: https://gitee.com/AdamLoong/Embedded_Menu_Simple
* GitHub:https://github.com/AdamLoong/Embedded_Menu_Simple
*
* B站UP:加油哦大灰狼
* 如果此程序对你有帮助记得给个一键三连哦! ( •̀ ω •́ )✧
********************************************************* */
#include "MENU.h"
#include "stdio.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include "usart.h"
static void UART_Printf(const char *format, ...)
{
char tmp[128];
va_list argptr;
va_start(argptr, format);
vsprintf((char* )tmp, format, argptr);
va_end(argptr);
HAL_UART_Transmit(&huart1, (const uint8_t *)&tmp, strlen(tmp), HAL_MAX_DELAY);
}
/* 配置菜单 */
#define MENU_X 0 // 菜单位置X坐标
#define MENU_Y 0 // 菜单位置Y坐标
#define MENU_WIDTH 128 - 1 // 菜单宽度
#define MENU_HEIGHT 64 - 1 // 菜单高度
#define MENU_LINE_H 16 // 每行高度
#define MENU_PADDING 1 // 内边距
#define MENU_MARGIN 1 // 外边距
#define MENU_FONT_W 8 // 字体宽度
#define MENU_FONT_H 16 // 字体高度
#define MENU_BORDER 1 // 边框线条尺寸
#define IS_CENTERED 1 // 是否居中显示
#define IS_OVERSHOOT 1 // 是否启用过冲效果(果冻效果)
// #define OVERSHOOT 0.321 // 过冲量 0 < 范围 < 1
#define OVERSHOOT 0.081 // 过冲量 0 < 范围 < 1
// #define ANIMATION_SPEED 0.321 // 动画速度 0 < 范围 <= 1
#define ANIMATION_SPEED 0.821 // 动画速度 0 < 范围 <= 1
// 计算可见的最大行数(光标限位)
#define CURSOR_CEILING (((MENU_HEIGHT - MENU_MARGIN - MENU_MARGIN) / MENU_LINE_H) - 1)
/** Port 移植接口 * **************************************************************/
/* 依赖头文件 */
#include "AdamLib_Button.h" // 按键
#include "oled.h"
// 按键结果
extern int32_t result1;
extern int32_t result2;
extern int32_t result3;
/// @brief 菜单指令回调函数
/// @param command 指令类型
/// @param ... 可变参数列表,根据指令定义
/// @return 返回值根据指令定义
int menu_command_callback(enum _menu_command command, ...)
{
int retval = 0;
switch (command)
{
/* Output 输出相关指令 */
case BUFFER_DISPLAY: // 无参无返,更新显示缓冲区
{
OLED_ShowFrame(); // 调用OLED更新函数
}
break;
case BUFFER_CLEAR: // 无参无返,清除显示缓冲区
{
OLED_NewFrame(); // 调用OLED清除函数
}
break;
case SHOW_STRING: // 显示字符串
// 参数:( int16_t x, int16_t y, char *str )
// 返回: uint8_t 字符串长度
{
/* 提取参数列表 */
int* arg_list = ((int*)&command) + 1; // 指针偏移4字节, 指向第一个参数
int show_x = arg_list[0]; // X坐标
int show_y = arg_list[1]; // Y坐标
char* show_string = (char*)arg_list[2]; // 要显示的字符串
/* 按需使用参数 */
// 调用OLED显示函数并返回字符串长度
// retval = OLED_Printf(show_x, show_y, MENU_FONT_W, show_string);
OLED_PrintString(show_x, show_y, show_string, &font16x16, OLED_COLOR_NORMAL);
// 如果显示函数没有返回值, 可以使用strlen()获取字符串长度
// retval = strlen(show_string);
retval = strlen(show_string);
}
break;
case SHOW_CURSOR: // 显示光标
// 参数:( int xsta, int ysta, int xend, int yend )
// 返回: 无
{
/* 提取参数列表 */
int* arg_list = ((int*)&command) + 1;
int cursor_xsta = arg_list[0]; // 起始X坐标
int cursor_ysta = arg_list[1]; // 起始Y坐标
int cursor_xend = arg_list[2]; // 结束X坐标
int cursor_yend = arg_list[3]; // 结束Y坐标
/* 按需使用参数 */
// 调用OLED反转区域函数显示光标
// OLED_ReverseArea(cursor_xsta, cursor_ysta,
// COORD_CHANGE_SIZE(cursor_xsta, cursor_xend),
// COORD_CHANGE_SIZE(cursor_ysta, cursor_yend));
// 或者使用绘制矩形的方式显示光标
OLED_DrawRectangle(cursor_xsta, cursor_ysta,
COORD_CHANGE_SIZE(cursor_xsta, cursor_xend),
COORD_CHANGE_SIZE(cursor_ysta, cursor_yend), 0);
}
break;
case DRAW_FRAME: // 绘制边框
// 参数:( int xsta, int ysta, int wide, int high )
// 返回: 无
{
/* 提取参数列表 */
int* arg_list = ((int*)&command) + 1;
int frame_x = arg_list[0]; // X坐标
int frame_y = arg_list[1]; // Y坐标
int frame_width = arg_list[2]; // 宽度
int frame_height = arg_list[3]; // 高度
/* 按需使用参数 */
// 调用OLED绘制矩形函数
OLED_DrawRectangle(frame_x, frame_y, frame_width, frame_height, 0);
}
break;
/* Input 输入相关指令 */
case GET_EVENT_ENTER: // 获取确认事件
// 参数: 无
// 返回: 布尔值(1表示有事件,0表示无事件)
{
// 确认事件可以由Enter键或Right键触发
// retval = Key_GetEvent_Enter() || Key_GetEvent_Right();
if(result3 == 1)
{
// UART_Printf("Enter\r\n");
retval = 1;
}
}
break;
case GET_EVENT_BACK: // 获取返回事件
// 参数: 无
// 返回: 布尔值
{
// 返回事件由Back键触发(3长按)
if(result3 == 9){
// UART_Printf("Back\r\n");
retval = 1;
}
}
break;
case GET_EVENT_WHEEL: // 获取滚轮/方向键事件
{
// 检查方向键
if (result1 > 0) {
// 单击移动1行,长按时间越长移动越快
retval = 1 + (result1 > 100 ? (result1 / 200) : 0);
}
else if (result2 > 0) {
retval = -1 - (result2 > 100 ? (result2 / 200) : 0);
}
}
break;
// case GET_EVENT_WHEEL: // 获取滚轮/方向键事件
// // 参数: 无
// // 返回: int16_t 滚动量(正数向上,负数向下)
// {
// // 检查方向键
// if (result1 == 1){
// retval = 1;} // 向上
// else if (result2 == 1){
// retval = -1; } // 向下
// else
// // 也可以支持编码器
// // if()
// ;
// // retval = Key_Encoder_Take(&Encoder1);
// }
// break;
default:
break;
}
return retval;
}
/* ***************************************************** Port 移植接口 ** */
/* ******************************************************** */
/// @brief 菜单运行函数
/// @param hMENU 菜单句柄
void MENU_RunMenu(MENU_HandleTypeDef* hMENU)
{
MENU_HandleInit(hMENU); // 初始化菜单句柄
// 菜单主循环
while (hMENU->isRun)
{
Button_Process(); // 处理按键(重要)
menu_command_callback(BUFFER_CLEAR); // 擦除显示缓冲区
MENU_ShowOptionList(hMENU); /* 显示选项列表 */
MENU_ShowCursor(hMENU); /* 显示光标 */
MENU_ShowBorder(hMENU); // 显示边框
menu_command_callback(BUFFER_DISPLAY); // 将缓冲区内容更新到显示器
MENU_Event_and_Action(hMENU); // 检查用户事件并执行相应操作
}
}
/// @brief 初始化菜单句柄
/// @param hMENU 菜单句柄
void MENU_HandleInit(MENU_HandleTypeDef* hMENU)
{
hMENU->isRun = 1; // 菜单运行标志
hMENU->AnimationUpdateEvent = 1; // 动画更新事件标志
hMENU->Catch_i = 1; // 默认选中第一个有效选项(跳过"<<<")
hMENU->Cursor_i = 0; // 光标初始位置
hMENU->Show_i = 0; // 显示起始索引
hMENU->Show_i_Previous = 1; // 上一次显示的起始索引
hMENU->Option_Max_i = 0; // 选项列表长度
// 计算选项列表长度(直到遇到".."结束标志)
for (hMENU->Option_Max_i = 0; hMENU->OptionList[hMENU->Option_Max_i].String[0] != '.';
hMENU->Option_Max_i++)
{
// 获取每个选项的字符串长度
hMENU->OptionList[hMENU->Option_Max_i].StrLen =
MENU_ShowOption(0, 0, &hMENU->OptionList[hMENU->Option_Max_i]);
}
hMENU->Option_Max_i--; // 不包含".."选项
}
/// @brief 检查用户事件并执行相应操作
/// @param hMENU 菜单句柄
void MENU_Event_and_Action(MENU_HandleTypeDef* hMENU)
{
// 检查确认事件
if (menu_command_callback(GET_EVENT_ENTER))
{
/* 如果选中的选项有功能函数则执行,否则退出菜单 */
if (hMENU->OptionList[hMENU->Catch_i].func != NULL)
{
hMENU->OptionList[hMENU->Catch_i].func(); // 执行选项功能
}
else
{
hMENU->isRun = 0; // 退出菜单
}
hMENU->AnimationUpdateEvent = 1; // 触发动画更新
}
// 检查返回事件
else if (menu_command_callback(GET_EVENT_BACK))
{
hMENU->isRun = 0; // 退出菜单
}
else
{
// 获取滚轮/方向键事件
hMENU->Wheel_Event = -menu_command_callback(GET_EVENT_WHEEL);
// 如果有滚动事件
if (hMENU->Wheel_Event)
{
MENU_UpdateIndex(hMENU); // 更新选中索引和光标位置
hMENU->AnimationUpdateEvent = 1; // 触发动画更新
}
}
}
/// @brief 更新菜单索引(选中项和光标位置)
/// @param hMENU 菜单句柄
void MENU_UpdateIndex(MENU_HandleTypeDef* hMENU)
{
/* 更新索引 */
hMENU->Cursor_i += hMENU->Wheel_Event; // 更新光标索引
hMENU->Catch_i += hMENU->Wheel_Event; // 更新选中项索引
/* 限制选中项索引范围 */
if (hMENU->Catch_i > hMENU->Option_Max_i)
hMENU->Catch_i = hMENU->Option_Max_i; // 最大索引限制
if (hMENU->Catch_i < 0)
hMENU->Catch_i = 0; // 最小索引限制
/* 限制光标索引范围 */
if (hMENU->Cursor_i > CURSOR_CEILING)
hMENU->Cursor_i = CURSOR_CEILING; // 最大可见行限制
if (hMENU->Cursor_i > hMENU->Option_Max_i)
hMENU->Cursor_i = hMENU->Option_Max_i; // 选项总数限制
if (hMENU->Cursor_i > hMENU->Catch_i)
hMENU->Cursor_i = hMENU->Catch_i; // 不能超过选中项
if (hMENU->Cursor_i < 0)
hMENU->Cursor_i = 0; // 最小索引限制
}
/// @brief 显示选项列表
/// @param hMENU 菜单句柄
void MENU_ShowOptionList(MENU_HandleTypeDef* hMENU)
{
static float VerticalOffsetBuffer; // 垂直偏移缓冲(用于动画)
/* 计算显示起始索引 */
hMENU->Show_i = hMENU->Catch_i - hMENU->Cursor_i;
// 如果显示起始索引有变化
if (hMENU->Show_i_Previous != hMENU->Show_i)
{
// 计算垂直偏移量(用于滚动动画)
VerticalOffsetBuffer = ((hMENU->Show_i - hMENU->Show_i_Previous) * MENU_LINE_H);
hMENU->Show_i_Previous = hMENU->Show_i; // 更新上一次显示的起始索引
}
// 如果有垂直偏移,逐渐归零(实现平滑滚动)
if (VerticalOffsetBuffer)
{
VerticalOffsetBuffer = STEPWISE_TO_TARGET(VerticalOffsetBuffer, 0, ANIMATION_SPEED);
}
// 遍历并显示选项
for (int16_t i = -1; i <= CURSOR_CEILING + 1; i++)
{
// 跳过无效索引
if (hMENU->Show_i + i < 0)
continue;
if (hMENU->Show_i + i > hMENU->Option_Max_i)
break;
#if (IS_CENTERED != 0)
// 水平居中显示
int16_t x = MENU_X + ((MENU_WIDTH - (hMENU->OptionList[hMENU->Show_i + i].StrLen * MENU_FONT_W)) / 2);
#else
// 左对齐显示(带边距)
int16_t x = MENU_X + MENU_MARGIN + MENU_PADDING;
#endif
// 计算Y坐标(考虑垂直偏移)
int16_t y = MENU_Y + MENU_MARGIN + (i * MENU_LINE_H) +
((MENU_LINE_H - MENU_FONT_H) / 2) + (int)VerticalOffsetBuffer;
/* 显示选项并记录字符串长度 */
hMENU->OptionList[hMENU->Show_i + i].StrLen =
MENU_ShowOption(x, y, &hMENU->OptionList[hMENU->Show_i + i]);
}
}
/// @brief 显示单个菜单选项
/// @param X X坐标
/// @param Y Y坐标
/// @param Option 选项结构体
/// @return 字符串长度
uint8_t MENU_ShowOption(int16_t X, int16_t Y, MENU_OptionTypeDef* Option)
{
char String[64]; // 字符串缓冲区
// 根据变量类型格式化字符串
switch (Option->StrVarType)
{
case INT8:
sprintf(String, Option->String, *(int8_t*)Option->StrVarPointer);
break;
case UINT8:
sprintf(String, Option->String, *(uint8_t*)Option->StrVarPointer);
break;
case INT16:
sprintf(String, Option->String, *(int16_t*)Option->StrVarPointer);
break;
case UINT16:
sprintf(String, Option->String, *(uint16_t*)Option->StrVarPointer);
break;
case INT32:
sprintf(String, Option->String, *(int32_t*)Option->StrVarPointer);
break;
case UINT32:
sprintf(String, Option->String, *(uint32_t*)Option->StrVarPointer);
break;
case CHAR:
sprintf(String, Option->String, *(char*)Option->StrVarPointer);
break;
case STRING:
sprintf(String, Option->String, (char*)Option->StrVarPointer);
break;
case FLOAT:
sprintf(String, Option->String, *(float*)Option->StrVarPointer);
break;
default:
sprintf(String, Option->String, (void*)Option->StrVarPointer);
break;
}
// 显示字符串并返回长度
return menu_command_callback(SHOW_STRING, X, Y, String);
}
/// @brief 显示光标
/// @param hMENU 菜单句柄
void MENU_ShowCursor(MENU_HandleTypeDef* hMENU)
{
// 实际位置变量
static float actual_xsta, actual_ysta, actual_xend, actual_yend;
// 目标位置变量
static float target_xsta, target_ysta, target_xend, target_yend;
#if (IS_OVERSHOOT != 0)
// 过冲效果相关变量
static float bounce_xsta, bounce_ysta, bounce_xend, bounce_yend;
static uint8_t bounce_cnt_xsta, bounce_cnt_ysta, bounce_cnt_xend, bounce_cnt_yend;
#endif
// 如果需要更新动画
if (hMENU->AnimationUpdateEvent)
{
hMENU->AnimationUpdateEvent = 0; // 清除更新标志
// 计算光标尺寸
uint16_t cursor_width = (MENU_PADDING +
(hMENU->OptionList[hMENU->Catch_i].StrLen * MENU_FONT_W) +
MENU_PADDING);
uint16_t cursor_height = MENU_LINE_H;
#if (IS_CENTERED != 0)
// 居中显示时光标的X坐标
target_xsta = MENU_X + ((MENU_WIDTH - cursor_width) / 2);
#else
// 左对齐显示时光标的X坐标
target_xsta = MENU_X + MENU_MARGIN;
#endif
// 光标Y坐标
target_ysta = MENU_Y + MENU_MARGIN + (hMENU->Cursor_i * MENU_LINE_H);
// 光标结束坐标
target_xend = SIZE_CHANGE_COORD(target_xsta, cursor_width);
target_yend = SIZE_CHANGE_COORD(target_ysta, cursor_height);
#if (IS_OVERSHOOT != 0)
// 计算过冲目标位置
bounce_xsta = target_xsta + (target_xsta - actual_xsta) * OVERSHOOT;
bounce_ysta = target_ysta + (target_ysta - actual_ysta) * OVERSHOOT;
bounce_xend = target_xend + (target_xend - actual_xend) * OVERSHOOT;
bounce_yend = target_yend + (target_yend - actual_yend) * OVERSHOOT;
// 设置反弹次数
bounce_cnt_xsta = 2;
bounce_cnt_ysta = 2;
bounce_cnt_xend = 2;
bounce_cnt_yend = 2;
#endif
}
#if (IS_OVERSHOOT != 0)
// 处理X方向过冲
if (bounce_xsta == actual_xsta)
{
if (bounce_cnt_xsta--)
bounce_xsta = target_xsta + (target_xsta - actual_xsta) * OVERSHOOT;
else
bounce_xsta = target_xsta;
}
// 处理Y方向过冲
if (bounce_ysta == actual_ysta)
{
if (bounce_cnt_ysta--)
bounce_ysta = target_ysta + (target_ysta - actual_ysta) * OVERSHOOT;
else
bounce_ysta = target_ysta;
}
// 处理X结束位置过冲
if (bounce_xend == actual_xend)
{
if (bounce_cnt_xend--)
bounce_xend = target_xend + (target_xend - actual_xend) * OVERSHOOT;
else
bounce_xend = target_xend;
}
// 处理Y结束位置过冲
if (bounce_yend == actual_yend)
{
if (bounce_cnt_yend--)
bounce_yend = target_yend + (target_yend - actual_yend) * OVERSHOOT;
else
bounce_yend = target_yend;
}
// 平滑过渡到过冲位置
actual_xsta = STEPWISE_TO_TARGET(actual_xsta, bounce_xsta, ANIMATION_SPEED);
actual_ysta = STEPWISE_TO_TARGET(actual_ysta, bounce_ysta, ANIMATION_SPEED);
actual_xend = STEPWISE_TO_TARGET(actual_xend, bounce_xend, ANIMATION_SPEED);
actual_yend = STEPWISE_TO_TARGET(actual_yend, bounce_yend, ANIMATION_SPEED);
#else
// 直接平滑过渡到目标位置
actual_xsta = STEPWISE_TO_TARGET(actual_xsta, target_xsta, ANIMATION_SPEED);
actual_ysta = STEPWISE_TO_TARGET(actual_ysta, target_ysta, ANIMATION_SPEED);
actual_xend = STEPWISE_TO_TARGET(actual_xend, target_xend, ANIMATION_SPEED);
actual_yend = STEPWISE_TO_TARGET(actual_yend, target_yend, ANIMATION_SPEED);
#endif
// 显示光标(四舍五入到整数坐标)
menu_command_callback(SHOW_CURSOR, (int)(actual_xsta + 0.5), (int)(actual_ysta + 0.5),
(int)(actual_xend + 0.5), (int)(actual_yend + 0.5));
}
/// @brief 显示菜单边框
/// @param hMENU 菜单句柄
void MENU_ShowBorder(MENU_HandleTypeDef* hMENU)
{
// 绘制多层边框(根据MENU_BORDER设置)
for (int16_t i = 0; i < MENU_BORDER; i++)
{
menu_command_callback(DRAW_FRAME, MENU_X + i, MENU_Y + i,
MENU_WIDTH - i - i, MENU_HEIGHT - i - i);
}
}
/* ******************************************************** */
/* ******************************************************** */
/* 应用示例 */
/// @brief 运行主菜单
void MENU_RunMainMenu(void)
{
// 定义主菜单选项列表
static MENU_OptionTypeDef MENU_OptionList[] = {
{"<<<"}, // 返回上级菜单
{"Tools", MENU_RunToolsMenu}, // 工具菜单
{"Games", MENU_RunGamesMenu}, // 游戏菜单
{"Setting", NULL}, // 设置菜单
{"Information", MENU_Information}, // 信息显示
{"Setting2", NULL}, // 设置菜单
{".."} // 结束标志
};
// 创建菜单句柄
static MENU_HandleTypeDef MENU = { .OptionList = MENU_OptionList };
// 运行菜单
MENU_RunMenu(&MENU);
}
/// @brief 运行工具菜单
void MENU_RunToolsMenu(void)
{
// 定义工具菜单选项列表
static MENU_OptionTypeDef MENU_OptionList[] = {
{"<<<"}, // 返回上级菜单
{"Seria", NULL}, // 串口工具
{"Oscilloscope", NULL}, // 示波器功能
{"PWM Output", NULL}, // PWM输出
{"PWM Input", NULL}, // PWM输入
{"ADC Input", NULL}, // ADC输入
{".."} // 结束标志
};
// 创建菜单句柄
static MENU_HandleTypeDef MENU = { .OptionList = MENU_OptionList };
// 运行菜单
MENU_RunMenu(&MENU);
}
/// @brief 运行游戏菜单
void MENU_RunGamesMenu(void)
{
// 定义游戏菜单选项列表
static MENU_OptionTypeDef MENU_OptionList[] = {
{"<<<"}, // 返回上级菜单
{"Snake", NULL}, // 贪吃蛇游戏
{"Snake II", NULL}, // 贪吃蛇2
{"Snake III", NULL}, // 贪吃蛇3
{"Game of Life", NULL}, // 康威生命游戏
{".."} // 结束标志
};
// 创建菜单句柄
static MENU_HandleTypeDef MENU = { .OptionList = MENU_OptionList };
// 运行菜单
MENU_RunMenu(&MENU);
}
/// @brief 显示信息页面
void MENU_Information(void)
{
// 清除显示缓冲区
menu_command_callback(BUFFER_CLEAR);
// 显示信息内容
menu_command_callback(SHOW_STRING, 5, 0, "menu v2.0\nBy:Adam\nbilibili\nUP:加油哦大灰狼");
// 更新显示
menu_command_callback(BUFFER_DISPLAY);
// 等待用户操作
while (1)
{
Button_Process();
// 按Back键返回
if (menu_command_callback(GET_EVENT_BACK))
return;
}
}
/**********************************************************/
为什么当子菜单对应为NULL时返回上一个菜单栏时会卡死程序