本篇万字,博客最细,oled多级菜单代码解析,与实现教程,指针实现(含源码)!!!

目录

教程前言                   

多级菜单基本知识

驱动文件创建

​编辑

​编辑

​编辑

定义菜单数据类型代码解析

按键代码解析

菜单数据赋值代码解析

菜单按键切换显示代码解析

 项目工程移植地址


教程前言                   

                                前言:编写不易,请勿搬运,仅供学习参考!!!

多级菜单基本知识

        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单色屏 | 立创开发板技术文档中心

        点击进去下拉到页面底部,就是百度网盘链接地址,然后下载,打开工程就行了。

        这个是引脚接线图。

                                        欢迎指正,希望对你,有所帮助!!!

        编写不易,禁止搬运,禁止转载,违者必究,仅供学习,仅供参考,感谢理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/3600.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

C++中STL的list类常用接口及其源码解析

1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。 2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向 其前一个元素和后一个元素。 3. list与…

csp2024T3

题目大意:对于每个数而言,可以将其染成红或蓝,对于每一个数,定义其贡献为,当且仅当这个数最近的同色数与其相等,否则其贡献为0,求最大贡献和。 思路:考虑dp 1.考场20多分钟想的奇怪…

Leetcode 198. 打家劫舍 动态规划

原题链接&#xff1a;Leetcode 198. 打家劫舍 class Solution { public:int rob(vector<int>& nums) {int n nums.size();if (n 1)return nums[0];int dp[n];dp[0] nums[0];dp[1] max(nums[1], nums[0]);for (int i 2; i < n; i) {dp[i] max(dp[i - 2] num…

Spring源码学习(五):Spring AOP

免责声明 本人还处于学习阶段&#xff0c;如果内容有错误麻烦指出&#xff0c;敬请见谅&#xff01;&#xff01;&#xff01;Demo <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.8<…

外包干了6年,技术退步明显.......

先说一下自己的情况&#xff0c;大专生&#xff0c;18年通过校招进入杭州某软件公司&#xff0c;干了接近6年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落! 而我已经在一个企业干了6年的功能测试…

24/11/5 算法笔记adagrad 自适应学习率

AdaGrad&#xff08;Adaptive Gradient Algorithm&#xff09;是一种用于随机优化的算法&#xff0c;它通过适应每个参数的学习率来优化目标函数。 自适应学习率&#xff1a; AdaGrad算法的核心特点是为每个参数自适应地调整学习率。这意味着每个参数都有自己的学习率&#xff…

逆向之断点和找解密方法

企名片科创平台 先找到解密内容 ctrlshiftF搜索关键字,一般用一个函数包裹的就是解密方法 有2个方法调用,给其中一个打上断点刷新页面,为什么要打断点?为什么不打断点我就没有办法在控制台直接输出变量的值或者调用函数呢&#xff1f;个人理解这时候i只是一个局部变量&#x…

【云备份】httplib库

目录 1.httplib库简介 2.httplib请求类 3.httplib响应类 4.Server类 5.Client类 6.httplib库搭建简单服务器 6.1.ubuntu20.04使用防火墙开放端口 6.2.效果 7.httplib库搭建简单服务器 注意&#xff1a;如果对HTTP不熟悉就去&#xff1a;【网络】HTTP_yum install telne…

【CENet】多模态情感分析的跨模态增强网络

在MSA领域&#xff0c;文本的准确度远远高于音频和视觉&#xff0c;如果文本能达到90%&#xff0c;那么音频和视觉的准确度只有60%~80%&#xff0c;但是过往研究很少针对情感分析的背景下去提高音频和视频的准确度。 abstract&#xff1a; 多模态情感分析&#xff08;MSA&…

多线程--模拟实现定时器--Java

一、定时器的概念 定时器的本质就是一个闹钟&#xff0c;时间到了开始执行某些逻辑。Java标准库中的定时器是Timer。 我们查阅Java文档可以详细看到定时器的使用方法&#xff1a; Timer最核心的方法就是schedule方法。值得注意的是我们通常描述任务是使用Runnable来描述&…

