[原创] LCD多层菜单的实现

zhaojun_xf   2012-1-24 09:49 楼主

       在前不久我发了《LCD单层菜单翻滚的实现》https://bbs.eeworld.com.cn/thread-314573-1-1.html,在此帖中实现了单层菜单的上下翻滚。此贴中实现的算法不能不说非常经典,而且此方法在我的一个手持机项目中也得到应用。

 

       还记得几年前一个项目中要使用到翻滚菜单,那时没有想到此算法,实现起来非常复杂,而且要添加或更改菜单将修改大量代码,而且弄得痛晕。每每想起这个菜单项的实现,都有点害怕。前不久公司的一个手持机项目也需要这样的菜单。为了改变这样状态,本人花了大量是时间在网络上阅读各种菜单程序,看了很多网友觉得非常经典的代码,可惜本人太笨了,居然没有读懂。。。。。。

 

       于是我想还是先写个简单的应付手头这个项目吧,于是就有了《LCD单层菜单翻滚的实现》是出现。通过在项目中的验证,本人惊奇的发现,此算法居然比我想象的还好用,修改或增加菜单项,并不须要更改代码,只须要在对应的地方添加或更改对应函数即可。同样本人还发现,如果要实现多层菜单,只须要适当修改算法即可实现,下面我们将详细介绍多层菜单在单层菜单的基础下实现的方法。

[ 本帖最后由 zhaojun_xf 于 2012-1-30 12:40 编辑 ]
我的博客

回复评论 (61)

下面我们就模仿手机菜单为例进行说明,首先需要说明几点:

 

1. 本例只有5个按键,所以没有退出按键,退出需要通过函数实现。

 

  1. // 按键值定义
    #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 编辑 ]
我的博客
点赞  2012-1-24 10:09

菜单结构体定义:

 

  1. /**************************************************************************************
    * Variable definition                                              
    **************************************************************************************/
    // 菜单参数
    typedef struct _MENU_PRMT
    {
         uint8 ExitMark;             // 退出菜单(0-不退出,1-退出)

  2.  

  3.      uint8 Cursor;             // 光标值(当前光标位置)
         uint8 PageNo;             // 菜单页(显示开始项)
         uint8 Index;             // 菜单索引(当前选择的菜单项)

  4.  

  5.      uint8 DispNum;             // 显示项数(每页可以现在菜单项)
         uint8 MaxPage;             // 最大页数(最大有多少种显示页)

  6. } MENU_PRMT;

  7. //------------------------------------------------------------------------------------

  8.  

  9. // 菜单执行
    typedef struct _MENU_TABLE
    {
         uint8 *MenuName;            // 菜单项目名称
         void  (*ItemHook)(void);                // 要运行的菜单函数

  10. } MENU_TABLE;

 

此处使用了两个结构体,一个是菜单项使用的参数,一个是菜单项的名称以及对应需要执行的函数。这里必须分成两个结构体,也是此菜单最大的不足之处,如果能够统一在一个结构体就好了。 ---------------------------------------------------------------- 网友可以对比单层菜单和这里的多层菜单的结构体,可以发现多层菜单添加了一个ExitMark,用来实现从子菜单层返回到母菜单的变量。。。 ---------------------------------------------------------------- 可是本人确实没有想出好的方法实现这两个结构体的统一。每个菜单对应一套参数可以理解,但是不同的菜单项却有不不同项数的菜单。例如下面我们要实现的菜单:

 

1. 主菜单

  1. //----------------------------------   主菜单   ---------------------------------------

  2. MENU_PRMT  GSetPrmt;                   // 常规设置参数

  3. MENU_TABLE GeneralSet[] =                        // 常规设置
    {
         {"1. 情景模式     ", Menu_Profile     },                // 情景模式
         {"2. 时间与日期   ", Menu_SetClock    },                // 时间与日期
         {"3. 语言         ", Menu_Null        },                // 选择语言
         {"4. 话机设置     ", Menu_Null        },                // 话机设置
         {"5. 语音设置     ", Menu_Null        },                // 语音设置

  4.      {"6. 安全设置     ", Menu_Null        },                // 安全设置
         {"7. 网络设置     ", Menu_Null        },                // 网络设置
         {"8. 个人快捷设置 ", Menu_Null        },                // 个人快捷设置
         {"9. 恢复出厂设置 ", Menu_Null        },                // 恢复出厂设置
         {"10.退出菜单     ", Menu_Null        },                // 退出菜单
    };

 

