前言
一般来说使用12864进行显示,配合按键作为人机界面交互,如果显示的内容较多的话,往往不能在一个页面显示完全,这里就需要用到多级菜单来进行管理,根据显示内容的不同,将其划分成不同的菜单,通过按键操作来选择不同的界面进行显示。这里介绍一种我自己使用的多级菜单实现原理和框架,并且分析一下其实现的方法,分享出来供大家参考。
一、菜单数据结构体
/************ 定义菜单列表结构体 **************/
typedef struct menu{
unsigned char menu_sum; /* 一个菜单中选项的个数 选项编号从0开始 一级最多256个选项 */
unsigned char menu_cursor_num; /* 当前选中的选项在菜单列表中的编号 */
unsigned char menu_cursor_lcd; /* 光标在屏幕上显示的位置 1~DP_NUM行 */
unsigned char menu_dp[DP_NUM]; /* 用于存储目前显示在屏幕上的3行菜单选项在各自列表中的序号 */
char **menu_str_point; /* 菜单选项名称字符串指针,指向各级菜单选项的名称,选项名称为一个指针数组 */
struct menu *menu_before; /* 指向上一级菜单列表 如果是主菜单就为NULL */
struct menu **menu_next; /* 指向下一级菜单列表 */
void (**menu_function)(); /* 函数指针 按下确认按键后 根据选项调用不同的函数执行操作 */
}menu;
多级菜单的实现最重要的就是定义一个菜单结构体,每级菜单都用一个菜单结构体来描述。
1. menu_sum表示每级菜单中的选项数量,由于其数据类型我定义为了unsigned char的类型,所以最大的选项个数类256个,基本够用了,一般没有人会设置超过256个选项吧。
2. menu_cursor_num表示光标位置,这个参数的类型需要和menu_sum的类型一致,为0时表示当前光标在第一个选项上。多级菜单势必要涉及到光标的操作,通过按键来上下移动光标,光标选中的那一行被反白显示,按下确认键之后,根据光标的位置进入到对应的下级菜单中或者执行对应的菜单函数。
3. menu_cursor_lcd用来确定当前光标在屏幕上的位置,12864一般采用3行来显示菜单列表信息,这个参数用来表示当前光标在第几行。
4. menu_dp[DP_NUM]表示当前显示在屏幕上的选项列表,DP_NUM表示屏幕最多可以显示几行列表,一般12864的液晶显示3行选项列表。0,1,2表示当前显示在屏幕上的是列表中标号为0,1,2的选项。
5. **menu_str_point指向一个字符串指针数组,数组里面存放着各个选项的名字字符串,一般定义成如下形式。然后将这个指针指向这个数组即可。
char const *menu_0_str[MENU_0_SUM] = {
{"放大增益"},
{"缩小比例"},
{"DDS频率"},
{"直流衰减"},
{"Ui峰峰值"},
{"继电器控制"}
};
6. *menu_before这个指针指向上级菜单,主菜单没有上级菜单设置为NULL,子菜单想返回上级菜单时即可通过这个指针返回上级菜单。通过这条语句即可now_menu = now_menu->menu_before;
7. **menu_next这是一个指向下级菜单列表的指针,由于一个菜单里面可能包含多个选项子菜单,每个子菜单又有可能对应一个菜单结构体,所以这个参数是一个双重指针,指向一个菜单结构体的列表。想进入光标所指的下级菜单式now_menu = now_menu->menu_next[now_menu->menu_cursor_num];
8. void (**menu_function)();回调函数指针参数,如果菜单选项对应的是一个具体的功能函数时,执行这个选项就会跳转到某个具体的功能函数去执行,一个菜单下面可能有很多的选项,每个选项都对应一个功能函数,这些函数就组成了一个函数列表,同样的用一个双重指针去描述。需要注意的是这里定义的回调函数是void fun(void)类型的函数,所以每个回调函数都必须是这个类型。
定义一个具体的菜单结构体时,可以在其中给其赋予初值,示例如下:
/*************** 数据查看 MENU_0下级菜单结构体定义 *************/
char const *menu_0_str[MENU_0_SUM] = {
{"放大增益"},
{"缩小比例"},
{"DDS频率"},
{"直流衰减"},
{"Ui峰峰值"},
{"继电器控制"}
};
menu menu_0 = {
MENU_0_SUM, /* 选项个数 */
0, /* 初始化默认指向 第0个选项 */
1, /* 屏幕上显示默认在第1行 */
{0,1,2}, /* 初始化默认显示前3个选项 */
(char **)menu_0_str, /* 指向菜单选项信息的指针数组 */
&menu_main, /* 指向上级目录 */
NULL, /* 指向下级目录 */
NULL, /* 函数指针 确认选项后操作的函数 */
};
二、菜单初始化
程序中通过一个菜单初始化函数来对所有的菜单结构体进行初始化,主要是为结构体中的双重指针申请内存,然后将双重指针指向对应的结构体和函数。
void Menu_Init(void)
{
/********************** 主菜单结构体中各成员变量初始化 **********************************/
menu_main.menu_next = Menu_Malloc( sizeof(int)* MENU_MAIN_SUM); /* 为下级菜单指针申请内存 */
/* 将主菜单结构体中的指向下级菜单的指针 一一指向各个菜单结构体 */
menu_main.menu_next[0] = &menu_0; /* 选项0 的下级菜单 没有就为NULL */
/* 将主菜单结构体中的指向下级菜单的执行函数 一一指向各个函数 */
menu_main.menu_function = Menu_Malloc( FUNC_SIZE* MENU_MAIN_SUM); /* 为选项的函数指针申请内存 */
menu_main.menu_function[0] = NULL; /* 选项0的执行函数指针 指向相对应的函数 没有就为NULL */
/*********************** menu_0菜单结构体中各成员变量初始化 *****************************/
/* menu_0菜单没有下级菜单不需要申请内存 */
menu_0.menu_next = NULL; /* 没有下级菜单了 将下级菜单指针初始化为NULL */
menu_0.menu_function = Menu_Malloc(FUNC_SIZE* MENU_0_SUM); /* 为menu_0选项的函数指针 申请内存 */
menu_0.menu_function[0] = &Func_0_0;/* 选项0的执行函数指针 指向相对应的函数 没有就为NULL */
menu_0.menu_function[1] = &Func_0_1;/* 选项1的执行函数指针 指向相对应的函数 没有就为NULL */
menu_0.menu_function[2] = &Func_0_2;/* 选项2的执行函数指针 指向相对应的函数 没有就为NULL */
menu_0.menu_function[3] = &Func_0_3;/* 选项3的执行函数指针 指向相对应的函数 没有就为NULL */
menu_0.menu_function[4] = &Func_0_4;/* 选项4的执行函数指针 指向相对应的函数 没有就为NULL */
menu_0.menu_function[5] = &Func_0_5;/* 选项5的执行函数指针 指向相对应的函数 没有就为NULL */
}
程序中有两个菜单,一个就是主菜单,主菜单下只有一个设置选项子菜单,设置菜单下有6个选项,这6个选项没有子菜单,都是具体的执行函数。首先初始化主菜单结构体,为指针申请内存,根据子菜单的数量申请内存,根据顺序指向子菜单结构体,之后为回调函数指针申请内存,由于是子菜单没有具体的执行函数,指针指向NULL。之后是设置子菜单结构体的初始化,过程是相同的,由于没有子菜单了,子菜单指针设置为NULL,设置回调函数指针指向每个选项的执行函数。
三、菜单的操作和显示
菜单的操作和显示主要通过两个函数来实现,一个就是Menu_Handler()菜单操作函数主要负责处理各种按键消息,根据不同的按键消息来进行不同的操作,还有一个就是Menu_Display()负责显示菜单列表。所有的菜单操作都是通过一个菜单结构体指针进行的,Current_Menu指针指向当前正在操作的结构体,为NULL时表示没有进行菜单操作。
menu *Current_Menu = NULL; /* 初始化时默认为NULL,不显示菜单 */
void Menu_Display(void)
{
if(Current_Menu != NULL) /* 有菜单时 */
{
/**************** 根据光标的位置显示向上和向下箭头 *******************/
if(Current_Menu->menu_sum > 1)//菜单的选项大于一个才显示箭头
{
if(Current_Menu->menu_cursor_num == 0) /* 当前的光标位置在第一个选项了,就没有那个向上箭头显示,只有一个向下箭头显示 */
{
Menu_ShowBmp(Menu_Arrow_X+8,0,Menu_Arrow_X+16,2,Menu_ShowNor,Menu_BMP_DOWN); /* 显示向下的箭头 */
Menu_Rect(Menu_Arrow_X,0,Menu_Arrow_X+8,2,0); /* 清除上箭头显示的位置 */
}
else if(Current_Menu->menu_cursor_num == (Current_Menu->menu_sum-1)) /* 光标在菜单的最后一个选项的时候,只显示向上的箭头 */
{
Menu_ShowBmp(Menu_Arrow_X,0,Menu_Arrow_X+8,2,Menu_ShowNor,Menu_BMP_UP); /* 显示向下的箭头 */
Menu_Rect(Menu_Arrow_X+8,0,Menu_Arrow_X+16,2,0); /* 清除下箭头显示的位置 */
}
else /* 其他光标在菜单中间选项时,同时显示两个箭头 */
{
Menu_ShowBmp(Menu_Arrow_X+8,0,Menu_Arrow_X+16,2,Menu_ShowNor,Menu_BMP_DOWN); /* 显示向下的箭头 */
Menu_ShowBmp(Menu_Arrow_X,0,Menu_Arrow_X+8,2,Menu_ShowNor,Menu_BMP_UP); /* 显示向下的箭头 */
}
}
if((Current_Menu->menu_cursor_lcd == 1)||(Current_Menu->menu_cursor_lcd == 2)||(Current_Menu->menu_cursor_lcd == 3)) /* 有效的光标位置 同时有选项 */
{
Menu_Display_PrintfShowClear(Menu_X,Menu_Line2,Menu_Size16,((Current_Menu->menu_cursor_lcd == 1)?Menu_ShowClear2:Menu_ShowClear0/* 光标选中的一行反白加填充 */),\
"%d.%s", Current_Menu->menu_dp[0]+1,Current_Menu->menu_str_point[Current_Menu->menu_dp[0]]); /* 显示菜单的第一个显示选项 */
if(Current_Menu->menu_sum > 1) /* 如果菜单的选项大于1 显示第二个选项 */
{
Menu_Display_PrintfShowClear(Menu_X,Menu_Line3,Menu_Size16,((Current_Menu->menu_cursor_lcd == 2)?Menu_ShowClear2:Menu_ShowClear0/* 光标选中的一行反白加填充 */),\
"%d.%s",Current_Menu->menu_dp[1]+1,Current_Menu->menu_str_point[Current_Menu->menu_dp[1]]); /* 显示菜单的第二个显示选项 */
}
if(Current_Menu->menu_sum > 2) /* 如果菜单的选项大于2 显示第三个选项 */
{
Menu_Display_PrintfShowClear(Menu_X,Menu_Line4,Menu_Size16,((Current_Menu->menu_cursor_lcd == 3)?Menu_ShowClear2:Menu_ShowClear0/* 光标选中的一行反白加填充 */),\
"%d.%s",Current_Menu->menu_dp[2]+1,Current_Menu->menu_str_point[Current_Menu->menu_dp[2]]); /* 显示菜单的第三个显示选项 */
}
}
}
}
/*****************************************
*函数名称:Menu_Handler
*输入参数:Menu_News:一般为按键消息 上、下、确认、取消 光标在菜单中只能上下移动
*返回值 :无
*功能描述:根据传递进来的消息改变菜单结构体值
*更新时间:2019-02-02
*****************************************/
void Menu_Handler(u8 Menu_News)
{
if((Menu_News == Menu_News_up)||(Menu_News == Menu_News_down)||(Menu_News == Menu_News_conf)||(Menu_News == Menu_News_cancel)) /* 判断消息的有效性,是否为有效的按键按下了 */
{
switch(Menu_News)
{
case(Menu_News_up): /* 向上按键按下 */
if(Current_Menu != NULL) /* 屏幕上已经显示了菜单 */
{
if(Current_Menu->menu_cursor_num != 0) /* 光标还没有移动到当前菜单的第一个选项 */
{
Current_Menu->menu_cursor_num--; /* 光标上移一个选项 */
if(Current_Menu->menu_cursor_lcd > 1) /* 屏上的光标显示不在第一行 */
{
Current_Menu->menu_cursor_lcd--; /* 光标上移一个选项 */
}
else /* 光标在屏幕上的第一行选项同时也不是菜单的第一个选项 需要改变屏幕上显示的三个选项 */
{
Current_Menu->menu_dp[0]--; /* 屏幕上显示的三个选项,整体上移一个选项 */
Current_Menu->menu_dp[1]--; /* 屏幕上显示的三个选项,整体上移一个选项 */
Current_Menu->menu_dp[2]--; /* 屏幕上显示的三个选项,整体上移一个选项 */
}
}
}
break;
case(Menu_News_down): /* 向下按键按下 */
if(Current_Menu != NULL) /* 屏幕上已经显示了菜单 */
{
if(Current_Menu->menu_cursor_num != (Current_Menu->menu_sum-1)) /* 光标是否已经移动到了最后一个选项 */
{
Current_Menu->menu_cursor_num++; /* 光标下移一个选项 */
if(Current_Menu->menu_cursor_lcd < DP_NUM) /* 屏上的光标显示不在最后一行 */
{
Current_Menu->menu_cursor_lcd++; /* 光标下移一个选项 */
}
else /* 光标在屏幕上的第三行选项同时也不是菜单的最后一个选项 需要改变屏幕上显示的三个选项 */
{
Current_Menu->menu_dp[0]++; /* 屏幕上显示的三个选项,整体下移一个选项 */
Current_Menu->menu_dp[1]++; /* 屏幕上显示的三个选项,整体下移一个选项 */
Current_Menu->menu_dp[2]++; /* 屏幕上显示的三个选项,整体下移一个选项 */
}
}
}
break;
case(Menu_News_conf): /* 确认按键被按下 */
if(Current_Menu == NULL) /* 屏幕上还没有显示菜单选项 */
{
Display_Clear();//清屏
Current_Menu = &menu_main; /* 菜单指针指向主菜单 */
Menu_Display_PrintfShowClear(0,0,Menu_Size16,0,"主菜单"); /* 显示菜单标题 */
}
else /* 屏幕上已经有显示的菜单了 */
{
Menu_Display_Clear(); /* 清屏 */
if(Current_Menu->menu_next != NULL) /* 如果当前的光标选择的选项有下级菜单 */
{
Menu_Display_PrintfShowClear(0,0,Menu_Size16,0,Current_Menu->menu_str_point[Current_Menu->menu_cursor_num]); /* 显示菜单标题 */
Current_Menu = Current_Menu->menu_next[Current_Menu->menu_cursor_num]; /* 菜单指针指向下级菜单 */
}
else if(Current_Menu->menu_function[Current_Menu->menu_cursor_num] != NULL) /* 如果没有下级菜单了,但是有回调函数,则执行回调函数 */
{
// Menu_Display_Clear(); /* 清屏 */
Menu_Display_PrintfShowClear(0,0,Menu_Size16,0,Current_Menu->menu_str_point[Current_Menu->menu_cursor_num]); /* 显示菜单标题 */
Current_Menu->menu_function[Current_Menu->menu_cursor_num](); /* 回调函数执行 */
Menu_Display_PrintfShowClear(0,0,Menu_Size16,0,Current_Menu->menu_before->menu_str_point[Current_Menu->menu_before->menu_cursor_num]); /* 显示菜单标题 */
}
else /* 既没有下级菜单,也没有回调函数,说明没有菜单初始化完成 */
{
Menu_Assert(0); /* 错误断言 */
}
}
break;
case(Menu_News_cancel): /* 取消返回 */
if(Current_Menu != NULL) /* 如果屏幕上现在是有菜单的才执行取消 */
{
Current_Menu->menu_cursor_num = 0; /* 光标位置清零 */
Current_Menu->menu_cursor_lcd = 1; /* 光标在屏幕上回到第一行 */
Current_Menu->menu_dp[0] = 0; /* 显示的选项初始化 */
Current_Menu->menu_dp[1] = 1; /* 显示的选项初始化 */
Current_Menu->menu_dp[2] = 2; /* 显示的选项初始化 */
Current_Menu = Current_Menu->menu_before; /* 回到上级菜单 */
Menu_Display_Clear(); /* 清屏 */
if(Current_Menu != NULL)
{
if(Current_Menu->menu_before == NULL) /* 主菜单 */
{
Menu_Display_PrintfShowClear(0,0,Menu_Size16,0,"主菜单"); /* 显示菜单标题 */
}
else
{
Menu_Display_PrintfShowClear(0,0,Menu_Size16,0,Current_Menu->menu_before->menu_str_point[Current_Menu->menu_before->menu_cursor_num]); /* 显示菜单标题 */
}
}
else /* 回到主界面了 */
{
}
}
break;
}
}
}
Menu_Display()函数主要是菜单显示,显示菜单选项、光标、箭头指示等。Menu_Handler()函数主要是根据按键操作来改变光标和菜单状态的。这两个函数注释的比较详细的,有兴趣的可以研究一下,可以参考借鉴,这里不再多做分析。
四、回调函数编写
回调函数是底层菜单最终需要执行的函数,根据这个函数来实现功能和显示不同的界面,由于结构体中的函数指针定义关系,回调函数的形式都是void fun(void)类型的,可以根据自己的需要进行更改。我这里来分享一下我的回调函数的编写框架。一个具体的回调函数如下所示:
const uint16_t CountDown_Ui[] = {100,10,1}; /* 根据光标位置加减数值 */
/*****************************************
*函数名称:Func_0_0 (放大增益)
*功能描述:主菜单 0_0选项的回调执行函数
*输入参数:无
*返回值 :无
*更新时间:2018-12-30
*****************************************/
void Func_0_0(void)
{
uint8_t key,cursor_sta = 0,res;
uint16_t Ui_gain_old = Ui_gain;
AV_MEASURE_UO_IN();//继电器控制到 测试Ui
delay_ms(50);//等待继电器动作完成
ADC_sample_sta = 0; //测量Ui
while(1)
{
Menu_Display_PrintfShowClear(0,Menu_Line2,Menu_Size16,Menu_ShowNor,"增益:%02d.%d",Ui_gain/10,Ui_gain%10);
Menu_Display_PrintfShowClear(0,Menu_Line3,Menu_Size16,Menu_ShowNor,"Ui_Vpp:%02d.%01dmV",ADC_sample_data.Ui/10,ADC_sample_data.Ui%10);
switch(cursor_sta)
{
case(0):
Menu_Display_PrintfShow(40,Menu_Line2,Menu_Size16,Menu_ShowWhite,"%d",Ui_gain/100); /* 显示光标 */
break;
case(1):
Menu_Display_PrintfShow(40+8,Menu_Line2,Menu_Size16,Menu_ShowWhite,"%d",(Ui_gain%100)/10); /* 显示光标 */
break;
case(2):
Menu_Display_PrintfShow(40+24,Menu_Line2,Menu_Size16,Menu_ShowWhite,"%d",Ui_gain%10); /* 显示光标 */
break;
}
res = xQueueReceive(Key_QueueHandle,&key,200); /* 200ms */
if(res == pdTRUE) //检测到按键
{
if((key == Menu_News_conf)||(key == Menu_News_cancel)) /* 按下了取消键或者确认键 跳出循环 */
break;
switch(key)
{
case(Menu_News_up): /* 上键 这里做加键 */
if((Ui_gain + CountDown_Ui[cursor_sta]) <= 500) /* 小于等于最大的设定值 */
{
Ui_gain = Ui_gain + CountDown_Ui[cursor_sta];
}
break;
case(Menu_News_down): /* 下键 这里做减键 */
if(((Ui_gain - CountDown_Ui[cursor_sta]) >= 200)&&(Ui_gain > CountDown_Ui[cursor_sta])) /* 大于最小设定值 */
{
Ui_gain = Ui_gain - CountDown_Ui[cursor_sta];
}
break;
case(Menu_News_left): /* 左键 */
if(cursor_sta == 0)
cursor_sta = 2;
else
cursor_sta--;
break;
case(Menu_News_right): /* 右键 */
if(cursor_sta == 2)
cursor_sta = 0;
else
cursor_sta++;
break;
}
}
}
if(Conf_Revise() == 0xaa ) /* 确认 */
{
Menu_Display_PrintfShowCent(Menu_Line2,Menu_Size16,Menu_ShowClear0,"修改中...");
Write_Gain();//写入存储
delay_ms(500);
}
else /* 取消 */
{
Ui_gain = Ui_gain_old; //增益调回原值
}
ADC_sample_sta = 0xff;
memset(ADC_sample_data.ADC_Ui_buf,0,sizeof(ADC_sample_data.ADC_Ui_buf));
ADC_sample_data.ADC_Ui_count = 0;
}
这个函数是一个参数设置函数,用来设置参数并保存,一个while(1)的死循环,在加上一个if break的语句,判断按下取消或确认键就退出死循环,退出循环后选择是否确认要保存参数,Conf_Revise返回0xaa表示确认保存,否则不保存修改的参数。
总结
上面分析了一下关于多级菜单实现的方法,介绍的方法最好基于RTOS来实现,创建一个任务来运行多级菜单的操作和显示,并且由于回调函数是一个死循环的形式,如果没有RTOS的任务调度和阻塞功能,就无法在回调函数执行的过程中处理别的数据,实现起来较为困难。