一:前言
刚做完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. 结语
对于多级菜单的设计可以提高自己对指针,结构体,以及链表的理解,愿大家持之以恒。
本文如有不足,请予指正,感谢!