菜单技术

出处:https://www.amobbs.com/thread-5688720-1-1.html

说在前面的话



      从我第一次讨论菜单技术(2006年)到现在,已经十多年过去了。回想起来,当时还是大一新生,一个
亲切的学姐找到我,请我帮她的毕业设计——凌阳声控机器人小车做一个液晶菜单,并许诺请我吃学校老区
最著名的新疆大盘鸡。有小姐姐请客吃饭自然动力满满,我立马熬了一个通宵好搞定了程序。老区的大盘鸡
什么味道 我连同学姐的脸一起已经忘得差不多了,好在这个菜单程序却幸存了下来,在之后的一年被我作为
凑字数的素材强行安插在 了[古董贴][交流]怎样在点阵屏上绘图——基于LCD12864的一百楼。

      现在再看当年的程序,真是恨不得找个地缝钻进去——除了原理是勉强对的,其它细节真的是……一言难
尽——说好听了叫青涩,说难听了就是一坨屎……不好意思,我就是这么直接。我摘抄一段,大家感受一下:


  1. struct MenuItem        
  2. {
  3.     char MenuCount;
  4.     char *DisplayString;
  5.     void (*Subs)();
  6.     struct MenuItem *ChildrenMenus;
  7.     struct MenuItem *ParentMenus;
  8. }NULL_MENU;

  9. void NULL_FUNCTION(void){}

  10. struct MenuItem MainMenu[3];
  11. struct MenuItem TimeMenu[4];
  12. struct MenuItem VoiceMenu[5];
  13. struct MenuItem RobotMenu[5];
  14. struct MenuItem FlashMenu[5];

  15. void FlashMenuInit(void)
  16. {
  17.     FlashMenu[0].MenuCount = 5;
  18.     FlashMenu[0].DisplayString = "  Flash Record  ";
  19.     FlashMenu[0].Subs = FlashRecord;
  20.     FlashMenu[0].ChildrenMenus = &Null;
  21.     FlashMenu[0].ParentMenus = MainMenu;

  22.     ...
  23. }

复制代码


以下是吐槽,可以忽略……

    首先来说说这个结构体。

  1. struct MenuItem        
  2. {
  3.     char MenuCount;
  4.     char *DisplayString;
  5.     void (*Subs)();
  6.     struct MenuItem *ChildrenMenus;
  7.     struct MenuItem *ParentMenus;
  8. }NULL_MENU;
复制代码

  •         槽点1:  为毛不用typedef
  •         槽点2:  为毛不用 stdint 里面的标准类型?
  •         槽点3:  为毛要浪费一个字节来保存  MenuCount?
  •         槽点4:  函数指针  void (*Subs)()  为毛形参要省略 void ?
  •         槽点5:  为什么每一个菜单项(MenuItem)都要包含一个指向父目录的指针?
  •         槽点6:  为什么缺失了 专门的类型来描述菜单?(这里是直接用 MenuItem 的数组裸体上阵直接表示一个menu)
  •         槽点7:  NULL_MENU是什么鬼?为什么要浪费这个空间?
  •         槽点8:  菜单项处理函数的原型,为什么返回值是void?难道不应该是一个状态机么?(返回 fsm_rt_t)
  •         ...
  •         槽点n: 没有用匈牙利算不算……


    再来说说这个结构体的初始化:

  1. struct MenuItem MainMenu[3];
  2. struct MenuItem TimeMenu[4];
  3. struct MenuItem VoiceMenu[5];
  4. struct MenuItem RobotMenu[5];
  5. struct MenuItem FlashMenu[5];

  6. void FlashMenuInit(void)
  7. {
  8.     FlashMenu[0].MenuCount = 5;
  9.     FlashMenu[0].DisplayString = "  Flash Record  ";
  10.     FlashMenu[0].Subs = FlashRecord;
  11.     FlashMenu[0].ChildrenMenus = &Null;
  12.     FlashMenu[0].ParentMenus = MainMenu;

  13.     ...
  14. }
