目录
前言
一、菜单分级和功能的编写
二、菜单的切换
三、结语
前言
本文讲解了如何用C语言实现一个菜单。本菜单的选项切换、确认、返回逻辑与菜单中选项具体要实现的功能分离开,让菜单的代码更加清晰,避免了一般状态机编写中状态机功能切换逻辑与具体功能写在一起造成混乱的情况。
一、菜单分级和功能的编写
一个菜单首先要分不同的层级,这些层级可以通过切换键(如左右键)、确定键、返回键进行同层级和不同层级的选择,这样就可以实现用菜单选择执行不同的程序了。
首先,根据菜单的结构,我们使用如下的菜单结构体:
typedef struct Menu
{
int mode1; // 0-代表主菜单,选择进入对应二级菜单 1、2、3...代表二级菜单
int mode2; // 0-代表二级菜单,选择进入对应三级菜单 1、2、3...代表三级菜单
int mode3; // 0-代表三级菜单,选择进入对应四级菜单 1、2、3...代表四级菜单
int mode4; // 同理
}sMenu;
这个菜单的结构可以用下图表示:
接下来,要编写进入每一级菜单时要执行的内容,以及进入最后一级时所要执行的程序。如下图所示:
建立文件menu.c、menu.h、main.c。
在menu.c中添加如下代码:
#include "menu.h"
//将传入字符串显示出来,并播报一遍
extern void Output(char* str);
//创建一个菜单
sMenu menu = {0,0,0,0};
//放在主循环中扫描
void Menu_Scan()
{
switch(menu.mode1)
{
/* 主菜单,选择进入对应二级菜单 */
//进入主菜单时显示“主菜单”,并播报一遍
case 0: Output("主菜单"); break;
/* 二级菜单1 */
case 1:
switch(menu.mode2)
{
/* 二级菜单,选择进入对应三级菜单 */
//进入二级菜单时显示“二级菜单1”,并播报一遍
case 0: Output("二级菜单1"); break;
/* 三级菜单1,进入可运行App1 */
case 1: Output("现在运行App1"); break;
/* 三级菜单2,进入可运行App2 */
case 2: Output("现在运行App2"); break;
/* 三级菜单3,进入可运行App3 */
case 3: Output("现在运行App3"); break;
}
break;
/* 二级菜单2 */
case 2: break;
/* 二级菜单3 */
case 3: break;
}
}
将Menu_Scan()放在主循环中扫描,每当所选的菜单改变时,就会执行相应的程序了。这里最后一级菜单所要执行的程序比较简单,只是显示一段文字和播报一遍该文字的音频。其中二级菜单2和二级菜单3的结构类似,这里就省略不写。在每次进入菜单时,都会进行一次的显示和播报,代表进入该菜单。第四级菜单结构也类似,这里就不写了。
在menu.h中添加如下代码:
#ifndef __MENU_H
#define __MENU_H
typedef struct Menu
{
int mode1; // 0-代表主菜单,选择进入对应二级菜单 1、2、3...代表二级菜单
int mode2; // 0-代表二级菜单,选择进入对应三级菜单 1、2、3...代表三级菜单
int mode3; // 0-代表三级菜单,选择进入对应四级菜单 1、2、3...代表四级菜单
int mode4; // 同理
}sMenu;
void Menu_Scan();
#endif
在main.c中添加如下代码:
#include "menu.h"
int main()
{
Some_Init(); //一些初始化操作
while(1)
{
Menu_Scan(); //扫描菜单
}
return 0;
}
二、菜单的切换
,菜单的分级问题解决了菜单有了最基本的一个框架,在这个框架中可以填入相应要运行的程序。现在要解决的是菜单同层级和不同层级的选择和切换问题。
以按键切换为例,按键有切换键(左右键)、确定键、返回键。
建立文件button.c、button.h。
在button.c中添加如下代码,代码有点多,重点在Button_Scan()函数中的内容:
#include "button.h"
//将传入字符串显示出来,并播报一遍
extern void Output(char* str);
int btnState[4] = {1,1,1,1}; //按键状态 0-按下 1-未按下
int btnFlag[4] = {0,0,0,0}; //防止按键重复检测
//获得所有按键状态
static void Button_GetAllState()
{
...
}
//复位btnFlag
static void resetBtnFlag()
{
for(int i=0; i<4; i++){
if(btnState[i]==BtnUp && btnFlag[i]==1){
btnFlag[i] = 0;
}
}
}
/* 按下按键返回1,未按下返回0 */
//上一个
int isBtnPrev()
{ //防止前一个和后一个同时按下出错
if(btnState[BtnP]==BtnDown && btnState[BtnN]==BtnUp && btnFlag[BtnP]==0){
btnFlag[BtnP] = 1;
return 1;
}
return 0;
}
//下一个
int isBtnNext()
{
if(btnState[BtnN]==BtnDown && btnState[BtnP]==BtnUp && btnFlag[BtnN]==0){
btnFlag[BtnN] = 1;
return 1;
}
return 0;
}
//确认键
int isBtnEnter()
{ //防止确认键和返回键同时按下出错
if(btnState[BtnE]==BtnDown && btnState[BtnR]==BtnUp && btnFlag[BtnE]==0){
btnFlag[BtnE] = 1;
return 1;
}
return 0;
}
//返回键
int isBtnReturn()
{
if(btnState[BtnR]==BtnDown && btnState[BtnE]==BtnUp && btnFlag[BtnR]==0){
btnFlag[BtnR] = 1;
return 1;
}
return 0;
}
/* 按键扫描 */
void Button_Scan()
{
static int chooseBuf = 1; //左右选择时不立即进入该菜单
Button_GetAllState();
switch(menu.mode1)
{
/*
* 主菜单
*/
case 0:
if(isBtnE()){ //确认键
menu.mode1 = chooseBuf; chooseBuf = 1;
}else if(isBtnR()){ //返回键(主菜单不用)
}else if(isBtnP()){ //左键
if(chooseBuf>1) chooseBuf--;
}else if(isBtnN()){ //右键
if(chooseBuf<3) chooseBuf++;
}
if(isBtnP() || isBtnN()){
switch(chooseBuf){ //左右选择时显示并播报所在菜单
case 1: Output("二级菜单1"); break;
case 2: Output("二级菜单2"); break;
case 3: Output("二级菜单3"); break;
}
}
break;
/*
* 二级菜单1
*/
case 1:
switch(menu.mode2)
{
/*
* 二级菜单选择
*/
case 0:
if(isBtnS()){ //确认键
menu.mode2 = chooseBuf; chooseBuf = 1;
}else if(isBtnR()){ //返回键
chooseBuf = 1; menu.mode2 = 0; menu.mode1 = 0;
}else if(isBtnP()){ //左键
if(chooseBuf>1) chooseBuf--;
}else if(isBtnN()){ //右键
if(chooseBuf<3) chooseBuf++;
}
if(isBtnP() || isBtnN()){
switch(chooseBuf){ //左右选择时显示并播报所在菜单
case 1: Output("点击确认选择App1"); break;
case 2: Output("点击确认选择App2"); break;
case 3: Output("点击确认选择App3"); break;
}
}
break;
/*
* 三级菜单1 App1
*/
case 1:
if(isBtnE()){ //确认键(App1中不需要确认键)
}else if(isBtnR()){ //返回键
menu.mode2 = 0;
}else if(isBtnP()){ //左键(App1中不需要左键)
}else if(isBtnN()){ //右键(App1中不需要右键)
}
break;
/*
* 三级菜单2 App2
*/
case 2:
...
break;
/*
* 三级菜单3 App3
*/
case 3:
...
break;
}
break;
/*
* 二级菜单2
*/
case 2:
...
break;
/*
* 二级菜单3
*/
case 3:
...
break;
}
//复位btnFlag
resetBtnFlag();
}
可以发现,menu.modex为0时,是作为一种菜单选择界面的状态,menu.modex为1、2、3时,进入菜单中的该选项。在菜单的末端其实就是运行某个程序,这里举例的程序比较简单,只需要一个返回键供退出程序使用。假如这个程序还需要其他按键实现功能,只需要在button.c中的Button_Scan函数中添加menu.mode3的选择相关代码,然后在menu.c中添加对应menu.mode3要实现的功能,这样就可以通过按键切换menu.mode3来运行对应menu.mode3功能的程序。
在button.h中添加如下代码:
#ifndef __BUTTON_H
#define __BUTTON_H
#define BtnP 0 /* 左键 */
#define BtnN 1 /* 右键 */
#define BtnE 2 /* 确认键 */
#define BtnR 3 /* 返回键 */
#define BtnUp 1 /* 按键弹起 */
#define BtnDown 0 /* 按键按下 */
void Button_Scan();
#endif
在main.c中添加如下代码:
#include "menu.h"
int main()
{
Some_Init(); //一些初始化操作
while(1)
{
Button_Scan(); //扫描按键,可以放在中断中,这里为了举例方便
Menu_Scan(); //扫描菜单
delay_ms(50); //按键消抖
}
return 0;
}
菜单实现的总体思路是,在Menu_Scan()中实现菜单的具体功能,在Button_Scan()中通过不同按键及其组合选择或者切换菜单的选项。这里将状态的选择与具体功能实现分开可以让代码编写更为清晰,因为如果实现的具体功能不仅仅是播报并显示一句话这么简单,而是要通过按键切换不同的状态,来实现一个“App”中各种功能,使用一般编写状态机的思路,将状态的切换和要实现的功能编写在一起会显得比较混乱。
上面的代码中,还有一些细节和bug需要修复。比如Menu_Scan()中某个选项会在主循环中不断被扫描执行,而很多情况下我们只想让写的程序在按键按下时只执行一次,比如这里的语音播报和文字显示,在一些情况下,我们又希望某个“App”中的一些函数能不断循环执行。还有在按下返回键时,就要回到上一级菜单的第一个选项,但是有时我们只是想回到当前选项的父菜单。所以还需要进行一些补充。
首先要实现返回时返回当前的父菜单,在menu.h中添加如下代码:
...
typedef struct Menu
{
...
int retNum1; //记录要返回的二级菜单
}sMenu;
...
修改menu.c中的代码:
#include "menu.h"
...
sMenu menu = {0,0,0,0,0};
//放在主循环中扫描
void Menu_Scan()
{
switch(menu.mode1)
{
case 0:
switch(menu.retNum1){ //更据要返回的二级菜单选择
case 1: Output("二级菜单1"); break;
case 2: Output("二级菜单2"); break;
case 3: Output("二级菜单3"); break;
}
break;
...
}
}
在button.c的三级菜单返回键功能中添加retNum1=n;其中n为要返回的二级菜单编号。
为了让菜单中一些程序只执行一次,而另一些循环执行,在menu.c中添加如下代码:
#include "menu.h"
...
//二级菜单1--三级菜单1(App)1s翻转一次LED
void Led_Triggle()
{
static int cnt = 0;
if(menu.mode1==1 && menu.mode2==1){
if(cnt<10){
cnt++;
}
if(cnt==10){ //LED翻转
LED != LED; cnt = 0;
}
}
}
//放在主循环中扫描
void Menu_Scan()
{
static sMenu lastMode = {0,0,0,0,0};
static int changeFlag = 1; //防止重复执行 只有当切换模式时置1,否则为0
//如果模式改变,flag=1
if(menu.mode1!=lastMode.mode1 || menu.mode2!=lastMode.mode2 || menu.mode3!=lastMode.mode3 || menu.mode4!=lastMode.mode4){
changeFlag = 0;
}else{
changeFlag = 1;
}
lastMode.mode1 = menu.mode1; lastMode.mode2 = menu.mode2;
lastMode.mode3 = menu.mode3; lastMode.mode4 = menu.mode4;
/*------------- 需要不断循环执行的代码 -------------*/
Led_Triggle();
/*-------------------------------------------------*/
if(changeFlag){
return;
}
switch(menu.mode1)
{
...
}
}
三、结语
这个菜单的编写方法是我在参加一个比赛中想到的,原本编写逻辑较为简单的程序,要切换模式直接用if-else来切换模式,但当模式的数量达到需要用一个菜单来表示时,代码是否清晰、易于阅读,在添加一个新功能时是否会影响已有的功能成为了一个很重要的问题。使用这种菜单编写方法,只需要在button.c中添加按键切换菜单的逻辑,在menu.c中添加该模式要实现的功能即可,这让编写一个需要大量模式的程序负担减小了不少。