2.二级菜单

 

  1. //-----------------------------------   二级菜单  -------------------------------------
    MENU_PRMT  ProfilePrmt;                   // 情景模式参数

  2. MENU_TABLE Profile[] =                            // 情景模式
    {
         {"1. 标准         ", Menu_Null        },                   // 标准
         {"2. 会议         ", Menu_Null        },                   // 会议
         {"3. 户外         ", Menu_Null        },                   // 户外
         {"4. 振动         ", Menu_Null        },                   // 振动

  3.      {"5. 耳机         ", Menu_Null        },                   // 耳机
         {"6. 蓝牙模式     ", Menu_Null        },                   // 蓝牙模式
         {"7. 离线模式     ", Menu_Null        },                   // 离线模式
         {"8. 退出菜单     ", Menu_ExitProfile },                   // 退出菜单
    };

  4.  

  5. MENU_PRMT  ClockPrmt;                         // 时间与日期参数

  6. MENU_TABLE Clock[] =                            // 时间与日期
    {
         {"1. 时间/日期设置", Menu_Null        },                   // 时间/日期设置
         {"2. 格式设置     ", Menu_Null        },                   // 格式设置
         {"3. 设置城市     ", Menu_Null        },                   // 设置城市
         {"4. 时区与时间   ", Menu_Null        },                   // 时区与时间
         {"5. 退出时钟设置 ", Menu_ExitClockSet},       // 退出时钟设置
    };

  7.  

上面我们列出了一个主菜单,2个二级菜单,我们可以看出,主菜单有10项,而二级菜单有8项5项等,使用导致了我们无法实现两个结构体的统一,如果那个网友能够想出统一的方法,希望能告诉一声,本人在这里先谢了。 以上菜单项中,没有实现的函数均用空函数代替,需要实现,直接填到相应想即可。 [ 本帖最后由 zhaojun_xf 于 2012-1-24 10:37 编辑 ]
我的博客
点赞  2012-1-24 10:18

1.空函数

 

  1. /**************************************************************************************
    * FunctionName   : Menu_Null()
    * Description    : 空函数
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void Menu_Null(void)
    {
        ;
    }

 

2. 按键函数

 

  1. /**************************************************************************************
    * FunctionName   : Menu_KeySan()
    * Description    : 获取按键值
    * EntryParameter : None
    * ReturnValue    : 返回按键值
    **************************************************************************************/
    uint8 Menu_KeySan(void)
    {
     uint8 key = KV_NO;

  2.  key = KeyGetValue();                               // 获取按键
     if (key != KV_NO)
     {
         BuzzerSound(1);                       // 有键按下,响一声
            while(KeyGetValue() != KV_NO)                   // 等待按键释放
      {
          ;
      }
     }

  3.  return key;
    }

 

3. 初始化函数

 

  1. /**************************************************************************************
    * 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;                     // 索引清零

  2.  prmt->DispNum  = num;                    // 页最多显示项目数
     prmt->MaxPage  = page;                    // 最多页数
    }

 

4.菜单显示

 

 

  1. /**************************************************************************************
    * FunctionName   : Menu_Display()
    * Description    : 显示菜单
    * EntryParameter : page - 显示页,dispNum - 每一页的显示项,cursor - 光标位置
    * ReturnValue    : None
    **************************************************************************************/
    void Menu_Display(MENU_TABLE *menu, uint8 page, uint8 dispNum, uint8 cursor)
    {
        uint8 i;

  2.  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. 菜单移动

 

  1. /**************************************************************************************
    * FunctionName   : Menu_Move()
    * Description    : 菜单移动
    * EntryParameter : prmt - 菜单参数, key - 按键值
    * ReturnValue    : 有确认返回0,否则返回1
    **************************************************************************************/
    uint8 Menu_Move(MENU_PRMT *prmt, uint8 key)
    {
        uint8 rValue = 1;

  2.     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;
       }

  3.   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;
       }

  4.   case KV_MD:                   // 确认
          {
              prmt->Index = prmt->Cursor + prmt->PageNo;                   // 计算执行项的索引
        rValue = 0;
              break;
       }

  5.   case KV_LF:                   // 左键跳到顶部
          {
           prmt->Cursor = 0;
        prmt->PageNo = 0;
        break;
       }

  6.   case KV_RT:                   // 右键跳到底部
          {
        prmt->Cursor = prmt->DispNum-1;                              // 光标到底
        prmt->PageNo = prmt->MaxPage-1;                          // 最后页
        break;
       }

  7.   default:break;
     }
     
     return rValue;                          // 返回执行索引
    }

 