复制代码


  •     槽点1: 为什么不用typedef的类型?(好吧也许不算个槽点)
  •     槽点2: 为什么不用静态赋值的方法初始化?(这里用的是一个函数)
  •     槽点3: 为什么数组的大小要直接用数字给定?(应该通过静态初始化由编译器来确定数组的大小)
  •     槽点4: 请大家无视“&Null”,这是我人生的污点……


    从结论上看,一坨屎的数据结构是不可能写出秀色可餐的算法的,所以请大家无视菜单的处理函数,我自
己也懒得吐槽了。

    让我们抛开过去,重新审视下这个话题:
嵌入式系统中,如何使用C语言构建一个可维护性强的图式菜单

【注】这里的图式菜单说的是菜单的数据结构是网状的,使用图论的方式来构建和维护



构建菜单的数据结构

    设计一个服务的数据结构,不仅要考虑用户的需求还要将目标环境的限制考虑在内。就菜单服务来说,我
们不光希望菜单的描述和维护简便、能适应不同类型的GUI(文字的还是图形化的),还要考虑嵌入式环境的
一些特点,比如对ROM和RAM的消耗要尽可能的小、嵌入式应用通常不会涉及到动态改变菜单结构、菜单服务
要能方便的进行裁剪和扩展等等。

    基于上述考虑,参考其它高级语言的常见菜单结构,我们决定使用“菜单容器”+"菜单项"的二元结构来构建
菜单的基本数据结构。简单的说就是我们要定义两个结构体,一个专门用来描述菜单项,一个专门作为容器来
装载菜单项,并追加菜单作为整体存在时所需的一些属性。

    首先从菜单项开始,容易得到:



  1. typedef struct __menu_item  menu_item_t;
  2. typedef struct __menu      menu_t;

  3. typedef fsm_rt_t menu_item_handler_t(menu_item_t *);

  4. struct __menu_item {
  5.     menu_item_handler_t *fnHandle;                      //!< handler
  6.     menu_t              *ptChild;                       //!< Child Menu
  7.     
  8.     //! depends on your application, you can add/remove/change following members
  9.     char                *pchTitle;                      //!< Menu Title
  10.     char                *pchDescription;                //!< Description for this menu item
  11.     char                chShortCutKey;                  //!< Shortcut Key value in current menu
  12. };
复制代码

    这里我们看到,菜单项的结构性主体由一个函数指针fnHandle和指向子目录的指针ptChild构成。一般来说,
二者是互斥的:

  • 当fnHandle不为NULL、指向对应的处理函数而ptChild为NULL时表示这是一个普通的菜单项,当用户选择了
    这一选项时,对应的处理程序就会被执行。值得注意的是,这里的fnHandler指向的是一个状态机,这意味着
    菜单引擎即有能力实现一个非阻塞的菜单结构,也可以无视状态机的返回,实现一个“一次性”的阻塞式菜单
    处理程序。
  • 当ptChild不为NULL、指向下一级菜单而fnHandler为NULL时表示这是一个多级菜单的入口。
  • ptChild和fnHandler同时不为空的情况较为少见,一般为了满足某些特殊的需求,需要定制特定的菜单引擎
    来配合。


    菜单项menu_item_t的剩下部分是和菜单的显示方式,或者说与菜单的外观效果高度相关的。根据需求的
不同,应该在这里添加对应的成员变量,比如例子中的 pchTitle用以保存菜单文字、pchDescription用以保存
菜单的简易提示信息,可以在用户悬停时以气球或者statusbar的形式进行显示。Windows菜单一般都有一个
快捷热键,如果我们的系统也希望增加这样的功能,那么chShortCutKey就是一个必不可少的部分,反之则是
多余的;甚至很多时候,简单的一个title就可以解决大部分问题。

    有些时候,我们希望用更为炫酷的方式来呈现菜单,而此时菜单的逻辑结构却是和菜单的显示方式无关的,
因此,在menu_item_t里加入额外的显示处理函数、字体、前景色、背景色、图标等等的描述(或者指针)
都是必要的。此时,你会发现,基础的menu_item_t更像是一个基类,而我们根据实际情况则需要继承和派生
出符合我们自己的实现形式。为了适应这一设定,我们将menu_item_t拆分成两个部分:

    基类部分 menu_item_t:

  1. typedef struct __menu_item  menu_item_t;
  2. typedef struct __menu      menu_t;

  3. typedef fsm_rt_t menu_item_handler_t(menu_item_t *);

  4. struct __menu_item {
  5.     menu_item_handler_t *fnHandle;                      //!< handler
  6.     menu_t              *ptChild;                       //!< Child Menu
  7. };
