基于stm32的u8g2菜单选择框切换的运动函数详细讲解!

一:前言

        刚做完stm32的健康监测手表,想和大家分享多级菜单这部分,在此分享一篇OLED菜单选择框的运动算法。我用的是stm32f103c8t6,7针SPI的0.96寸的OLED屏。

1.准备工作

        想必大家在做到多级菜单这部时已经把SPI硬件的配置和u8g2的移植配置好了吧!如果还没有的话可以去看我的上一篇文章噢。

2.菜单结构体定义

        多级菜单使用双向链表结构

typedef struct Function_Obj{  //小菜单结构体 
	struct Function_Obj *Next;  //指向上一个菜单 
	struct Function_Obj *Last;  //指向下一个菜单
	struct Function_Obj *Head;  //指向链表头
	struct Function_Obj *Final; //指向最后一个菜单 
	//函数指针指向每一个函数具体功能
	//Fc f;   //传入地址类型
	//const uint8_t *img;   //菜单图标 
	uint16_t NowY;
	char *Fname;  //菜单名 
	uint16_t ListNum;  //列表节点个数
}Function;

 3.初始化菜单链表

        对于初始化定义,选择定义为全局变量,菜单个数自主选择。对于菜单链接这里使用链表尾插法。

Function* Function_init(char *Fname,uint16_t nowY,Function* Fc,const uint8_t *img) //菜单初始化函数 
{
	Function *fc=(Function*)malloc(sizeof(Function));  //使用malloc函数给新菜单分配空间,注意需要导入string.h 
	fc->Fname=Fname;  //初始化菜单名 
	//fc->img=img;      
	fc->NowY=nowY;
	fc->ListNum=1;  //当前列表的节点个数初始化为1
	fc->Next=NULL;
	fc->Last=NULL;
	fc->Final=NULL;
	fc->Head=Fc;
	//fc->f=NULL;
	return fc;
}
void Function_List(Function *Head,Function *FinalList) //需传入头节点,和需要尾插的节点
{
	if(Head->Next==NULL) //头节点的下一个无任何节点 
	{
		Head->Next=FinalList;
		Head->Final=FinalList;
		FinalList->Last=Head; //直接将传入的尾节点插入尾部 
		(Head->ListNum)++;  //每插入一个节点个数加1
	}
	else //有则使用双向链表尾插法 
	{
		FinalList->Last=Head->Final;
		Head->Final->Next=FinalList;
		Head->Final=FinalList;
		(Head->ListNum)++;
	}
}

4. 选择框运动函数

        这里我先直接贴代码,代码旁有详细注释,然后跟大家讲解其中部分的思路。

4.1 当前界面的显示菜单名称的函数

void MinMenuStr(u8g2_t *u8g2,Function *fc)  //显示当前界面所有的菜单名
{
	Function *Now=fc;   //定义一个局部变量赋值为当前节点,也可以作为他们的头节点
	u8g2_SetFont(u8g2,u8g2_font_t0_13b_tr);  //设置字体 
	while(Now!=NULL)
	{
		u8g2_DrawStr(u8g2,5,Now->NowY,Now->Fname);  //u8g2显示字符串函数
		Now=Now->Next;
	}
	u8g2_DrawBox(u8g2,0,0,2,64);  //旁边加一个竖线只为了显示美观
}

4.2 矩形框运动函数

        这段别看代码多,其实都是if else判断,时间复杂度小

