目录
OLED菜单实现的方法有很多,本文介绍的方法也很常见。相对来说比较特殊的地方就是九宫格,但实现起来难度也不是特别大。本文只介绍了该菜单的基础实现方法,并没有将菜单做的特别花哨,大家学会后,可以自己添加东西在上面。下面开始介绍
函数指针
在实现这种菜的之前,有必要先复习一下函数指针(不是指针函数)。众所周知。指针在C语言里面指向一个地址,既然如此,指针也应该可以指向函数的地址。
函数指针的定义方式
数据类型 (*指针名)();
数据类型表示函数返回值的类型,*表示指针,第一个括号不能省略,省略后就变成指针函数,意味着函数的返回值是指针。第二个括号里面可以放数据类型,例如int、char,代表指针所指函数的形参类型。
使用举例
int max(int a,int b); //函数声明
int (*p_fun)(int,int);
p_fun = max; //指向目标函数
(*p_fun)(x,y); //调用
如果目标函数没有参数的话,可以什么都不写,或者用void。
菜单索引
菜单索引的作用,就好像一个目录。设置索引的目的就是要帮助我们根据索引的内容,判断如果某个按键按下的话,下一步应该显示什么页面执行什么动作。也就是说,索引里面要包含整个菜单的框架。所以说按键要和页面在索引里面对应起来,一个按键对应一个页面,一个页面要对应一个动作(函数),当然也会有特殊情况。
既然索引里面要包括这么多内容,而且不单单是一个页面。如果是九宫格的话,就会有九个主页面,每一个页面下,都应当包含对应的动作,和在当前页面下,某个按键对应的页面。
为了方便,描述有标号来代表对应的页面,使用的按键是五向开关,实际上就是五个按键,分别是上下左右和中间的按键。
假设当前页面在0号页面,若果此时我们按下左键,那下一个要显示的页面就是1号页面,下一个执行的动作就是1号页面对应的动作。
索引格式
首先说明,本文描述的菜单相当于是一个二级菜单,第一级是9个页面,页面之间是互通的,可以通过按键相互转换,按下确定按键后进入第二级菜单页面,但是每一个页面对应自己的二级菜单页面,也就是说二级菜单页面是相互独立的,不能通过左右来转换到另一个页面。
根据以上的描述,得到以下索引
typedef struct
{
u8 Cur_Index; //当前索引项
u8 left; //左面
u8 right; //右面
u8 up; //上面
u8 down; //下面
u8 enter; //确认
void (*current_operation)(void); //当前索引执行的函数(界面)
} Main_Menu;
Cur_Index表示当前显示页面,left、right、up、down、enter分别表示对应按键按下后的要显示的页面。最后一个是函数指针,指向当前页面要执行的函数。
如果使用变量来代表页面的话,最方便的方式就是枚举。如下所示
enum
{
_P0_Option = 0, //默认页面
_P1_Option,
_P2_Option,
_P3_Option,
_P4_Option,
_P5_Option,
_P6_Option,
_P7_Option,
_P8_Option,
_P11_Option,
_P22_Option,
_P33_Option,
_P44_Option,
_P55_Option,
_P66_Option,
_P77_Option,
_P88_Option
};
索引列表
如果有多个页面的话,就要使用结构体数组来实现。
Main_Menu table[20] =
{
//当前索引页 , 左面 , 右面 , 上面 , 下面 , 确认 ,执行函数。
{_P0_Option, _P1_Option, _P2_Option, _P3_Option, _P4_Option, _P0_Option, _P0_Option_task}, // 主 界面
{_P1_Option, _P1_Option, _P0_Option, _P5_Option, _P7_Option, _P11_Option, _P1_Option_task}, // 1 界面
{_P2_Option, _P0_Option, _P2_Option, _P6_Option, _P8_Option, _P22_Option, _P2_Option_task}, // 2 界面
{_P3_Option, _P5_Option, _P6_Option, _P3_Option, _P0_Option, _P33_Option, _P3_Option_task}, // 3 界面
{_P4_Option, _P7_Option, _P8_Option, _P0_Option, _P4_Option, _P44_Option, _P4_Option_task}, // 4 界面
{_P5_Option, _P5_Option, _P3_Option, _P5_Option, _P1_Option, _P55_Option, _P5_Option_task}, // 5 界面
{_P6_Option, _P3_Option, _P6_Option, _P6_Option, _P2_Option, _P66_Option, _P6_Option_task}, // 6 界面
{_P7_Option, _P7_Option, _P4_Option, _P1_Option, _P7_Option, _P77_Option, _P7_Option_task}, // 7 界面
{_P8_Option, _P4_Option, _P8_Option, _P2_Option, _P8_Option, _P88_Option, _P8_Option_task}, // 8 界面
// 5 3 6
// 1 0 2
// 7 4 8
//子级菜单
//当前索引页 , 左面 , 右面 , 上面 , 下面 , 确认 ,执行函数。
{_P11_Option, _P1_Option, _P1_Option, _P11_Option, _P11_Option, _P11_Option, _P11_Option_task}, // 11 界面
{_P22_Option, _P2_Option, _P2_Option, _P22_Option, _P22_Option, _P22_Option, _P22_Option_task}, // 22 界面
{_P33_Option, _P3_Option, _P3_Option, _P33_Option, _P33_Option, _P33_Option, _P33_Option_task}, // 33 界面
{_P44_Option, _P4_Option, _P4_Option, _P44_Option, _P44_Option, _P44_Option, _P44_Option_task}, // 44 界面
{_P55_Option, _P5_Option, _P5_Option, _P55_Option, _P55_Option, _P55_Option, _P55_Option_task}, // 55 界面
{_P66_Option, _P6_Option, _P6_Option, _P66_Option, _P66_Option, _P66_Option, _P8_Option_task}, // 66 界面
{_P77_Option, _P7_Option, _P7_Option, _P77_Option, _P77_Option, _P77_Option, _P77_Option_task}, // 77 界面
{_P88_Option, _P8_Option, _P8_Option, _P88_Option, _P88_Option, _P88_Option, _P88_Option_task}, // 88 界面
};
可以看到,_Px_Option代表x号页面,_Pxx_Option代表x号页面下的子级页面,而0号页面没有自己的子级页面。_Px_Option_task表示在x号页面下需要执行的函数,_Pxx_Option_task表示在x号页面的子级页面下需要执行的函数。
下面来分析一下,以_P0_Option也就是0号页面为例,当在0号页面时,按下左键后,需要显示的就是1号页面,用_P1_Option表示,其余方向同理。0号页面比较特殊,按下确定按键后还是当前页面,其余的跳转到子级页面。最后一项是函数指针,指向对应页面的函数,里面一边放一些屏幕显示函数和自己编写的程序。
按键获取
如下图所示,获取按键值,是一种比较常规的写法(部分参考学长)。
//按键索引值
enum
{
KEY_PREVIOUS = 0,
KEY_ENTER_PRESS,
KEY_U_PRESS,
KEY_D_PRESS,
KEY_L_PRESS,
KEY_R_PRESS,
NOTHING
};
#define KEY_ENTER Key_filter(C0, 0)
#define KEY_U Key_filter(F9, 0)
#define KEY_D Key_filter(C1, 0)
#define KEY_L Key_filter(C2, 0)
#define KEY_R Key_filter(A0, 0)
// 软件按键滤波
u8 Key_filter(PIN_enum pin, u8 aim)
{
u8 count = 0, i;
for (i = 0; i < 5; i++)
if (gpio_get(pin) != aim)
return 0;
return 1;
}
u8 Get_KEY_Value(void)
{
static u8 key_up = 0;
// if(c_begin)key_up=0;
if (!key_up && (KEY_ENTER || KEY_L || KEY_R || KEY_U || KEY_D))
{
key_up = 1;
if (KEY_ENTER)
return KEY_ENTER_PRESS;
if (KEY_L)
return KEY_L_PRESS;
if (KEY_R)
return KEY_R_PRESS;
if (KEY_U)
return KEY_U_PRESS;
if (KEY_D)
return KEY_D_PRESS;
}
if (key_up && !KEY_ENTER && !KEY_L && !KEY_R && !KEY_U && !KEY_D)
key_up = 0;
return NOTHING;
}
UI刷新
void GUI_Refresh(void)
{
key_val = Get_KEY_Value();
if (key_val != NOTHING) //只有按键按下才刷屏
{
switch (key_val)
{
case KEY_ENTER_PRESS:
func_index = table[func_index].enter; //更新索引值
break;
case KEY_L_PRESS:
func_index = table[func_index].left; //更新索引值
break;
case KEY_R_PRESS:
func_index = table[func_index].right; //更新索引值
break;
case KEY_U_PRESS:
func_index = table[func_index].up;
break;
case KEY_D_PRESS:
func_index = table[func_index].down;
break;
default:
break;
}
if (!c_begin || key_val == KEY_L_PRESS || key_val == KEY_R_PRESS)
lcd_clear(WHITE);
}
current_operation_func = table[func_index].current_operation;
(*current_operation_func)(); //执行当前索引对应的函数
last_index = func_index; //更新上一界面索引值
}
需要说明的是,func_index表示当前的页面,last_index表示上一次的页面。根据按键的不同,进行幅值。修改页面后,将本页面的对应的函数指针赋值给current_operation_func,然后执行对应函数。为了防止卡屏,中间加了条件进行刷屏,这个可以根据自己需求修改。
光标
很多菜单里面都是用的反色来显示光标,我习惯用箭头来显示,但是画箭头的方式有点笨拙就不在这里展示了。跟大家分享一下光标的移动。
void guangbiao(u8 min, u8 max)
{
if (!c_begin)
{
if (key_val == KEY_U_PRESS)
m_opt--;
else if (key_val == KEY_D_PRESS)
m_opt++;
}
if (m_opt < min)
m_opt = max;
else if (m_opt > max)
m_opt = min;
if (c_begin)
jiantou(m_opt, GRAY);
else
jiantou(m_opt, BLUE);
}
C_begin用来表示是否选中某一项,m_opt表示光标位置。因为在不同的页面光标移动的范围可能是不一样的的,所以写了这个函数
页面函数
以1号页面为例,简要说明一下页面函数。这个页面用来调节参数。
void _P1_Option_task(void)
{
if (last_index != func_index)
m_opt = 1;
if (key_val != NOTHING) //固定显示,不用时刻刷新
{
lcd_showstr(40, 0, "balance ");
lcd_showstr(20, 1, "AngleZero");
lcd_showstr(20, 2, "Pitch_P ");
lcd_showstr(20, 3, "Pitch_D ");
lcd_showstr(20, 4, "Gyro_P ");
lcd_showstr(20, 5, "Gyro_I ");
lcd_showstr(20, 6, "Gyro_D ");
}
lcd_showfloat(100, 1, AngleZero, 2, 2);
lcd_showfloat(100, 2, Pitch_P, 2, 2);
lcd_showfloat(100, 3, Pitch_D, 2, 2);
lcd_showfloat(100, 4, Gyro_P, 2, 2);
lcd_showfloat(100, 5, Gyro_I, 2, 2);
lcd_showfloat(100, 6, Gyro_D, 2, 2);
}
这里很简单,除了显示什么的,就只有一个判断,如果上一个页面不等于这个页面,说明是第一次进入,光标复位。
void _P11_Option_task(void)
{
_P1_Option_task();
guangbiao(1, 6);
if (key_val == KEY_ENTER_PRESS && last_index == func_index)
c_begin = !c_begin;
if (c_begin)
switch (m_opt)
{
case 1:
{
if (key_val == KEY_U_PRESS)
AngleZero += 0.1;
else if (key_val == KEY_D_PRESS)
AngleZero -= 0.1;
break;
}
case 2:
{
if (key_val == KEY_U_PRESS)
Pitch_P += 1;
else if (key_val == KEY_D_PRESS)
Pitch_P -= 1;
break;
}
case 3:
{
if (key_val == KEY_U_PRESS)
Pitch_D += 1;
else if (key_val == KEY_D_PRESS)
Pitch_D -= 1;
break;
}
case 4:
{
if (key_val == KEY_U_PRESS)
Gyro_P += 0.02;
else if (key_val == KEY_D_PRESS)
Gyro_P -= 0.02;
break;
}
case 5:
{
if (key_val == KEY_U_PRESS)
Gyro_I += 0.01;
else if (key_val == KEY_D_PRESS)
Gyro_I -= 0.01;
break;
}
case 6:
{
if (key_val == KEY_U_PRESS)
Gyro_D += 0.01;
else if (key_val == KEY_D_PRESS)
Gyro_D -= 0.01;
break;
}
}
}
在子级页面,继续调用上一级函数,用于显示参数,然后对光标进限制,但这个写的不太好,重复限制好像没什么用。然后是判断是都选中某一项,选中后根据光标位置,修改相应的参数。
结语
参考了网上的写法,仍有进步空间。