复制代码

    以及一个满足大部分应用情形的默认模板(派生类):


  1. typedef struct __default_menu_item_t  default_menu_item_t;

  2. struct __default_menu_item_t   {

  3.     //! inherit from base class menu_item_t
  4.     menu_item_t; 

  5.     //! depends on your application, you can add/remove/change following members
  6.     char                *pchTitle;                      //!< Menu Title
  7.     char                *pchDescription;                //!< Description for this menu item
  8.     char                chShortCutKey;                  //!< Shortcut Key value in current menu
  9. };

复制代码


为了让模板的定义更为简单,我们提供了以下的宏进行辅助:

【注】如果你觉得宏让你头晕,请忽略这部分,它对理解这个菜单结构并没有实质性的作用。


  1. #define __declare_menu_item_template(__NAME)                                    \
  2.     typedef struct __##__NAME __NAME;
  3. #define declare_menu_item_template(__NAME)                                      \
  4.         __declare_menu_item_template(__NAME)
  5.     
  6. #define __def_menu_item_template(__NAME)                                        \
  7.     struct __##__NAME {                                                         \
  8.         menu_item_t;                                                            
  9. #define def_menu_item_template(__NAME)                                          \
  10.             __def_menu_item_template(__NAME)                                    

  11. #define end_def_menu_item_template(__NAME)                                      \
  12.     };
复制代码


因此,前面的默认模板就可以用极为简单的形式进行描述:



  1. typedef struct __menu_item  menu_item_t;
  2. typedef struct __menu      menu_t;

  3. typedef fsm_rt_t menu_item_handler_t(menu_item_t *);

  4. struct __menu_item {
  5.     menu_item_handler_t *fnHandle;                      //!< handler
  6.     menu_t              *ptChild;                       //!< Child Menu
  7. };


  8. declare_menu_item_template(default_menu_item_t)

  9. def_menu_item_template(default_menu_item_t)
  10.     //! depends on your application, you can add/remove/change following members
  11.     char                *pchTitle;                      //!< Menu Title
  12.     char                *pchDescription;                //!< Description for this menu item
  13.     char                chShortCutKey;                  //!< Shortcut Key value in current menu
  14. end_def_menu_item_template(default_menu_item_t)
复制代码


    你也可以参考这种形式通过这一系列宏来定义自己的菜单项模板。这里就不再赘述。接下来,我们来看看
菜单容器(menu_t)的结构:


  1. typedef struct __menu_engine_cb menu_engine_cb_t;
  2. typedef fsm_rt_t menu_engine_t(menu_engine_cb_t *);

  3. struct __menu {
  4.     menu_item_t        *ptItems;                        //!< menu item list
  5.     uint_fast8_t        chCount;                        //!< menu item count
  6.     menu_t             *ptParent;                       //!< parent menu;
  7.     menu_engine_t      *fnEngine;                       //!< engine for process current menu
  8. }; 
复制代码


  • 非常直接的,menu_t结构体的前两个成员就是体现其容器作用的ptItems和chCount。前者指向具体的菜单
    项数组,后者用以描述数组中有效的菜单项个数。
  • ptParent用以指向当前菜单的父菜单。单一的指针限定了当前菜单的扇入为1——也就是只有唯一的一个父
    目录——这对大部分应用来说不是什么问题。如果要支持更为灵活的结构,解决方案并不是在这里追加父目录
    指针的数量,而是在这一数据结构外额外增加一个动态的单链表,用以记录用户打开目录的路径,用以实现与
    手机浏览时常见的“返回之前页面”类似的效果——在这种情况下,ptParent理论上也可以从结构体中删除。
  • 一般来说,简单的应用倾向于给每一个菜单都使用同样的导航方式(根据按键处理菜单选择的方式)——
    也就是说唯一的一个菜单引擎就够了,然而在一些复杂的情况下,不同的菜单可能有自己独特的按键布局和
    菜单跳转需要——比如前一个目录是简单的线性排列结构而后面一个目录是平面的二维目录结构,他们对按键
    的响应方式是不同的——在这种情况下,为每一个菜单增加一个指向对应菜单引擎的函数指针就非常有必要了。


    看过 menu_item_t 推演过程的朋友可能会有疑问了,menu_t 是否也有作为基类在需要的时候进行扩展的可