void Frec_run(u8g2_t *u8g2,Function *fc,uint8_t dir) //菜单矩形框运动函数
{
	uint16_t SHigh=13;  //获取字符串的最大高度,这里可以提前用u8g2_GetMaxCharHeight(u8g2)这个函数求出此值
	uint16_t startX=5;  //所有x均一致,这里看个人需求
	uint16_t SWide=0; //定义当前菜单名称的宽度
	uint16_t LastNamecount=0;  //定义上一个字符串的宽度
	static int FinalY=3; //定义矩形框移动的终点
	static int LastY=3;  //定义矩形框移动的起点
	LastY=FinalY;    //将前一次终点赋值给此次的起点
	if(dir)  //矩形框需要向下移动时
	{
		if(fc!=NULL) //先判断当前节点是否为NULL,目的是防止菜单框运动到头(尾)
		{
			uint8_t Onflag=0; //定义此变量检查当前菜单界面的矩形框是否到底(顶)
			SWide=u8g2_GetStrWidth(u8g2,fc->Fname); //将当前节点的菜单名长度取出
			LastNamecount=u8g2_GetStrWidth(u8g2,fc->Last->Fname); //将当前节点的上一个节点的名称长度取出
			if((FinalY+(SHigh+3))<=51) //判断矩形框是否移动到界面底端。这里已经以下的SHigh+3都是每个菜单名之间的空大小
				FinalY+=(SHigh+3); //无,则将矩形框下一次需要移动到的Y坐标重新赋值
			else Onflag=1; //有,则 标记为1为下一段代码准备
			for(int i=1;i<=(SHigh+3);i++)
			{
				u8g2_ClearBuffer(u8g2); //清除缓冲区,相当于清屏
				if(Onflag)
				{
					Function *NowFc=fc->Head;
					while(NowFc!=NULL)
					{
						(NowFc->NowY)-=1;
						NowFc=NowFc->Next;
					}   //这一段代码都是为了移动菜单名所处的Y坐标
				} //因为矩形框运动到底时,此时的运动可视为菜单名动 而矩形框位置不动,根据相对论即可看作矩形框在向下移动
				MinMenuStr(u8g2,fc->Head);
				if(Onflag!=1) //正常的本界面菜单之间运动
				{  //矩形框移动分为3中结果
					if(SWide>LastNamecount) //前一个菜单名比当前短,框伸长 
						u8g2_DrawRFrame(u8g2,startX-1,LastY+i-1,(uint16_t)((float)(SWide-LastNamecount)/(SHigh+3)*i)+LastNamecount+2,SHigh+1,5); //这里为什么要这样计算看我下面的思路分析
					else if(SWide<LastNamecount) //前一个菜单名比当前长,框缩短
						u8g2_DrawRFrame(u8g2,startX-1,LastY+i-1,LastNamecount-(uint16_t)((float)(LastNamecount-SWide)/(SHigh+3)*i)+2,SHigh+1,5);
					else //前一个菜单名与当前长度相同,框长度不变
						u8g2_DrawRFrame(u8g2,startX-1,LastY+i-1,SWide+2,SHigh+1,5);
				}
				else //下面的代码则是矩形框不动的情况
				{
					if(SWide>LastNamecount)
						u8g2_DrawRFrame(u8g2,startX-1,LastY-1,(uint16_t)((float)(SWide-LastNamecount)/(SHigh+3)*i)+LastNamecount+2,SHigh+1,5);
					else if(SWide<LastNamecount)
						u8g2_DrawRFrame(u8g2,startX-1,LastY-1,LastNamecount-(uint16_t)((float)(LastNamecount-SWide)/(SHigh+3)*i)+2,SHigh+1,5);
					else
						u8g2_DrawRFrame(u8g2,startX-1,LastY-1,SWide+2,SHigh+1,5);
				}
				u8g2_SendBuffer(u8g2);  //最终将他们一同写入缓冲区一起显示
				HAL_Delay(5);  //延时5ms使其看起来更流程
			}
		}
	}
	else   //这部分则是向上运动,与上面的向下运动大同小异
	{
		if(fc!=NULL)
		{
			uint8_t Inflag=0;
			SWide=u8g2_GetStrWidth(u8g2,fc->Fname);
			LastNamecount=u8g2_GetStrWidth(u8g2,fc->Next->Fname);
			if((FinalY-(SHigh+3))>=3) FinalY-=(SHigh+3);
			else Inflag=1;
			for(int i=1;i<=(SHigh+3);i++)
			{
				u8g2_ClearBuffer(u8g2);
				if(Inflag)
				{
					Function *NowFc=fc->Head;
					while(NowFc!=NULL)
					{
						(NowFc->NowY)+=1;
						NowFc=NowFc->Next;
					}
				}
				MinMenuStr(u8g2,fc->Head);
				if(Inflag!=1)
				{
					if(SWide>LastNamecount)
						u8g2_DrawRFrame(u8g2,startX-1,LastY-i-1,(uint16_t)((float)(SWide-LastNamecount)/(SHigh+3)*i)+LastNamecount+2,SHigh+1,5);
					else if(SWide<LastNamecount)
						u8g2_DrawRFrame(u8g2,startX-1,LastY-i-1,LastNamecount-(uint16_t)((float)(LastNamecount-SWide)/(SHigh+3)*i)+2,SHigh+1,5);
					else
						u8g2_DrawRFrame(u8g2,startX-1,LastY-i-1,SWide+2,SHigh+1,5);
				}
				else
				{
					if(SWide>LastNamecount)
						u8g2_DrawRFrame(u8g2,startX-1,LastY-1,(uint16_t)((float)(SWide-LastNamecount)/(SHigh+3)*i)+LastNamecount+2,SHigh+1,5);
					else if(SWide<LastNamecount)
						u8g2_DrawRFrame(u8g2,startX-1,LastY-1,LastNamecount-(uint16_t)((float)(LastNamecount-SWide)/(SHigh+3)*i)+2,SHigh+1,5);
					else
						u8g2_DrawRFrame(u8g2,startX-1,LastY-1,SWide+2,SHigh+1,5);
				}
				u8g2_SendBuffer(u8g2);
				HAL_Delay(5);
			}
		}
	}
}

        思路:如何让矩形框移动的丝滑?

        对于矩形框的移动,其主要运动的是Y坐标的移动和菜单名长度的变化,于是让两值有一种函数关系,这里我就用最简单的y=kx,再复杂一点可以使用log函数和抛物线函数实现先快后慢或先慢后快的视觉效果。

        for中的这段代码(float)(SWide-LastNamecount)/(SHigh+3)*i)+LastNamecount,就是矩形框的总增量微分得到其Y坐标每加1,矩形框的所对应的增量是多少。

