目录
教程前言
多级菜单基本知识
驱动文件创建
编辑
编辑
编辑
定义菜单数据类型代码解析
按键代码解析
菜单数据赋值代码解析
菜单按键切换显示代码解析
项目工程移植地址
教程前言
前言:编写不易,请勿搬运,仅供学习参考!!!
多级菜单基本知识
oled多级菜单,在oled应用里面较为常用,很多时候都需要编写一个oled多级菜单,通过按键来跟用户交互或者功能实现。本篇讲解实现代码,从细节原理讲起。
多级菜单,每个菜单,可能有 父菜单,子菜单,兄弟菜单,这些属性展现了层次关系,或者树形关系,这些关系使用 指针 来实现。
父菜单 last | 当前菜单的上一级菜单 |
子菜单 child | 进入当前菜单出现的菜单 |
兄弟菜单 next | 当前页面共同展示的菜单 |
驱动文件创建
这里文件的话需要两个,按键跟菜单,放这两个的函数和声明数据,创建Key跟Menu两个文件
文件内声明两个 .c 跟 .h文件。
创建完成在keil5里面添加.c文件和添加头文件路径,
在文件里面的bsp文件夹下面,找到刚才定义的.c文件然后选中,点击add添加,把Key.c添加进去,然后menu.c重复这个步奏。
然后我们添加头文件路径,选择Key的路径之后,在重复一次吧menu的路径选择进去,这样就完成了,头文件路径的添加。
预处理指进行宏定义,和.c文件中引入.h文件,在Key文件和menu文件中,这么些,图片如下,
然后Key文件中也这么写,重复一下就好了。
定义菜单数据类型代码解析
使用指针来创建多级菜单的好处是,可以在程序运行的时候,创建 修改 连接不同的菜单,或者通过多级菜单,直接改程序里面的参数,都是可以的,首先定义一个多级菜单。
typedef struct
{struct Menu_data * last;//上一个菜单struct Menu_data * next;//下一个菜单struct Menu_data * child;//子菜单void (*display_oled)(void);//函数指针指向每个选项的显示函数void (*execute_oled)(void);//函数指针指向选项的执行函数
}Menu_data;
这里其中一个问题是,下面着句代码里面去掉 struct 这个关键字行不行?
struct MenuItem* parent;
不行,typedef定义的结构体在声明结束之后才有效,也就是在 06 行之后才有效,在结构体内部引用也就是 自引用 的时候,必须加 struct 关键字 加了关键字的 MenuItem 编译器才知道你是结构体。
这里定义结构体里面属性类型,都是指针类型,成员属性非得用指针嘛,改成结构体属性类型不行嘛。
01 typedef struct MenuItem {
02 char name; // 菜单项名称
03 struct MenuItem parent; // 父菜单
04 struct MenuItem child; // 子菜单
05 struct MenuItem sibling; // 同级菜单
06 } MenuItem;
这里有一个bug,分析一下,在MenuItem这个结构体里面有 struct MenuItem child 这样一个属性,是一个结构体这个结构体又包含结构体,包含的结构体也会包含结构体,这样就导致了无限嵌套下去。
这里使用指针是因为,指针存放的是指向结构体变量的地址,同时大小确定,这个地址可以为空,就不会导致无限嵌套问题。其次每个菜单必须与其他菜单关联,这种关联就是指针实现的。
这里同时完成对,每个菜单的赋值,同时选出展示在oled上面的菜单。
// 示例菜单结构
MenuItem menu1 = {"Option 1", NULL, NULL, &menu2};
MenuItem menu2 = {"Option 2", NULL, &submenu1, &menu3};
MenuItem menu3 = {"Option 3", NULL, NULL, NULL};
MenuItem submenu1 = {"Submenu 1-1", &menu2, NULL, NULL};MenuItem* currentMenu = &menu1; // 展示在oled上面菜单
这里 menu1 menu2 menu3 是同级菜单,menu2有一个submenu1的子菜单。
按键代码解析
在设计oled多级菜单里面,需要按键来控制菜单的 上 下 确认 返回 来控制菜单页面,但是为了节省引脚在程序里面,只涉及了两个引脚,分别用来切换菜单,跟执行功能函数,所以这里我们需要初始化两个引脚,同时写两个按键检测函数。
void Key_Init(void) // 初始化按键引脚
{GPIO_InitTypeDef GPIO_InitStruct;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);// 初始化 GPIOAGPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 根据需要设置为合适的模式GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8;GPIO_InitStruct.GPIO_Speed = GPIO_Speed_10MHz;GPIO_Init(GPIOA, &GPIO_InitStruct);GPIO_SetBits(GPIOA, GPIO_Pin_8);GPIO_SetBits(GPIOA, GPIO_Pin_9);// 初始化 GPIOCGPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; // 仅针对 PC13GPIO_Init(GPIOC, &GPIO_InitStruct);// 设置为高电平GPIO_ResetBits(GPIOC, GPIO_Pin_13);
}
这里的话,因为选择,开发板自带的pc13灯的亮灭作为执行函数,所以顺带着pc13这个引脚给初始化了,然后写两个按键检测函数用来检测按键是否被按下,这个初始化函数写法里面,有讲究的是下面这一句,当同时填入两个引脚的时候,初始化会把两个引脚都进行初始化。
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8;
然后写两个按键检测函数,同时附带两个返回值,通过检测返回值的数值用来检测按键是否被按下,这里的话 Key1_flag这个标志位不能用static这个关键字来修饰,如果修饰的话,会延长这个变量的声明周期,自动保存上一次的值,导致程序错误。
int Key1_Scanf(void)
{ int Key1_Flag = 0;if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8) == RESET) {delay_ms(1000); // 去抖动if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8) == RESET) {Key1_Flag++; // 更新状态return Key1_Flag; // 返回当前状态}}return Key1_Flag; // 返回当前状态(未改变)
}int Key2_Scanf(void)
{ int Key2_Flag= 0 ;if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_9) == RESET){delay_ms(100);if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_9) == RESET){Key2_Flag++;return Key2_Flag;}}return 0;
}
菜单数据赋值代码解析
在文章的开头每个菜单结构体定义的时候,每个结构体类型有着自己的,oled显示函数,执行功能函数,这里需要对oled菜单的每个选项进行结构体赋值,赋值每个选项自己的显示函数,执行函数,同时还有的是每个选项的 父菜单 同级菜单 子菜单 这些都需要传到结构体数据里面。
Menu_data control_sub_option = {NULL,NULL,NULL,NULL,sub_menu_3};
Menu_data close_sub_option = {NULL,&control_sub_option,NULL,Led_Close,sub_menu_2};
Menu_data open_sub_option = {NULL,&close_sub_option,NULL,Led_Open,sub_menu_1};
Menu_data Init_sub_option = {NULL,&open_sub_option,NULL,NULL,sub_menu_0};//菜单类型结构体数据
Menu_data control_option ={NULL,&Init_sub_option,NULL,NULL,menu_3};
Menu_data close_option = {NULL,&control_option,NULL,Led_Close,menu_2};
Menu_data open_option = {NULL,&close_option,NULL,Led_Open,menu_1};
Menu_data Init_option = {NULL,&open_option,NULL,NULL,menu_0};//没有光标选项卡
同时还需要,定义穿进去结构体数据里面的显示函数,和执行功能函数,在传参的时候直接写进去函数名字就可以了。
void menu_0 (void) //主菜单光标第一行菜单显示函数
{OLED_ShowString(0,8,(uint8_t *)"*",12,1);//6*12 “ABC”OLED_ShowChinese(30, 10, 0, 16, 1); OLED_ShowChinese(46, 10, 1, 16, 1); //开灯OLED_ShowChinese(30, 26, 2, 16, 1); OLED_ShowChinese(46, 26, 3, 16, 1); // 关灯OLED_ShowChinese(30, 42, 0, 16, 1);OLED_ShowChinese(46, 42, 2, 16, 1); OLED_ShowChinese(62, 42, 4, 16, 1);OLED_ShowChinese(78, 42, 5, 16, 1);//开关控制OLED_Refresh();
}void menu_1 (void) //主菜单光标第一行菜单显示函数
{OLED_ShowString(22,10,(uint8_t *)"*",12,1);//6*12 “ABC”OLED_ShowChinese(30, 10, 0, 16, 1); OLED_ShowChinese(46, 10, 1, 16, 1); //开灯OLED_ShowChinese(30, 26, 2, 16, 1); OLED_ShowChinese(46, 26, 3, 16, 1); // 关灯OLED_ShowChinese(30, 42, 0, 16, 1);OLED_ShowChinese(46, 42, 2, 16, 1); OLED_ShowChinese(62, 42, 4, 16, 1);OLED_ShowChinese(78, 42, 5, 16, 1);//开关控制OLED_Refresh();
}
void menu_2 (void)//主菜单光标第二行菜单显示函数
{ OLED_ShowString(22,26,(uint8_t *)"*",12,1);//6*12 “ABC”OLED_ShowChinese(30, 10, 0, 16, 1); OLED_ShowChinese(46, 10, 1, 16, 1); //开灯OLED_ShowChinese(30, 26, 2, 16, 1); OLED_ShowChinese(46, 26, 3, 16, 1); // 关灯OLED_ShowChinese(30, 42, 0, 16, 1);OLED_ShowChinese(46, 42, 2, 16, 1); OLED_ShowChinese(62, 42, 4, 16, 1);OLED_ShowChinese(78, 42, 5, 16, 1);//开关控制OLED_Refresh();
}
void menu_3 (void)//主菜单光标第三行菜单显示函数
{OLED_ShowString(22,42,(uint8_t *)"*",12,1);//6*12 “ABC”OLED_ShowChinese(30, 10, 0, 16, 1); OLED_ShowChinese(46, 10, 1, 16, 1); //开灯OLED_ShowChinese(30, 26, 2, 16, 1); OLED_ShowChinese(46, 26, 3, 16, 1); // 关灯OLED_ShowChinese(30, 42, 0, 16, 1);OLED_ShowChinese(46, 42, 2, 16, 1); OLED_ShowChinese(62, 42, 4, 16, 1);OLED_ShowChinese(78, 42, 5, 16, 1);//开关控制OLED_Refresh();
}
void sub_menu_0(void)
{OLED_ShowChinese(30, 10, 0, 16, 1); OLED_ShowChinese(46, 10, 1, 16, 1); //开灯OLED_ShowChinese(30, 26, 2, 16, 1); OLED_ShowChinese(46, 26, 3, 16, 1); // 关灯OLED_Refresh();
// OLED_ShowString(30, 42, "退出", OLED_8X16);
}
void sub_menu_1(void)
{OLED_ShowString(22,10,(uint8_t *)"*",12,1);//6*12 “ABC”OLED_ShowChinese(30, 10, 0, 16, 1); OLED_ShowChinese(46, 10, 1, 16, 1); //开灯OLED_ShowChinese(30, 26, 2, 16, 1); OLED_ShowChinese(46, 26, 3, 16, 1); // 关灯OLED_ShowChinese(62, 42, 6, 16, 1); OLED_ShowChinese(78, 42, 7, 16, 1); // 退出OLED_Refresh();
// OLED_ShowString(30, 42, "退出", OLED_8X16);
}
void sub_menu_2(void)
{OLED_ShowString(22,26,(uint8_t *)"*",12,1);//6*12 “ABC”OLED_ShowChinese(30, 10, 0, 16, 1); OLED_ShowChinese(46, 10, 1, 16, 1); //开灯OLED_ShowChinese(30, 26, 2, 16, 1); OLED_ShowChinese(46, 26, 3, 16, 1); // 关灯OLED_ShowChinese(62, 42, 6, 16, 1); OLED_ShowChinese(78, 42, 7, 16, 1); // 退出
// OLED_ShowString(30, 42, "退出", OLED_8X16);OLED_Refresh();
}
void sub_menu_3(void)
{OLED_ShowString(22,42,(uint8_t *)"*",12,1);//6*12 “ABC”OLED_ShowChinese(30, 10, 0, 16, 1); OLED_ShowChinese(46, 10, 1, 16, 1); //开灯OLED_ShowChinese(30, 26, 2, 16, 1); OLED_ShowChinese(46, 26, 3, 16, 1); // 关灯OLED_ShowChinese(62, 42, 6, 16, 1); OLED_ShowChinese(78, 42, 7, 16, 1); // 退出
// OLED_ShowString(30, 42, "退出", OLED_8X16);OLED_Refresh();}
void Led_Open(void)
{GPIO_SetBits(GPIOC,GPIO_Pin_13);
}void Led_Close(void)
{GPIO_ResetBits(GPIOC,GPIO_Pin_13);
}
这里定义完显示函数跟执行功能函数,就直接可以传参进去了,然后再传参声明结构体类型的参数到时候需要特别注意的有一点,被取地址的结构体类型需要首先声明,如果没有声明,而进行取地址传入next这个参数,这个时候编译器会提示,这个结构体没有被定义,其实你有定义只不过是在下面定义了。
01 Menu_data close_sub_option = {NULL,&control_sub_option,NULL,Led_Close,sub_menu_2};
02 Menu_data control_sub_option = {NULL,NULL,NULL,NULL,sub_menu_3};
如果我们换种写法这么些的时候,在01行的时候就会提示,&control_sub_option这个属性类型没有被定义,所以被取地址的结构体类型我们把他放到最上面进行声明。
菜单按键切换显示代码解析
然后菜单里面特别重要的是,当按键按下的时候切换光标位置,这里实现的方法是,检测按键按下,这个时候改变指针的指向,改到当前指向的结构体类型里面的 .next属性,让指针指向这个.next,同时执行新指向的函数显示函数这样,就完成了按键按下,同时oled刷新光标显示。
void display_menu(void)//通过两个按键检测 切换 Menu_data 类型进行显示 和执行相关功能函数
{ static Menu_data * current_ptr = &Init_option;//初始化指针指向主菜单页面 static int Key1_flag = 0;static int Key2_flag = 0;current_ptr->display_oled(); //显示方法进行执行Key1_flag = Key1_Scanf(); if(Key1_flag)//切换显示指针到下个一页面显示函数{current_ptr = current_ptr->next;//改变指针指向下一个结点}Key2_flag = Key2_Scanf();if(Key2_flag)//执行当前指针指向的数据功能函数{current_ptr->execute_oled();}
}
上面有一点错误的是,当指针指向子页面的最后一个选项的时候,指针没有办法返回,因为上面最后一个选型没有,赋值.next属性,当时忘了,后面加上了,把最后一个菜单选项的.next值给赋值到主菜单就行了。
Menu_data control_sub_option = {NULL,&Init_option,NULL,NULL,sub_menu_3};
当光标落在最后子菜单的最后一个选项的时候,这个时候按下按键,就会回到最初的页面,菜单页面也就完成了闭环显示,
到这里的话基本也就讲完了,没什么需要注意的地方了,各位复刻的小伙伴有什么问题可以后后台私信我,学到东西的小伙伴可以留个赞,喜欢这种详细的教程的可以后台私信我,有什么问题也可以后台私信我,看到都会回复的。
项目工程移植地址
这里的话使用的是嘉立创的0.96寸oled工程模版,写的oled多级菜单函数,所以这里给出网址给出下载链接,给出引脚连接定义,大家下载完成之后,跟着写代码,完成复刻基本上没什么大问题。
0.96寸IIC单色屏 | 立创开发板技术文档中心
点击进去下拉到页面底部,就是百度网盘链接地址,然后下载,打开工程就行了。
这个是引脚接线图。
欢迎指正,希望对你,有所帮助!!!
编写不易,禁止搬运,禁止转载,违者必究,仅供学习,仅供参考,感谢理解。