能呢?答案是肯定的。实际上,fnEngine 就可以被看作是扩展出来的。同样的,如果GUI非常炫酷,我们需要
给不同的菜单提供不同的背景色、背景图片、前景色等等属性时,我们也需要把 menu_t 当作是一个基类,并
根据自己的需要进行对应的扩展。这是一个举一反三地过程,我很乐意把这一发挥空间留给大家,根据自己的
实际情况去扩展,这里就不再赘述。

    总结下来,我们完成了菜单最基础的数据结构,并初步定义了一些宏来简化菜单项模板的定义:


  1. #ifndef __FSM_RT_TYPE__
  2. #define __FSM_RT_TYPE__
  3. //! \name finit state machine state
  4. //! @{
  5. typedef enum {
  6.     fsm_rt_err          = -1,    //!< fsm error, error code can be get from other interface
  7.     fsm_rt_cpl          = 0,     //!< fsm complete
  8.     fsm_rt_on_going     = 1,     //!< fsm on-going
  9.     fsm_rt_wait_for_obj = 2,     //!< fsm wait for object
  10.     fsm_rt_asyn         = 3,     //!< fsm asynchronose complete, you can check it later.
  11. } fsm_rt_t;
  12. //! @}

  13. #endif


  14. typedef struct __menu_item  menu_item_t;
  15. typedef struct __menu      menu_t;

  16. typedef fsm_rt_t menu_item_handler_t(menu_item_t *);

  17. struct __menu_item {
  18.     menu_item_handler_t *fnHandle;                      //!< handler
  19.     menu_t              *ptChild;                       //!< Child Menu
  20. };

  21. typedef struct __menu_engine_cb menu_engine_cb_t;
  22. typedef fsm_rt_t menu_engine_t(menu_engine_cb_t *);

  23. struct __menu {
  24.     menu_item_t        *ptItems;                        //!< menu item list
  25.     uint_fast8_t        chCount;                        //!< menu item count
  26.     menu_t             *ptParent;                       //!< parent menu;
  27.     menu_engine_t      *fnEngine;                       //!< engine for process current menu
  28. }; 

复制代码




“菜单的描述要优雅”


    有了菜单的数据结构为基础,我们就可以来考虑如何描述一个实际的菜单,也就是底层上如何对一个菜单数据
结构进行初始化的问题。基于 default_menu_item_t 一个可能的初始化形式是:


  1. extern fsm_rt_t top_menu_engine(menu_engine_cb_t*ptThis);

  2. extern fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem);
  3. extern fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem);
  4. extern fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem);

  5. extern const menu_t c_tTopMenu;

  6. default_menu_item_t c_tTopMenuItems[] = {
  7.     {
  8.         top_menu_item_a_handler,
  9.         NULL,                                           //!< child menu
  10.         "Top Menu A",
  11.         "This is Top Menu A",
  12.     },
  13.     {
  14.         top_menu_item_b_handler,
  15.         NULL,                                           //!< child menu
  16.         "Top Menu B",
  17.         "This is Top Menu B"
  18.     },
  19.     {
  20.         top_menu_item_c_handler,
  21.         NULL,                                           //!< child menu
  22.         "Top Menu C",
  23.         "This is Top Menu C"
  24.     }
  25. };

  26. const menu_t c_tTopMenu = {
  27.     (menu_item_t *)c_tTopMenuItems,                                    //!< menu item list
  28.     UBOUND(c_tTopMenuItems),                            //!< menu item count
  29.     NULL,                                               //!< top menu has no parent
  30.     top_menu_engine,                                    
  31. };


  32. fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem)
  33. {
  34.     return fsm_rt_cpl;
  35. }

  36. fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem)
  37. {
  38.     return fsm_rt_cpl;
  39. }

  40. fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem)
  41. {
  42.     return fsm_rt_cpl;
  43. }

  44. fsm_rt_t top_menu_engine(menu_engine_cb_t*ptThis)
  45. {
  46.     return fsm_rt_cpl;
  47. }
