選單技術

無痕幽雨發表於2018-03-19

出處: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,這樣選單引

擎才能載入合適的驅動。

相關文章