在前不久我发了《LCD单层菜单翻滚的实现》https://bbs.eeworld.com.cn/thread-314573-1-1.html,在此帖中实现了单层菜单的上下翻滚。此贴中实现的算法不能不说非常经典,而且此方法在我的一个手持机项目中也得到应用。
还记得几年前一个项目中要使用到翻滚菜单,那时没有想到此算法,实现起来非常复杂,而且要添加或更改菜单将修改大量代码,而且弄得痛晕。每每想起这个菜单项的实现,都有点害怕。前不久公司的一个手持机项目也需要这样的菜单。为了改变这样状态,本人花了大量是时间在网络上阅读各种菜单程序,看了很多网友觉得非常经典的代码,可惜本人太笨了,居然没有读懂。。。。。。
于是我想还是先写个简单的应付手头这个项目吧,于是就有了《LCD单层菜单翻滚的实现》是出现。通过在项目中的验证,本人惊奇的发现,此算法居然比我想象的还好用,修改或增加菜单项,并不须要更改代码,只须要在对应的地方添加或更改对应函数即可。同样本人还发现,如果要实现多层菜单,只须要适当修改算法即可实现,下面我们将详细介绍多层菜单在单层菜单的基础下实现的方法。
[ 本帖最后由 zhaojun_xf 于 2012-1-30 12:40 编辑 ]下面我们就模仿手机菜单为例进行说明,首先需要说明几点:
1. 本例只有5个按键,所以没有退出按键,退出需要通过函数实现。
// 按键值定义
#define KV_NO (0x00) // 无
#define KV_UP (0x02) // 上
#define KV_DN (0x04) // 下
#define KV_MD (0x08) // 中
#define KV_LF (0x10) // 左
#define KV_RT (0x20) // 右
2.本例液晶只可以显示4行汉子,每行显示8个汉字(128*64),所以一行显示标题,3行实现菜单显示和翻滚。
3.光标所以项反白显示。
4.对于按键与显示等,本例不在说明,只说明菜单实现方式。
[ 本帖最后由 zhaojun_xf 于 2012-1-24 10:20 编辑 ]
菜单结构体定义:
/**************************************************************************************
* Variable definition
**************************************************************************************/
// 菜单参数
typedef struct _MENU_PRMT
{
uint8 ExitMark; // 退出菜单(0-不退出,1-退出)
uint8 Cursor; // 光标值(当前光标位置)
uint8 PageNo; // 菜单页(显示开始项)
uint8 Index; // 菜单索引(当前选择的菜单项)
uint8 DispNum; // 显示项数(每页可以现在菜单项)
uint8 MaxPage; // 最大页数(最大有多少种显示页)
} MENU_PRMT;
//------------------------------------------------------------------------------------
// 菜单执行
typedef struct _MENU_TABLE
{
uint8 *MenuName; // 菜单项目名称
void (*ItemHook)(void); // 要运行的菜单函数
} MENU_TABLE;
此处使用了两个结构体,一个是菜单项使用的参数,一个是菜单项的名称以及对应需要执行的函数。这里必须分成两个结构体,也是此菜单最大的不足之处,如果能够统一在一个结构体就好了。 ---------------------------------------------------------------- 网友可以对比单层菜单和这里的多层菜单的结构体,可以发现多层菜单添加了一个ExitMark,用来实现从子菜单层返回到母菜单的变量。。。 ---------------------------------------------------------------- 可是本人确实没有想出好的方法实现这两个结构体的统一。每个菜单对应一套参数可以理解,但是不同的菜单项却有不不同项数的菜单。例如下面我们要实现的菜单:
1. 主菜单
//---------------------------------- 主菜单 ---------------------------------------
MENU_PRMT GSetPrmt; // 常规设置参数
MENU_TABLE GeneralSet[] = // 常规设置
{
{"1. 情景模式 ", Menu_Profile }, // 情景模式
{"2. 时间与日期 ", Menu_SetClock }, // 时间与日期
{"3. 语言 ", Menu_Null }, // 选择语言
{"4. 话机设置 ", Menu_Null }, // 话机设置
{"5. 语音设置 ", Menu_Null }, // 语音设置
{"6. 安全设置 ", Menu_Null }, // 安全设置
{"7. 网络设置 ", Menu_Null }, // 网络设置
{"8. 个人快捷设置 ", Menu_Null }, // 个人快捷设置
{"9. 恢复出厂设置 ", Menu_Null }, // 恢复出厂设置
{"10.退出菜单 ", Menu_Null }, // 退出菜单
};
2.二级菜单
//----------------------------------- 二级菜单 -------------------------------------
MENU_PRMT ProfilePrmt; // 情景模式参数
MENU_TABLE Profile[] = // 情景模式
{
{"1. 标准 ", Menu_Null }, // 标准
{"2. 会议 ", Menu_Null }, // 会议
{"3. 户外 ", Menu_Null }, // 户外
{"4. 振动 ", Menu_Null }, // 振动
{"5. 耳机 ", Menu_Null }, // 耳机
{"6. 蓝牙模式 ", Menu_Null }, // 蓝牙模式
{"7. 离线模式 ", Menu_Null }, // 离线模式
{"8. 退出菜单 ", Menu_ExitProfile }, // 退出菜单
};
MENU_PRMT ClockPrmt; // 时间与日期参数
MENU_TABLE Clock[] = // 时间与日期
{
{"1. 时间/日期设置", Menu_Null }, // 时间/日期设置
{"2. 格式设置 ", Menu_Null }, // 格式设置
{"3. 设置城市 ", Menu_Null }, // 设置城市
{"4. 时区与时间 ", Menu_Null }, // 时区与时间
{"5. 退出时钟设置 ", Menu_ExitClockSet}, // 退出时钟设置
};
1.空函数
/**************************************************************************************
* FunctionName : Menu_Null()
* Description : 空函数
* EntryParameter : None
* ReturnValue : None
**************************************************************************************/
void Menu_Null(void)
{
;
}
2. 按键函数
/**************************************************************************************
* FunctionName : Menu_KeySan()
* Description : 获取按键值
* EntryParameter : None
* ReturnValue : 返回按键值
**************************************************************************************/
uint8 Menu_KeySan(void)
{
uint8 key = KV_NO;
key = KeyGetValue(); // 获取按键
if (key != KV_NO)
{
BuzzerSound(1); // 有键按下,响一声
while(KeyGetValue() != KV_NO) // 等待按键释放
{
;
}
}
return key;
}
3. 初始化函数
/**************************************************************************************
* FunctionName : Menu_PrmtInit()
* Description : 初始化菜单参数
* EntryParameter : prmt - 菜单参数, num - 显示项, page - 显示页
* ReturnValue : None
**************************************************************************************/
void Menu_PrmtInit(MENU_PRMT *prmt, uint8 num, uint8 page)
{
prmt->ExitMark = 0; // 清除退出菜单标志
prmt->Cursor = 0; // 光标清零
prmt->PageNo = 0; // 页清零
prmt->Index = 0; // 索引清零
prmt->DispNum = num; // 页最多显示项目数
prmt->MaxPage = page; // 最多页数
}
4.菜单显示
/**************************************************************************************
* FunctionName : Menu_Display()
* Description : 显示菜单
* EntryParameter : page - 显示页,dispNum - 每一页的显示项,cursor - 光标位置
* ReturnValue : None
**************************************************************************************/
void Menu_Display(MENU_TABLE *menu, uint8 page, uint8 dispNum, uint8 cursor)
{
uint8 i;
for (i=0; i<dispNum; i++)
{
if (cursor == i)
{
LCMDisplayStrRvs(0, (i+1)*2, menu[page+i].MenuName); // 反白显示菜单项
}
else
{
LCMDisplayStr(0, (i+1)*2, menu[page+i].MenuName); // 显示菜单项
}
}
}
5. 菜单移动
/**************************************************************************************
* FunctionName : Menu_Move()
* Description : 菜单移动
* EntryParameter : prmt - 菜单参数, key - 按键值
* ReturnValue : 有确认返回0,否则返回1
**************************************************************************************/
uint8 Menu_Move(MENU_PRMT *prmt, uint8 key)
{
uint8 rValue = 1;
switch (key)
{
case KV_UP: // 向上
{
if (prmt->Cursor != 0) // 光标不在顶端
{
prmt->Cursor--; // 光标上移
}
else // 光标在顶端
{
if (prmt->PageNo != 0) // 页面没有到最小
{
prmt->PageNo--; // 向上翻
}
else
{
prmt->Cursor = prmt->DispNum-1; // 光标到底
prmt->PageNo = prmt->MaxPage-1; // 最后页
}
}
break;
}
case KV_DN: // 向下
{
if (prmt->Cursor < prmt->DispNum-1) // 光标没有到底,移动光标
{
prmt->Cursor++; // 光标向下移动
}
else // 光标到底
{
if (prmt->PageNo < prmt->MaxPage-1) // 页面没有到底,页面移动
{
prmt->PageNo++; // 下翻一页
}
else // 页面和光标都到底,返回开始页
{
prmt->Cursor = 0;
prmt->PageNo = 0;
}
}
break;
}
case KV_MD: // 确认
{
prmt->Index = prmt->Cursor + prmt->PageNo; // 计算执行项的索引
rValue = 0;
break;
}
case KV_LF: // 左键跳到顶部
{
prmt->Cursor = 0;
prmt->PageNo = 0;
break;
}
case KV_RT: // 右键跳到底部
{
prmt->Cursor = prmt->DispNum-1; // 光标到底
prmt->PageNo = prmt->MaxPage-1; // 最后页
break;
}
default:break;
}
return rValue; // 返回执行索引
}
6.菜单处理
/**************************************************************************************
* FunctionName : Menu_Process()
* Description : 处理菜单项
* EntryParameter : menuName - 菜单名称,prmt - 菜单参数,table - 菜单表项, num - 菜单项数
* ReturnValue : None
**************************************************************************************/
void Menu_Process(uint8 *menuName, MENU_PRMT *prmt, MENU_TABLE *table, uint8 num)
{
uint8 key;
Menu_PrmtInit(prmt, PAGE_DISP_NUM, num-PAGE_DISP_NUM+1); // 显示项数和页数设置
do
{
LCMDisplayStr(0, 0, menuName); // 菜单标题显示
Menu_Display(table, prmt->PageNo, prmt->DispNum, prmt->Cursor); // 显示菜单项
key = Menu_KeySan(); // 获取按键
if (Menu_Move(prmt, key) == 0x00) // 菜单移动
{
LCMClearScreen(LCM_WHITE); // 清屏
table[prmt->Index].ItemHook(); // 执行相应项
}
} while (prmt->ExitMark == 0);
}
7.主菜单
/**************************************************************************************
* FunctionName : MenuGeneralSet()
* Description : 常规设置
* EntryParameter : None
* ReturnValue : None
**************************************************************************************/
void MenuGeneralSet(void)
{
uint8 menuNum;
menuNum = sizeof(GeneralSet)/sizeof(GeneralSet[0]); // 菜单项数
Menu_Process(" -= 常规设置 =- ", &GSetPrmt, GeneralSet, menuNum);
}
8. 二级菜单,情景模式
/**************************************************************************************
* FunctionName : Menu_Profile()
* Description : 情景模式
* EntryParameter : None
* ReturnValue : None
**************************************************************************************/
void Menu_Profile(void)
{
uint8 menuNum;
menuNum = sizeof(Profile)/sizeof(Profile[0]); // 菜单项数
Menu_Process(" -= 情景模式 =- ", &ProfilePrmt, Profile, menuNum);
}
/**************************************************************************************
* FunctionName : Menu_ExitProfile()
* Description : 退出情景模式
* EntryParameter : None
* ReturnValue : None
**************************************************************************************/
void Menu_ExitProfile(void)
{
ProfilePrmt.ExitMark = 0x01; // 退出情景模式
LCMClearScreen(LCM_WHITE); // 清屏
}
9. 二级菜单,设置设置
/**************************************************************************************
* FunctionName : Menu_SetClock()
* Description : 时间与日期设置
* EntryParameter : None
* ReturnValue : None
**************************************************************************************/
void Menu_SetClock(void)
{
uint8 menuNum;
menuNum = sizeof(Clock)/sizeof(Clock[0]); // 菜单项数
Menu_Process(" -= 时间日期 =- ", &ClockPrmt, Clock, menuNum);
}
/**************************************************************************************
* FunctionName : Menu_ExitClockSet()
* Description : 退出日期与时钟
* EntryParameter : None
* ReturnValue : None
**************************************************************************************/
void Menu_ExitClockSet(void)
{
ClockPrmt.ExitMark = 0x01; // 退出时钟设置
LCMClearScreen(LCM_WHITE); // 清屏
}
引用: 原帖由 zhaojun_xf 于 2012-1-24 11:05 发表 我编辑了半天,被你占了4楼,导致我的帖子发不上来。。。
菜单程序应用说明:
菜单程序已经做到基本独立,需要应用时完全可以不动菜单核心程序,只需要注意以下几点即可:
1.定义两个结构体变量:一个参数变量,一个菜单项数组。
MENU_PRMT GSetPrmt; // 常规设置参数
MENU_TABLE GeneralSet[] = // 常规设置
{
{"1. 情景模式 ", Menu_Profile }, // 情景模式
{"2. 时间与日期 ", Menu_SetClock }, // 时间与日期
{"3. 语言 ", Menu_Null }, // 选择语言
{"4. 话机设置 ", Menu_Null }, // 话机设置
{"5. 语音设置 ", Menu_Null }, // 语音设置
{"6. 安全设置 ", Menu_Null }, // 安全设置
{"7. 网络设置 ", Menu_Null }, // 网络设置
{"8. 个人快捷设置 ", Menu_Null }, // 个人快捷设置
{"9. 恢复出厂设置 ", Menu_Null }, // 恢复出厂设置
{"10.退出菜单 ", Menu_Null }, // 退出菜单
};
需要注意的是,数组中的函数,如果是主菜单,那么菜单项对应的应该是子菜单的菜单函数;如果是子菜单,而且后面也有子菜单那么一样对应的是其子菜单的函数;如果没有了子菜单,那么应该是对应菜单项具体的功能,所以应该对应功能函数。
函数和菜单的关联是通过数组实现的,并不需要我们通过函数或其他方式去调用。所以,菜单项中的函数一定要填正确。。。
2.编写菜单项函数
/**************************************************************************************
* FunctionName : MenuGeneralSet()
* Description : 常规设置
* EntryParameter : None
* ReturnValue : None
**************************************************************************************/
void MenuGeneralSet(void)
{
uint8 menuNum;
menuNum = sizeof(GeneralSet)/sizeof(GeneralSet[0]); // 菜单项数
Menu_Process(" -= 常规设置 =- ", &GSetPrmt, GeneralSet, menuNum);
}
这是主菜单函数,我们必须通过主函数来调用它,在这个函数中,我们首先需要获取菜单有几个项,然后通过菜单执行程序实现菜单功能。在调用此函数时需要传递几个参数:
A.菜单名称,在屏幕的第一行显示。
B.菜单对于的参数,即我们定义的参数结构体。
C.菜单数组,即我们定义的菜单数组。
D.菜单项数,及我们获取的菜单数组的元素个数,当然你也可以直接指定,这里通过获取,而不指定数组大小是方便修改。
通过以上两步就获取了一级菜单了,是不是非常方便啊。
那么二级菜单的获取,其实和上面一模一样,只是需要注意的是,二级菜单的调用,不需要我们去调,而是通过父菜单关联实现调用的。