复制代码


通过加入一些辅助宏,我们可以让这个初始化的菜单的定义过程更简单(只消耗ROM):

【注】如果你觉得宏让你头晕,请忽略这部分,它对理解这个菜单结构并没有实质性的作用。


  1. #define __def_menu(__NAME, __PARENT, __ENGINE, __TEMPLATE)                      \
  2. extern const menu_t c_tMenu##__NAME;                                            \
  3. __TEMPLATE c_tMenu##__NAME##Items[] = {
  4. #define def_menu(__NAME, __PARENT, __ENGINE)                                    \
  5.             __def_menu(__NAME, (__PARENT), (__ENGINE), default_menu_item_t)
  6.             
  7. #define def_menu_ex(__NAME, __PARENT, __ENGINE, __TEMPLATE)                     \
  8.             __def_menu(__NAME, (__PARENT), (__ENGINE), __TEMPLATE)

  9. #define __end_def_menu(__NAME, __PARENT, __ENGINE, __TEMPLATE)                  \
  10.     };                                                                          \
  11.     const menu_t c_tMenu##__NAME = {                                            \
  12.         (menu_item_t *)c_tMenu##__NAME##Items,                                  \
  13.         (sizeof(c_tMenu##__NAME##Items)/sizeof(__TEMPLATE)),                    \
  14.         (menu_t *)(__PARENT),                                                   \
  15.         (__ENGINE),                                                             \
  16.     };
  17. #define end_def_menu(__NAME, __PARENT, __ENGINE)                                \
  18.             __end_def_menu(__NAME, (__PARENT), (__ENGINE), default_menu_item_t)
  19. #define end_def_menu_ex(__NAME, __PARENT, __ENGINE, __TEMPLATE)                 \
  20.             __end_def_menu(__NAME, (__PARENT), (__ENGINE), __TEMPLATE)
  21.             
  22. #define __extern_menu(__NAME)   extern const menu_t c_tMenu##__NAME;
  23. #define extern_menu(__NAME)     __extern_menu(__NAME)

  24. #define __menu(__NAME)          c_tMenu##__NAME
  25. #define menu(__NAME)            __menu(__NAME)            
  26.           
  27. #define __menu_item(__HANDLER, __CHILD_MENU, ...)                               \
  28.     {                                                                           \
  29.         (__HANDLER),                                                            \
  30.         (menu_t *)(__CHILD_MENU),                                               \
  31.         __VA_ARGS__,                                                            \
  32.     },
  33. #define menu_item(__HANDLER, __CHILD_MENU, ...)                                 \
  34.             __menu_item((__HANDLER), (__CHILD_MENU), __VA_ARGS__)
复制代码


从前面的宏中,我们注意到无论是 def_menu 还是 end_def_menu 都默认的采用了 default_menu_item_t 作为菜
单项的模板,因此,前面初始化的例子就等效的可以简化为:


  1. extern fsm_rt_t top_menu_engine(menu_engine_cb_t*ptThis);

  2. extern fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem);
  3. extern fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem);
  4. extern fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem);


  5. def_menu(TopMenu, NULL, top_menu_engine)
  6.     menu_item(
  7.         top_menu_item_a_handler, 
  8.         NULL, 
  9.         "Top Menu A",
  10.         "This is Top Menu A"
  11.     )
  12.     menu_item(
  13.         top_menu_item_a_handler, 
  14.         NULL, 
  15.         "Top Menu B",
  16.         "This is Top Menu B"
  17.     )
  18.     menu_item(
  19.         top_menu_item_a_handler, 
  20.         NULL, 
  21.         "Top Menu C",
  22.         "This is Top Menu C"
  23.     )
  24. end_def_menu(TopMenu, NULL, top_menu_engine)


  25. fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem)
  26. {
  27.     return fsm_rt_cpl;
  28. }

  29. fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem)
  30. {
  31.     return fsm_rt_cpl;
  32. }

  33. fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem)
  34. {
  35.     return fsm_rt_cpl;
  36. }

  37. fsm_rt_t top_menu_engine(menu_engine_cb_t*ptThis)
  38. {
  39.     return fsm_rt_cpl;
  40. }