5. 最终实现代码

5.1  定义并初始化各个菜单节点

Function *Ga,*ba,*ta,*ha,*ma,*la;  //初始化节点为全局变量

void Function_Game_InitList(void)  //尾插节点并初始化
{
	Ga=Function_init("gaming",3,NULL,NULL); //先将Ga作为头节点并初始化
	Ga->Head=Ga;  //Ga的头节点赋值为Ga
	ba=Function_init("u8g2ffft",19,Ga,NULL);//按照我的字体,这里两菜单名相差16个像素最好
	ta=Function_init("hstp",35,Ga,NULL); //这里的16=单个字体的最大高度+3
	ha=Function_init("block",51,Ga,NULL); //3是因为每个菜单名空三个刚刚好
	ma=Function_init("playball",67,Ga,NULL);
	la=Function_init("jkl",83,Ga,NULL);
	
	Function_List(Ga,ba);//尾插法,直接将头节点和尾节点传入
	Function_List(Ga,ta);
	Function_List(Ga,ha);
	Function_List(Ga,ma);
	Function_List(Ga,la);
}

5.2 最终show函数

void MinMenu_init(u8g2_t *u8g2,Function *Fc)
{	
	u8g2_ClearBuffer(u8g2);
	u8g2_SetFontPosTop(u8g2);  //将字符串的起始显示坐标改为左上角
	u8g2_SetFont(u8g2,u8g2_font_t0_13b_tr);  //设定字体

	uint16_t StrH=u8g2_GetMaxCharHeight(u8g2); //测出当前菜单名中字符的最大高度,要先u8g2中写入才能使用
	uint16_t StrW=u8g2_GetStrWidth(u8g2,Fc->Fname); //测出当前菜单名的长度
	
	MinMenuStr(u8g2,Fc);
	
	u8g2_DrawRFrame(u8g2,4,2,StrW+2,StrH+2,5);  //画出最开始位置的矩形框
	u8g2_SendBuffer(u8g2);
}

//----------------------------测试函数----------------------------------//

void show(u8g2_t *u8g2)
{

	Function_Game_InitList();
	Function *Now=Ga;
	MinMenu_init(u8g2,Now);
	HAL_Delay(1000);
	while(1){ //表演效果为移动到最后一个菜单再移动回来,以此往复,这里大家可以加按键实现移动
		for(int i=0;i<Now->Head->ListNum-1;i++)
		{
			Now=Now->Next;
			Frec_run(u8g2,Now,1);
			HAL_Delay(1000);
		}
		for(int i=0;i<Now->Head->ListNum-1;i++)
		{
			Now=Now->Last;
			Frec_run(u8g2,Now,0);
			HAL_Delay(1000);
		}
	}
}

5.3 视频效果

        手机拍摄会有闪屏效果,实物显示无此现象。

OLED多级菜单

6. 结语

        对于多级菜单的设计可以提高自己对指针,结构体,以及链表的理解,愿大家持之以恒。

本文如有不足,请予指正,感谢!

对于使用STM32U8g2库创建丝滑菜单,你可以按照以下步骤进行: 1. 首先,确保你已经正确安装了STM32的开发环境,并且你的硬件连接正常。 2. 下载并安装U8g2库。你可以从GitHub上找到U8g2库的源代码,并将其添加到你的项目中。 3. 在你的代码中包含U8g2库的头文件,并定义相关的引脚和屏幕类型。例如: ```c #include <U8g2lib.h> #include <Wire.h> U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); ``` 这里使用了SSD1306驱动芯片的128x64像素的OLED屏幕作为示例。 4. 在`setup()`函数中初始化U8g2库: ```c void setup() { u8g2.begin(); } ``` 5. 在`loop()`函数中编写你的丝滑菜单逻辑。这可能涉及到使用U8g2库提供的绘制函数来绘制菜单界面,以及监听用户输入来响应菜单选项的选择。以下是一个简单的示例: ```c void loop() { // 清空屏幕 u8g2.clearBuffer(); // 绘制菜单选项 u8g2.drawStr(0, 10, "Option 1"); u8g2.drawStr(0, 20, "Option 2"); u8g2.drawStr(0, 30, "Option 3"); // 刷新屏幕缓冲区 u8g2.sendBuffer(); // 监听用户输入 // 根据用户输入选择菜单选项并执行相应的操作 } ``` 这只是一个简单的示例,你可以根据你的需求进行扩展和修改。 请注意,以上仅为大致的步骤和示例代码,具体的实现可能会根据你使用的硬件和屏幕类型有所不同。你需要根据U8g2库的文档和你的具体需求进行相应的调整和修改。希望能对你有所帮助!如果你有更多问题,请继续提问。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

紫色小薇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值