6.菜单处理

 

  1. /**************************************************************************************
    * 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;

  2.  Menu_PrmtInit(prmt, PAGE_DISP_NUM, num-PAGE_DISP_NUM+1);                          // 显示项数和页数设置

  3.     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();                               // 执行相应项
      }

  4.     } while (prmt->ExitMark == 0);
    }

 

7.主菜单

 

 

  1. /**************************************************************************************
    * FunctionName   : MenuGeneralSet()
    * Description    : 常规设置
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void MenuGeneralSet(void)
    {
     uint8 menuNum;

  2.     menuNum = sizeof(GeneralSet)/sizeof(GeneralSet[0]);         // 菜单项数
        Menu_Process(" -= 常规设置 =- ", &GSetPrmt, GeneralSet, menuNum);
    }

  3.  

 

8. 二级菜单,情景模式

 

  1. /**************************************************************************************
    * FunctionName   : Menu_Profile()
    * Description    : 情景模式
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void Menu_Profile(void)
    {
     uint8 menuNum;

  2.     menuNum = sizeof(Profile)/sizeof(Profile[0]);         // 菜单项数
        Menu_Process(" -= 情景模式 =- ", &ProfilePrmt, Profile, menuNum);
    }

  3.  

  4. /**************************************************************************************
    * FunctionName   : Menu_ExitProfile()
    * Description    : 退出情景模式
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void Menu_ExitProfile(void) 
    {
     ProfilePrmt.ExitMark = 0x01;         // 退出情景模式
     LCMClearScreen(LCM_WHITE);             // 清屏
    }

  5.  

 

9. 二级菜单,设置设置

 

  1. /**************************************************************************************
    * FunctionName   : Menu_SetClock()
    * Description    : 时间与日期设置
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void Menu_SetClock(void)
    {
     uint8 menuNum;

  2.     menuNum = sizeof(Clock)/sizeof(Clock[0]);         // 菜单项数
        Menu_Process(" -= 时间日期 =- ", &ClockPrmt, Clock, menuNum);
    }

  3. /**************************************************************************************
    * FunctionName   : Menu_ExitClockSet()
    * Description    : 退出日期与时钟
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void Menu_ExitClockSet(void) 
    {
     ClockPrmt.ExitMark = 0x01;             // 退出时钟设置
     LCMClearScreen(LCM_WHITE);             // 清屏
    }

我的博客
点赞  2012-1-24 11:09

菜单格式:

 

1.png

 

IMG_3092.JPG

IMG_3091.JPG

IMG_3093.JPG

IMG_3093.JPG

IMG_3095.JPG

IMG_3096.JPG

IMG_3097.JPG

 

 

 

 

 

[ 本帖最后由 zhaojun_xf 于 2012-1-24 11:33 编辑 ]
我的博客
点赞  2012-1-24 11:14
够辛苦了
点赞  2012-1-24 15:54
辛苦
点赞  2012-1-24 19:49
引用: 原帖由 zhaojun_xf 于 2012-1-24 11:05 发表 我编辑了半天,被你占了4楼,导致我的帖子发不上来。。。


这个不会有影响的啊  会自动到第五楼的 

是不是网络问题呢?
加油!在电子行业默默贡献自己的力量!:)
点赞  2012-1-24 21:21
学习了
点赞  2012-1-24 22:22

回复 8楼 soso 的帖子

反正没有了,就没有心思在编辑了
我的博客
点赞  2012-1-25 08:45

菜单程序应用说明:

 

菜单程序已经做到基本独立,需要应用时完全可以不动菜单核心程序,只需要注意以下几点即可:

 

1.定义两个结构体变量:一个参数变量,一个菜单项数组。

  1.  

  2. MENU_PRMT  GSetPrmt;                   // 常规设置参数

  3. MENU_TABLE GeneralSet[] =                        // 常规设置
    {
     {"1. 情景模式     ", Menu_Profile     },                // 情景模式
     {"2. 时间与日期   ", Menu_SetClock    },                // 时间与日期
     {"3. 语言         ", Menu_Null        },                // 选择语言
     {"4. 话机设置     ", Menu_Null        },                // 话机设置
     {"5. 语音设置     ", Menu_Null        },                // 语音设置

  4.  {"6. 安全设置     ", Menu_Null        },                // 安全设置
     {"7. 网络设置     ", Menu_Null        },                // 网络设置
     {"8. 个人快捷设置 ", Menu_Null        },                // 个人快捷设置
     {"9. 恢复出厂设置 ", Menu_Null        },                // 恢复出厂设置
     {"10.退出菜单     ", Menu_Null        },                // 退出菜单
    };

 

需要注意的是,数组中的函数,如果是主菜单,那么菜单项对应的应该是子菜单的菜单函数;如果是子菜单,而且后面也有子菜单那么一样对应的是其子菜单的函数;如果没有了子菜单,那么应该是对应菜单项具体的功能,所以应该对应功能函数。

 

函数和菜单的关联是通过数组实现的,并不需要我们通过函数或其他方式去调用。所以,菜单项中的函数一定要填正确。。。

 

2.编写菜单项函数

 

  1. /**************************************************************************************
    * FunctionName   : MenuGeneralSet()
    * Description    : 常规设置
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void MenuGeneralSet(void)
    {
     uint8 menuNum;

  2.     menuNum = sizeof(GeneralSet)/sizeof(GeneralSet[0]);         // 菜单项数
        Menu_Process(" -= 常规设置 =- ", &GSetPrmt, GeneralSet, menuNum);
    }

 

这是主菜单函数,我们必须通过主函数来调用它,在这个函数中,我们首先需要获取菜单有几个项,然后通过菜单执行程序实现菜单功能。在调用此函数时需要传递几个参数:

 

A.菜单名称,在屏幕的第一行显示。

B.菜单对于的参数,即我们定义的参数结构体。

C.菜单数组,即我们定义的菜单数组。

D.菜单项数,及我们获取的菜单数组的元素个数,当然你也可以直接指定,这里通过获取,而不指定数组大小是方便修改。

 

通过以上两步就获取了一级菜单了,是不是非常方便啊。

 

那么二级菜单的获取,其实和上面一模一样,只是需要注意的是,二级菜单的调用,不需要我们去调,而是通过父菜单关联实现调用的。

 

 

我的博客
点赞  2012-1-25 09:13

回复 11楼 zhaojun_xf 的帖子

你说的那个两个菜单项没办法合在一起,是因为你用的数组,而它们的长度不一样。
其实,有没有考虑过用 结构体做成链表呢。
我是说,不用把那个 菜单执行的函数和菜单名单独合成一个结构体,然后先前还定义一个 菜单项数目。
直接就用 菜单名和该菜单执行的函数还有一些其他动作,比如光标什么的位置,直接做成一个 菜单项 的独立结构体。然后在这些结构体之间,利用指针链成一个链表。这里可能要双向链表。出来还要进去。
强者为尊,弱者,死无葬身之地
点赞  2012-1-25 11:23
一个主菜单,两个二级菜单,总共8+10+5= 23,都是菜单项。
每一个菜单项都是一个同类型的结构体。
其中包括你想要的 菜单参数,MEMU_PRMT;菜单函数 MENU_TABLE;
以及菜单名,然后再给一个index,比如按顺序编号。
如果某项的一个项目是空的,那就留空它。

然后是一个同类型的结构体指针,用以指向下一个结构体——这里,到底是做成单向链表还是双向链表,还要再琢磨琢磨。

如此,当你要对菜单进行操作,初始化也好,移动,显示也好,你就只需要传入这个菜单项的结构体就好了。

当然,这么做,更重要的是为了适应你说的,由于 不同菜单的菜单项数目不同,因此使用数组的话,总是不能使用一个数组。
强者为尊,弱者,死无葬身之地
点赞  2012-1-25 11:35
好贴。。。
点赞  2012-1-25 17:18
大亮!慢慢学习!
点赞  2012-1-25 20:26
引用: 原帖由 zhaojun_xf 于 2012-1-25 08:45 发表 反正没有了,就没有心思在编辑了


不好意思啊 年后我让技术查查
加油!在电子行业默默贡献自己的力量!:)
点赞  2012-1-26 10:18

请求楼主

请教下楼主,反白显示那个函数是怎么写的呢?
点赞  2012-4-11 11:09

回复 17楼 postlily 的帖子

反白很简单,就是把显示内容取反显示就可以了。
我的博客
点赞  2012-4-11 11:19
学习了精彩
点赞  2012-5-23 11:56

顶!!!!
点赞  2012-6-21 12:35
1234下一页
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复