复制代码


    当我们要采用自己定义的菜单项模板(而不是默认的 default_menu_item_t )时,应该使用宏 def_menu_ex,并
将自己定义的模板作为最后一个参数传递给该宏。注意到 menu_item 宏的最后一项是可变参数列表,因此无论你的
模板扩展了多少元素,都可以放心的使用 menu_item() 宏进行初始化:



  1. declare_menu_item_template( my_menu_item_t )
  2. def_menu_item_template( my_menu_item_t )
  3. ...
  4. end_def_menu_item_template( my_menu_item_t )



  5. def_menu_ex( my_top_menu, NULL, top_menu_engine,  my_menu_item_t  )

  6. ...
  7.     menu_item(
  8.         top_menu_item_a_handler,
  9.         NULL,
  10.         //! initialise the extended members 
  11.         ...
  12.     )
  13. ...

  14. end_def_menu_ex( my_top_menu, NULL, top_menu_engine,  my_menu_item_t  )

复制代码


     使用上述方式构建的菜单,会消耗多少SRAM呢?答案是0。观察数据结构,我们会注意到,描述菜单的结构体的
值在编译时刻都是已知的——并且,这个并且至关重要——我们扩展的模板也不会引用(指针指向)或者包含任何在
运行时刻会发生变化的内容,因此完全可以将整个数据结构保存在ROM中——在ARM体系架构下,简单的const就可以
完成这一使命——如果你使用了不同的平台,则应该根据自己的实际情况修改前述的宏模板,使得最终产生的菜单内容
被保存在ROM中。

    下面我们来看一个2级菜单的例子(更多级菜单的情况请以此类推):


  1. extern fsm_rt_t default_menu_engine(menu_engine_cb_t*ptThis);

  2. extern fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem);
  3. extern fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem);
  4. extern fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem);

  5. extern_menu(lv2_menu_A)

  6. def_menu(TopMenu, NULL, default_menu_engine)
  7.     menu_item(
  8.         top_menu_item_a_handler, 
  9.         &menu(lv2_menu_A), 
  10.         "Top Menu A",
  11.         "This is Top Menu A"
  12.     )
  13.     menu_item(
  14.         top_menu_item_a_handler, 
  15.         NULL, 
  16.         "Top Menu B",
  17.         "This is Top Menu B"
  18.     )
  19.     menu_item(
  20.         top_menu_item_a_handler, 
  21.         NULL, 
  22.         "Top Menu C",
  23.         "This is Top Menu C"
  24.     )
  25. end_def_menu(TopMenu, NULL, default_menu_engine)




  26. extern fsm_rt_t lv2_menu_item_1_handler(menu_item_t *ptItem);

  27. def_menu(lv2_menu_A, &menu(TopMenu), default_menu_engine)
  28.     menu_item(
  29.         lv2_menu_item_1_handler, 
  30.         NULL, 
  31.         "Lv2 Menu 1",
  32.         "This is Lv2 Menu 1"
  33.     )
  34.    ...
  35. end_def_menu(lv2_menu_1, &menu(TopMenu), default_menu_engine)


复制代码



“实际咋用”


定义好的menu数据结构怎么引用呢?

需要用到变量的时候,用menu宏,例如:

  1.     menu_t *ptThis = &menu(TopMenu);                        
复制代码


如果要extern给其它.c文件使用怎么办呢?使用extern_menu宏,例如:

  1. ...
  2.     extern_menu(TopMenu);
  3. ...                       
复制代码


     通过前面的内容,我们会很容易注意到,这个菜单结构的灵活性还体现在菜单的处理函数上,不仅每一个目录可以单
独指定处理引擎,每一个菜单项也都可以通过状态机的形式来处理具体的菜单事件。抛开应用高度相关的菜单项处理函数
不谈,我们来重点看一看菜单的引擎(以默认菜单引擎为例):


  1. fsm_rt_t default_menu_engine(menu_engine_cb_t*ptThis)
  2. {
  3.      ...
  4.      return fsm_on_going;
  5. }