‌MySQL中‌between and的基本用法‌

文章目录 一、between and语法二、使用示例2.1、between and数值查询2.2、between and时间范围查询2.3、not between and示例 BETWEEN AND操作符可以用于数值、日期等类型的字段&#xff0c;包括边界值。 一、between and语法 MySQL中的BETWEEN AND操作符用于在两个值之间选择…

视频一键转换3D:Autodesk 发布 Video to 3D Scene

Video 3D Scene 最近 Autodesk 旗下公司 Wonder Dynamics 推出了 Wonder Animation 的测试版&#xff0c;它使用突破性的视频到 3D 场景技术&#xff0c;通过将任何视频序列转换为 3D 动画场景来加速动画电影的制作。 Video 3D Scene Video 3D Scene 生成效果 作为 Wonder Stud…

数据结构 C/C++(实验一:线性表)

&#xff08;大家好&#xff0c;今天分享的是数据结构的相关知识&#xff0c;大家可以在评论区进行互动答疑哦~加油&#xff01;&#x1f495;&#xff09; 目录 提要&#xff1a;实验题目 一、实验目的 二、实验内容及要求 三、算法思想 实验1 实验2 四、源程序及注释 …

关于SQLServer在局域网内无法连接的问题的解决思路

针对SQL Server 2008在局域网内无法连接的问题&#xff0c;以下是一些详细的解决办法。我们在过程中需要用到Microsoft SQL Server 2008和Microsoft SQL Server tools 2008数据库软件中的配置管理器以及SQL Server Management Studio工具&#xff0c;入下截图所示。 一、检查网…

【C++】RBTree——红黑树

文章目录 一、红黑树的概念1.1 红⿊树的规则&#xff1a;1.2 理解最长路径长度不超过最短路径长度的 2 倍1.3 红⿊树的效率 二、 红⿊树的实现2.1 红⿊树的结构2.2 红⿊树的插⼊2.2.1 红⿊树树插⼊⼀个值的⼤概过程 2.3 红⿊树的插⼊代码实现 一、红黑树的概念 红⿊树是⼀棵⼆…

Docker-- cgroups资源控制实战

上一篇&#xff1a;容器化和虚拟化 什么是cgroups&#xff1f; cgroups是Linux内核中的一项功能&#xff0c;最初由Google的工程师提出&#xff0c;后来被整合进Linux内核; 它允许用户将一系列系统任务及其子任务整合或分隔到按资源划分等级的不同组内&#xff0c;从而为系统…

vscode ssh连接autodl失败

autodl服务器已开启&#xff0c;vscode弹窗显示连接失败 0. 检查状态 这里的端口和主机根据自己的连接更改 ssh -p 52165 rootregion-45.autodl.pro1. 修改config权限 按返回的路径找到config文件 右键--属性--安全--高级--禁用继承--从此对象中删除所有已继承的权限--添加…

你适合哪种tiktok广告账户类型?

TikTok在广告营销方面的分类体系极为详尽。在开设广告账户时&#xff0c;根据不同的海外市场和商品类型&#xff0c;TikTok会有各自的开户标准。此外&#xff0c;广告主所开设的TikTok广告账户类型会直接影响其可投放的广告类型。在广告出价方面&#xff0c;广告主的营销目标不…

大规模语言模型:从理论到实践(1)

1、绪论 大规模语言模型&#xff08;Large Language Models&#xff0c;LLM&#xff09;是由包含数百亿以上参数的深度神经网络构建的语言模型&#xff0c;采用自监督学习方法通过大量无标注文本进行训练。自2018年以来&#xff0c;多个公司和研究机构相继发布了多种模型&#…

SpringBoot中@Validated或@Valid注解校验的使用

文章目录 SpringBoot中Validated或Valid注解校验的使用1. 添加依赖2. 使用示例准备2-1 测试示例用到的类2-2 实体Dto&#xff0c;加入校验注解2-2 Controller 3. 示例测试4. Valid 和 Validated注解详解4-1 常用规则注解4-2 分组验证4-2-1 示例准备4-2-2 Controller接口4-2-3 P…