复制代码


  • 这是一个状态机,意味着菜单的处理可以是非阻塞的,这对喜欢酷炫特效以及需要在菜单后面做小动作的应用来说非常
    关键。状态机的模板参考我专门的帖子。在106楼的例子非常有参考价值,这里就不具体展开了。
  • 状态机的传入参数 ptThis 指向的是一个菜单引擎的runtime控制块,里面存放的是关于菜单当前状态的各种信息,容易
    想到:

    1. typedef struct __menu_engine_cb menu_engine_cb_t;
    2. typedef fsm_rt_t menu_engine_t(menu_engine_cb_t *);

    3. struct __menut {
    4.     menu_item_t        *ptItems;                        //!< menu item list
    5.     uint_fast8_t        chCount;                        //!< menu item count
    6.     menu_t             *ptParent;                       //!< parent menu;
    7.     menu_engine_t      *fnEngine;                       //!< engine for process current menu
    8. }; 

    9. struct __menu_engine_cb {
    10.     const menu_t    *ptCurrentMenu;
    11.     uint_fast8_t    chCurrentItemIndex;
    12. };
    复制代码

    这里,ptCurrentMenu是一个指向当前菜单的指针,而chCurrentItemIndex保存的就是当前用户选中的菜单项的数组下标。
    仅靠这两个变量,理论上再配合按键输入,已经能够实现一个简单的菜单引擎。实际上,考虑到目录菜单引擎是一个状态
    机,因而将状态机变量也放在menu_engine_cb_t中也是可行的,比如:

    1. struct __menu_engine_cb {
    2.     uint_fast8_t tState;
    3.     const menu_t    *ptCurrentMenu;
    4.     uint_fast8_t    chCurrentItemIndex;
    5. };
    复制代码


    这里给出一个简化的引擎范例(伪代码):


    1. #ifndef this
    2. #   define this        (*ptThis)
    3. #endif

    4. typedef enum {
    5.     KEY_NULL = 0,
    6.     KEY_DOWN,
    7.     KEY_UP,
    8.     KEY_ENTER,
    9.     KEY_ESC,
    10. } key_t;

    11. extern key_t get_key(void);

    12. fsm_rt_t default_menu_engine(menu_engine_cb_t *ptThis)
    13. {
    14. #define DEFAULT_MENU_ENGINE_RESET_FSM() \
    15.     do { this.tState = START; } while(0)

    16.     enum {
    17.         START = 0,
    18.         READ_KEY_EVENT, 
    19.         KEY_PROCESS, 
    20.         RUN_ITEM_HANDLER
    21.     };
    22.     key_t tKey;
    23.     menu_item_t *ptItem;
    24.     
    25.     switch(this.tState) {
    26.         case START:
    27.             this.tState++;
    28.         case READ_KEY_EVENT: 
    29.             tKey = get_key();
    30.             if (KEY_NULL == tKey) {
    31.                 break;
    32.             }
    33.             //this.tState = KEY_PROCESS;
    34.             
    35.         case KEY_PROCESS:
    36.             switch (tKey) {
    37.                 case KEY_DOWN:
    38.                     this.chCurrentItemIndex++;
    39.                     if (this.chCurrentItemIndex >= this.ptCurrentMenu->chCount) {
    40.                         this.chCurrentItemIndex = 0;
    41.                     }
    42.                     break;
    43.                 case KEY_UP:
    44.                     if (0 == this.chCurrentItemIndex) {
    45.                         this.chCurrentItemIndex = this.ptCurrentMenu->chCount - 1;
    46.                     }
    47.                     break;
    48.                 case KEY_ENTER: {
    49.                         ptItem = &(this.ptCurrentMenu->ptItems[this.chCurrentItemIndex]);
    50.                         if (NULL != ptItem->fnHandle) {
    51.                             this.tState = RUN_ITEM_HANDLER;
    52.                         } else if (NULL != ptItem->ptChild) {
    53.                             this.ptCurrentMenu = ptItem->ptChild;
    54.                             this.chCurrentItemIndex = 0;
    55.                             
    56.                             DEFAULT_MENU_ENGINE_RESET_FSM();
    57.                             return fsm_rt_cpl;
    58.                         }
    59.                     }
    60.                     break;
    61.                 case KEY_ESC:
    62.                 
    63.                     //! return to upper menu
    64.                     if (NULL != this.ptCurrentMenu->ptParent) {
    65.                         this.ptCurrentMenu = this.ptCurrentMenu->ptParent;
    66.                         this.chCurrentItemIndex = 0;
    67.                             
    68.                         DEFAULT_MENU_ENGINE_RESET_FSM();
    69.                         return fsm_rt_cpl;
    70.                     }
    71.                     break;
    72.                 default:
    73.                     break;
    74.             }
    75.             break;
    76.             
    77.         case RUN_ITEM_HANDLER:
    78.             ptItem = &(this.ptCurrentMenu->ptItems[this.chCurrentItemIndex]);
    79.             fsm_rt_t tFSM = ptItem->fnHandle(ptItem);
    80.             if (IS_FSM_ERR(tFSM)) {
    81.                 //! report error
    82.                 DEFAULT_MENU_ENGINE_RESET_FSM();
    83.                 return tFSM;
    84.             } else if (fsm_rt_cpl == tFSM) {
    85.                 DEFAULT_MENU_ENGINE_RESET_FSM();
    86.                 return fsm_rt_cpl;
    87.             }
    88.             break;
    89.     }

    90.     return fsm_rt_on_going;
    91. }
    复制代码




最后我们还需要一个最顶层的任务,用来运行我们这个菜单服务(这个顶层任务和具体用哪个引擎无关):



  1. #ifndef this
  2. #   define this        (*ptThis)
  3. #endif


  4. fsm_rt_t menu_task(menu_engine_cb_t *ptThis)
  5. {
  6.     do {
  7.         /* this validation code could be removed for release version */
  8.         if (NULL == ptThis) {
  9.             break;
  10.         } else if (NULL == this.ptCurrentMenu) {
  11.             break;
  12.         } else if (NULL == this.ptCurrentMenu->fnEngine) {
  13.             break;
  14.         } else if (NULL == this.ptCurrentMenu->ptItems) {
  15.             break;
  16.         } else if (0 == this.ptCurrentMenu->chCount) {
  17.             break;
  18.         }
  19.         
  20.         return this.ptCurrentMenu->fnEngine(ptThis);
  21.         
  22.     } while(false);
  23.     
  24.     return fsm_rt_err;
  25. }

  26. static menu_engine_cb_t s_tMenuDemo = {
  27.     .ptCurrentMenu = &menu(TopMenu),
  28. };

  29. void main(void)
  30. {
  31.      ...
  32.      while(1) {
  33.          menu_task(&s_tMenuDemo);
  34.      }
  35. }

复制代码

“能玩出啥花儿不?”
    看的人多了,提问的人多了,再写!
相关链接
        - [古董贴][交流]怎样在点阵屏上绘图——基于LCD12864
        - 最近读傻孩子的菜单程序,做了一个PPT,希望大家批评指正



打完收工!

<-----------------------------------------分割线----------------------------------------->

说说我的理解,不一定对,但是会给提供一个参考,希望大家踊跃留言。

菜单就是一系列的界面的有机结合,你可以简单认为是个树形结构。至于这个树形结构是怎么

实现,数组也好、链表也行。然后就是实现的问题,我们可以把内容和现实耦合在一起,就是

每屏都有自己的画刷;也可以把画刷剥离出来,但是写成一个菜单显示引擎。再有就是菜单的

索引驱动,怎么设计。基于上面这篇论文,我们可以把菜单的引擎独立出来,它包括显示驱动

(也可以是一个fnHandler,由每屏传入,本论文没有这么做)+搜索驱动(也可以是一个

fnHandler,由每屏传入,本论文就是这么设计的),在这里我感觉可以这么设计,就是用户如果

传入的是NULL指针,就调用系统的标准处理函数,如果传入的不是NULL,就调用用户的函数。

好了,到目前为止,我们提取了菜单引擎。那么菜单如何设计内?我们知道一屏内容可能包含几种

控件,每种控件都有自己的内容和属性。于是就有了菜单项和菜单容器的概念,一个菜单项就是一

个控件,容器可以认为是一屏,可以类比MFC。这里需要为每个菜单项添加一个type,这样菜单引

擎才能加载合适的驱动。

你可能感兴趣的:(单片机)