Gorgon_Meducer 发表于 2018-3-14 21:25:30

[交流][专题]再谈菜单技术 2018-03-14 Update

本帖最后由 Gorgon_Meducer 于 2018-10-24 18:45 编辑

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

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


struct MenuItem      
{
    char MenuCount;
    char *DisplayString;
    void (*Subs)();
    struct MenuItem *ChildrenMenus;
    struct MenuItem *ParentMenus;
}NULL_MENU;

void NULL_FUNCTION(void){}

struct MenuItem MainMenu;
struct MenuItem TimeMenu;
struct MenuItem VoiceMenu;
struct MenuItem RobotMenu;
struct MenuItem FlashMenu;

void FlashMenuInit(void)
{
    FlashMenu.MenuCount = 5;
    FlashMenu.DisplayString = "Flash Record";
    FlashMenu.Subs = FlashRecord;
    FlashMenu.ChildrenMenus = &Null;
    FlashMenu.ParentMenus = MainMenu;

    ...
}



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

    首先来说说这个结构体。

struct MenuItem      
{
    char MenuCount;
    char *DisplayString;
    void (*Subs)();
    struct MenuItem *ChildrenMenus;
    struct MenuItem *ParentMenus;
}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: 没有用匈牙利算不算……


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

struct MenuItem MainMenu;
struct MenuItem TimeMenu;
struct MenuItem VoiceMenu;
struct MenuItem RobotMenu;
struct MenuItem FlashMenu;

void FlashMenuInit(void)
{
    FlashMenu.MenuCount = 5;
    FlashMenu.DisplayString = "Flash Record";
    FlashMenu.Subs = FlashRecord;
    FlashMenu.ChildrenMenus = &Null;
    FlashMenu.ParentMenus = MainMenu;

    ...
}



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


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

    让我们抛开过去,重新审视下这个话题:
嵌入式系统中,如何使用C语言构建一个可维护性强的图式菜单。
【注】这里的图式菜单说的是菜单的数据结构是网状的,使用图论的方式来构建和维护

温馨提示

后续内容涉及到一些C99引入的特殊语法,比如匿名结构体。
关于匿名结构体的使用,我有一个专门的帖子:[交流][微知识]模块的封装(三):无伤大雅的形式主义

文中出现但未提供定义的宏,定义如下:

#define UBOUND(__ARR)    (sizeof(__ARR)/sizeof(__ARR))



完整的可编译的MDK工程在这里:

https://github.com/GorgonMeducer/Generic_MCU_Software_Infrastructure

这是范例源代码:

https://github.com/GorgonMeducer/Generic_MCU_Software_Infrastructure/blob/master/example/system.c


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

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

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



typedef struct __menu_itemmenu_item_t;
typedef struct __menu      menu_t;

typedef fsm_rt_t menu_item_handler_t(menu_item_t *);

struct __menu_item {
    menu_item_handler_t *fnHandle;                      //!< handler
    menu_t            *ptChild;                     //!< Child Menu
   
    //! depends on your application, you can add/remove/change following members
    char                *pchTitle;                      //!< Menu Title
    char                *pchDescription;                //!< Description for this menu item
    char                chShortCutKey;                  //!< Shortcut Key value in current menu
};

    这里我们看到,菜单项的结构性主体由一个函数指针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:

typedef struct __menu_itemmenu_item_t;
typedef struct __menu      menu_t;

typedef fsm_rt_t menu_item_handler_t(menu_item_t *);

struct __menu_item {
    menu_item_handler_t *fnHandle;                      //!< handler
    menu_t            *ptChild;                     //!< Child Menu
};

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


typedef struct __default_menu_item_tdefault_menu_item_t;

struct __default_menu_item_t   {

    //! inherit from base class menu_item_t
    menu_item_t;

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



为了让模板的定义更为简单,我们提供了以下的宏进行辅助:
【注】如果你觉得宏让你头晕,请忽略这部分,它对理解这个菜单结构并没有实质性的作用。

#define __declare_menu_item_template(__NAME)                                    \
    typedef struct __##__NAME __NAME;
#define declare_menu_item_template(__NAME)                                    \
      __declare_menu_item_template(__NAME)
   
#define __def_menu_item_template(__NAME)                                        \
    struct __##__NAME {                                                         \
      menu_item_t;                                                            
#define def_menu_item_template(__NAME)                                          \
            __def_menu_item_template(__NAME)                                    

#define end_def_menu_item_template(__NAME)                                    \
    };


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



typedef struct __menu_itemmenu_item_t;
typedef struct __menu      menu_t;

typedef fsm_rt_t menu_item_handler_t(menu_item_t *);

struct __menu_item {
    menu_item_handler_t *fnHandle;                      //!< handler
    menu_t            *ptChild;                     //!< Child Menu
};


declare_menu_item_template(default_menu_item_t)

def_menu_item_template(default_menu_item_t)
    //! depends on your application, you can add/remove/change following members
    char                *pchTitle;                      //!< Menu Title
    char                *pchDescription;                //!< Description for this menu item
    char                chShortCutKey;                  //!< Shortcut Key value in current menu
end_def_menu_item_template(default_menu_item_t)


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


typedef struct __menu_engine_cb menu_engine_cb_t;
typedef fsm_rt_t menu_engine_t(menu_engine_cb_t *);

struct __menu {
    menu_item_t      *ptItems;                        //!< menu item list
    uint_fast8_t      chCount;                        //!< menu item count
    menu_t             *ptParent;                     //!< parent menu;
    menu_engine_t      *fnEngine;                     //!< engine for process current menu
};



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


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

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


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

#endif


typedef struct __menu_itemmenu_item_t;
typedef struct __menu      menu_t;

typedef fsm_rt_t menu_item_handler_t(menu_item_t *);

struct __menu_item {
    menu_item_handler_t *fnHandle;                      //!< handler
    menu_t            *ptChild;                     //!< Child Menu
};

typedef struct __menu_engine_cb menu_engine_cb_t;
typedef fsm_rt_t menu_engine_t(menu_engine_cb_t *);

struct __menu {
    menu_item_t      *ptItems;                        //!< menu item list
    uint_fast8_t      chCount;                        //!< menu item count
    menu_t             *ptParent;                     //!< parent menu;
    menu_engine_t      *fnEngine;                     //!< engine for process current menu
};




“菜单的描述要优雅”

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


extern fsm_rt_t top_menu_engine(menu_engine_cb_t*ptThis);

extern fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem);
extern fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem);
extern fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem);

extern const menu_t c_tTopMenu;

default_menu_item_t c_tTopMenuItems[] = {
    {
      top_menu_item_a_handler,
      NULL,                                           //!< child menu
      "Top Menu A",
      "This is Top Menu A",
    },
    {
      top_menu_item_b_handler,
      NULL,                                           //!< child menu
      "Top Menu B",
      "This is Top Menu B"
    },
    {
      top_menu_item_c_handler,
      NULL,                                           //!< child menu
      "Top Menu C",
      "This is Top Menu C"
    }
};

const menu_t c_tTopMenu = {
    (menu_item_t *)c_tTopMenuItems,                                    //!< menu item list
    UBOUND(c_tTopMenuItems),                            //!< menu item count
    NULL,                                             //!< top menu has no parent
    top_menu_engine,                                    
};


fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem)
{
    return fsm_rt_cpl;
}

fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem)
{
    return fsm_rt_cpl;
}

fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem)
{
    return fsm_rt_cpl;
}

fsm_rt_t top_menu_engine(menu_engine_cb_t*ptThis)
{
    return fsm_rt_cpl;
}


通过加入一些辅助宏,我们可以让这个初始化的菜单的定义过程更简单(只消耗ROM):
【注】如果你觉得宏让你头晕,请忽略这部分,它对理解这个菜单结构并没有实质性的作用。

#define __def_menu(__NAME, __PARENT, __ENGINE, __TEMPLATE)                      \
extern const menu_t c_tMenu##__NAME;                                          \
__TEMPLATE c_tMenu##__NAME##Items[] = {
#define def_menu(__NAME, __PARENT, __ENGINE)                                    \
            __def_menu(__NAME, (__PARENT), (__ENGINE), default_menu_item_t)
            
#define def_menu_ex(__NAME, __PARENT, __ENGINE, __TEMPLATE)                     \
            __def_menu(__NAME, (__PARENT), (__ENGINE), __TEMPLATE)

#define __end_def_menu(__NAME, __PARENT, __ENGINE, __TEMPLATE)                  \
    };                                                                        \
    const menu_t c_tMenu##__NAME = {                                          \
      (menu_item_t *)c_tMenu##__NAME##Items,                                  \
      (sizeof(c_tMenu##__NAME##Items)/sizeof(__TEMPLATE)),                  \
      (menu_t *)(__PARENT),                                                   \
      (__ENGINE),                                                             \
    };
#define end_def_menu(__NAME, __PARENT, __ENGINE)                              \
            __end_def_menu(__NAME, (__PARENT), (__ENGINE), default_menu_item_t)
#define end_def_menu_ex(__NAME, __PARENT, __ENGINE, __TEMPLATE)               \
            __end_def_menu(__NAME, (__PARENT), (__ENGINE), __TEMPLATE)
            
#define __extern_menu(__NAME)   extern const menu_t c_tMenu##__NAME;
#define extern_menu(__NAME)   __extern_menu(__NAME)

#define __menu(__NAME)          c_tMenu##__NAME
#define menu(__NAME)            __menu(__NAME)            
         
#define __menu_item(__HANDLER, __CHILD_MENU, ...)                               \
    {                                                                           \
      (__HANDLER),                                                            \
      (menu_t *)(__CHILD_MENU),                                             \
      __VA_ARGS__,                                                            \
    },
#define menu_item(__HANDLER, __CHILD_MENU, ...)                                 \
            __menu_item((__HANDLER), (__CHILD_MENU), __VA_ARGS__)


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


extern fsm_rt_t top_menu_engine(menu_engine_cb_t*ptThis);

extern fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem);
extern fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem);
extern fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem);


def_menu(TopMenu, NULL, top_menu_engine)
    menu_item(
      top_menu_item_a_handler,
      NULL,
      "Top Menu A",
      "This is Top Menu A"
    )
    menu_item(
      top_menu_item_a_handler,
      NULL,
      "Top Menu B",
      "This is Top Menu B"
    )
    menu_item(
      top_menu_item_a_handler,
      NULL,
      "Top Menu C",
      "This is Top Menu C"
    )
end_def_menu(TopMenu, NULL, top_menu_engine)


fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem)
{
    return fsm_rt_cpl;
}

fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem)
{
    return fsm_rt_cpl;
}

fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem)
{
    return fsm_rt_cpl;
}

fsm_rt_t top_menu_engine(menu_engine_cb_t*ptThis)
{
    return fsm_rt_cpl;
}


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



declare_menu_item_template( my_menu_item_t )
def_menu_item_template( my_menu_item_t )
...
end_def_menu_item_template( my_menu_item_t )



def_menu_ex( my_top_menu, NULL, top_menu_engine,my_menu_item_t)

...
    menu_item(
      top_menu_item_a_handler,
      NULL,
      //! initialise the extended members
      ...
    )
...

end_def_menu_ex( my_top_menu, NULL, top_menu_engine,my_menu_item_t)



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

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


extern fsm_rt_t default_menu_engine(menu_engine_cb_t*ptThis);

extern fsm_rt_t top_menu_item_a_handler(menu_item_t *ptItem);
extern fsm_rt_t top_menu_item_b_handler(menu_item_t *ptItem);
extern fsm_rt_t top_menu_item_c_handler(menu_item_t *ptItem);

extern_menu(lv2_menu_A)

def_menu(TopMenu, NULL, default_menu_engine)
    menu_item(
      top_menu_item_a_handler,
      &menu(lv2_menu_A),
      "Top Menu A",
      "This is Top Menu A"
    )
    menu_item(
      top_menu_item_a_handler,
      NULL,
      "Top Menu B",
      "This is Top Menu B"
    )
    menu_item(
      top_menu_item_a_handler,
      NULL,
      "Top Menu C",
      "This is Top Menu C"
    )
end_def_menu(TopMenu, NULL, default_menu_engine)




extern fsm_rt_t lv2_menu_item_1_handler(menu_item_t *ptItem);

def_menu(lv2_menu_A, &menu(TopMenu), default_menu_engine)
    menu_item(
      lv2_menu_item_1_handler,
      NULL,
      "Lv2 Menu 1",
      "This is Lv2 Menu 1"
    )
   ...
end_def_menu(lv2_menu_1, &menu(TopMenu), default_menu_engine)




“实际咋用”

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

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

    menu_t *ptThis = &menu(TopMenu);                        


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

...
    extern_menu(TopMenu);
...                     


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


fsm_rt_t default_menu_engine(menu_engine_cb_t*ptThis)
{
   ...
   return fsm_on_going;
}



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

typedef struct __menu_engine_cb menu_engine_cb_t;
typedef fsm_rt_t menu_engine_t(menu_engine_cb_t *);

struct __menut {
    menu_item_t      *ptItems;                        //!< menu item list
    uint_fast8_t      chCount;                        //!< menu item count
    menu_t             *ptParent;                     //!< parent menu;
    menu_engine_t      *fnEngine;                     //!< engine for process current menu
};

struct __menu_engine_cb {
    const menu_t    *ptCurrentMenu;
    uint_fast8_t    chCurrentItemIndex;
};

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

struct __menu_engine_cb {
    uint_fast8_t tState;
    const menu_t    *ptCurrentMenu;
    uint_fast8_t    chCurrentItemIndex;
};


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


#ifndef this
#   define this      (*ptThis)
#endif

typedef enum {
    KEY_NULL = 0,
    KEY_DOWN,
    KEY_UP,
    KEY_ENTER,
    KEY_ESC,
} key_t;

extern key_t get_key(void);

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

    enum {
      START = 0,
      READ_KEY_EVENT,
      KEY_PROCESS,
      RUN_ITEM_HANDLER
    };
    key_t tKey;
    menu_item_t *ptItem;
   
    switch(this.tState) {
      case START:
            this.tState++;
      case READ_KEY_EVENT:
            tKey = get_key();
            if (KEY_NULL == tKey) {
                break;
            }
            //this.tState = KEY_PROCESS;
            
      case KEY_PROCESS:
            switch (tKey) {
                case KEY_DOWN:
                  this.chCurrentItemIndex++;
                  if (this.chCurrentItemIndex >= this.ptCurrentMenu->chCount) {
                        this.chCurrentItemIndex = 0;
                  }
                  break;
                case KEY_UP:
                  if (0 == this.chCurrentItemIndex--) {
                        this.chCurrentItemIndex = this.ptCurrentMenu->chCount - 1;
                  }
                  break;
                case KEY_ENTER: {
                        ptItem = &(this.ptCurrentMenu->ptItems);
                        if (NULL != ptItem->fnHandle) {
                            this.tState = RUN_ITEM_HANDLER;
                        } else if (NULL != ptItem->ptChild) {
                            this.ptCurrentMenu = ptItem->ptChild;
                            this.chCurrentItemIndex = 0;
                           
                            DEFAULT_MENU_ENGINE_RESET_FSM();
                            return fsm_rt_cpl;
                        }
                  }
                  break;
                case KEY_ESC:
               
                  //! return to upper menu
                  if (NULL != this.ptCurrentMenu->ptParent) {
                        this.ptCurrentMenu = this.ptCurrentMenu->ptParent;
                        this.chCurrentItemIndex = 0;
                           
                        DEFAULT_MENU_ENGINE_RESET_FSM();
                        return fsm_rt_cpl;
                  }
                  break;
                default:
                  break;
            }
            break;
            
      case RUN_ITEM_HANDLER:
            ptItem = &(this.ptCurrentMenu->ptItems);
            fsm_rt_t tFSM = ptItem->fnHandle(ptItem);
            if (IS_FSM_ERR(tFSM)) {
                //! report error
                DEFAULT_MENU_ENGINE_RESET_FSM();
                return tFSM;
            } else if (fsm_rt_cpl == tFSM) {
                DEFAULT_MENU_ENGINE_RESET_FSM();
                return fsm_rt_cpl;
            }
            break;
    }

    return fsm_rt_on_going;
}




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



#ifndef this
#   define this      (*ptThis)
#endif


fsm_rt_t menu_task(menu_engine_cb_t *ptThis)
{
    do {
      /* this validation code could be removed for release version */
      if (NULL == ptThis) {
            break;
      } else if (NULL == this.ptCurrentMenu) {
            break;
      } else if (NULL == this.ptCurrentMenu->fnEngine) {
            break;
      } else if (NULL == this.ptCurrentMenu->ptItems) {
            break;
      } else if (0 == this.ptCurrentMenu->chCount) {
            break;
      }
      
      return this.ptCurrentMenu->fnEngine(ptThis);
      
    } while(false);
   
    return fsm_rt_err;
}

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

void main(void)
{
   ...
   while(1) {
         menu_task(&s_tMenuDemo);
   }
}



“能玩出啥花儿不?”

    看的人多了,提问的人多了,再写!

相关链接
      - [古董贴][交流]怎样在点阵屏上绘图——基于LCD12864
      - 最近读傻孩子的菜单程序,做了一个PPT,希望大家批评指正



打完收工!

鸣谢:200楼 LB342342

823032003 发表于 2018-3-14 21:36:53

强帖收藏 {:smile:}

yuanpiggy 发表于 2018-3-14 21:42:08

这个必须顶

AppleFarm 发表于 2018-3-14 21:42:25

waymcu 发表于 2018-3-14 22:06:07

强帖收藏学习

我是一个大白菜 发表于 2018-3-14 22:14:57

必须收藏了,谢谢大师啊

armku 发表于 2018-3-14 22:30:52

大师的贴,必须顶

Gline77 发表于 2018-3-14 22:32:26

顶起,果断收藏!

就在今天 发表于 2018-3-14 22:44:15

收藏学习!

Wisen 发表于 2018-3-14 22:46:18

坐前排听课{:lol:}

himan 发表于 2018-3-14 22:48:41

等待完成好好学习 上次比着别人的修改了一个 觉得不方便

ziziy 发表于 2018-3-14 22:54:00

收藏学习

gdjsfy_86 发表于 2018-3-14 22:56:35

收藏 学习

hkjabcd 发表于 2018-3-14 22:58:33

欢迎大神!!好久没听到大神讲课啦

chenguanghua 发表于 2018-3-14 23:00:05

前排占座听课!

LQS1200 发表于 2018-3-14 23:05:45

谢谢,学习下

cu_ice 发表于 2018-3-14 23:14:20

前排板凳出租{:lol:}{:lol:}

security 发表于 2018-3-14 23:18:40

占座听课。

lcl 发表于 2018-3-14 23:26:35

C语言渣渣路过,假装听懂,凑个数

金牛AKI 发表于 2018-3-14 23:27:25

说的不错,哈哈

yanyanyan168 发表于 2018-3-14 23:39:14

占座听课

chen_ym 发表于 2018-3-14 23:44:20

小姐姐后来怎么样了?

Gorgon_Meducer 发表于 2018-3-14 23:44:47

chen_ym 发表于 2018-3-14 23:44
小姐姐后来怎么样了?

不知道,毕业了吧。

L7科创 发表于 2018-3-15 00:23:03

假装听懂。。

Gorgon_Meducer 发表于 2018-3-15 01:59:23

终于写完了。哈哈哈。欢迎拍砖。

leiyitan 发表于 2018-3-15 05:47:03

楼主,晚上都不休息么?
每天做项目,都是胡乱拼凑交差…很难像楼主一样能静下心来去总结去思考一些有意思的东西。

wicy001 发表于 2018-3-15 06:11:32

标记一下,慢慢看

Gorgon_Meducer 发表于 2018-3-15 06:26:52

本帖最后由 Gorgon_Meducer 于 2018-11-9 18:44 编辑

故意埋了一个不影响编译和功能的笔误在里面,看看谁真正的读了代码。

这个“逻辑大坑”已经在224楼由 gamep 第一个拿下!恭喜!这是真正认真阅读代码,并认真思考的人。
送上我的感激和敬意。

fbwcpu 发表于 2018-3-15 08:27:06

大一时候的水平就相当高了。。

犯戒和尚 发表于 2018-3-15 08:39:31

不明觉厉

rogerllg 发表于 2018-3-15 09:01:05

标记一下,慢慢再学~

dragonbbc 发表于 2018-3-15 09:03:09

前排占座

cddxhy 发表于 2018-3-15 09:14:32

占座 假装看明白了

guolun 发表于 2018-3-15 09:18:07

像大神这样用心于技术的人,李克强应该给你个国务院津贴,让你专心搞技术。天天喊科技中国,不如来这个实际。

leicai05 发表于 2018-3-15 09:26:37

大一时候的水平真的很高啊。

LVmcu 发表于 2018-3-15 09:27:15

有些定义找不到。比如
typedef struct __menu_itemmenu_item_t;
typedef struct __menut      menu_t;

不舍的六年 发表于 2018-3-15 09:38:20

好久没有见到大师发贴啦{:lol:}

Jmhh247 发表于 2018-3-15 09:50:13

占座学习。

突然奇想,可以取个网红标题《大师十年亲身经历,手把手教你构建数据结构》。。。哈哈

apeng2012 发表于 2018-3-15 09:55:44

真心看不懂。看到一堆define就晕菜了。

apeng2012 发表于 2018-3-15 10:30:31

Gorgon_Meducer 发表于 2018-3-15 06:26
故意埋了一个不影响编译和功能的笔误在里面,看看谁真正的读了代码。

} else if (NULL == this.ptCurrentMenu) {


this不影响编译

Wangwy 发表于 2018-3-15 10:32:39

欢迎大师讲解,俺自带小板凳听课的

alexcnsz 发表于 2018-3-15 10:37:32

收藏了,慢慢学习!

rainbow 发表于 2018-3-15 11:47:30

好好学习一下菜单。

绝对零度 发表于 2018-3-15 11:49:20

强帖!收藏学习!

pjdu 发表于 2018-3-15 12:16:13

收藏,学习!!!

znsword 发表于 2018-3-15 12:27:00

强帖,学习。

ponder2077 发表于 2018-3-15 12:27:07


前排占座听课!

zouzhichao 发表于 2018-3-15 12:36:19

我也写过菜单{:lol:}{:lol:}
https://www.amobbs.com/thread-5670258-1-1.html

kinsno 发表于 2018-3-15 12:54:36

本帖最后由 kinsno 于 2018-3-15 12:59 编辑

Gorgon_Meducer 发表于 2018-3-15 01:59
终于写完了。哈哈哈。欢迎拍砖。

出一个工程会更好,因为有些变量或定义,你并不完整,有人会看的云里来雾里去。。对于新手上路还是有些困难的。。自然也就无从推广喽。。看得懂的,未必会用;

我更喜欢涵数直接跳转。。更简单明了。。就象一个个page的样子。。

我更习惯用page的角度去看界面。。我希望我的各界面是独立存在的。。

PS: 宏这种东西,除了定义类型,定义常量和硬件移值。太晦涩了。因为代码,某个意义上不是写给自己看的,是写给别人看的。。铁打的代码,流水的工程师。。

simplorer 发表于 2018-3-15 15:25:47

原来写过一个菜单,page和item方式实现的。
由于菜单中还牵涉到通讯的问题,一直没找到比较好的解耦方式,希望好孩子能讲讲这方面。
另外,我跟50#的意见相同,define这种东西,并不适合这么大规模使用,
毕竟除了编写者,阅读者和后续维护者要维护宏太麻烦了。

Gorgon_Meducer 发表于 2018-3-15 17:27:11

guolun 发表于 2018-3-15 09:18
像大神这样用心于技术的人,李克强应该给你个国务院津贴,让你专心搞技术。天天喊科技中国,不如来这个实际 ...

哈哈,我也这么想的。

Gorgon_Meducer 发表于 2018-3-15 17:28:29

LVmcu 发表于 2018-3-15 09:27
有些定义找不到。比如
typedef struct __menu_itemmenu_item_t;
typedef struct __menut      menu_t; ...

再往下在看,等讲解到对应内容就会发现定义了。

Gorgon_Meducer 发表于 2018-3-15 17:30:35

apeng2012 发表于 2018-3-15 09:55
真心看不懂。看到一堆define就晕菜了。

其实我已经考虑到这种情况了,请耐心看,真正核心的内容和关键代码,我讲解的时候
都是抛开宏来说的,你即便完全不用我定义的宏,也应该可以通过我提供的代码的原始
形式理解关键的内容。

如果原始形式你有不太明白的地方,请务必留言。我给你解释。

Gorgon_Meducer 发表于 2018-3-15 17:31:05

zouzhichao 发表于 2018-3-15 12:36
我也写过菜单
https://www.amobbs.com/thread-5670258-1-1.html

很喜欢你的用xml的方式!

Gorgon_Meducer 发表于 2018-3-15 17:42:13

本帖最后由 Gorgon_Meducer 于 2018-3-15 18:02 编辑

kinsno 发表于 2018-3-15 12:54
出一个工程会更好,因为有些变量或定义,你并不完整,有人会看的云里来雾里去。。对于新手上路还是有些困 ...

之所以没有提供专门的工程,因为我想避免加入过多其它东西干扰菜单数据结构本身的纯粹性。
我这篇文章提供的定义和变量,原则上必须是完整和独立的——如果你发现缺失了什么,请务必
告诉我。
另外,我并不需要推广,我已经过了需要别人认同的年龄和心里状态,写出来纯属自娱自乐,
顺便让人可以”自助“的获取自己想要的内容,并且可以的话,引起一些讨论,抛砖引玉就可以了。
有人用我快乐,没人用无所谓啊。

至于page的形式,这里我需要说明下。我提供的数据结构本质上只是一个工具,你可以根据你
自己的理解进行使用——当你心中是page,那么,这就是page。打个比方,结构体就是个普通
C语言的语法单元,在面向过程的人心里,他就是个结构体,在面向对象的人心里,他就是个
裁剪版的类,是不是这个道理?

关于宏的问题:
1、首先,我一开始就考虑到了,所以如果你真的认真读了文章会发现,所有核心代码,核心
    数据结构,核心原理都跟宏没有任何关系——全部都用C语言的原始形式提供的。所以可以
    放心食用,宏只是味精,你大可忽略。如果抛开宏以后的部分,你觉得有不好理解或者容易
    让人产生歧义的部分请务必告诉我,非常感谢。
2、我同意你
    ”代码,某个意义上不是写给自己看的,是写给别人看的。。铁打的代码,流水的工程师。。“
    我也是这么想的。

    但我们在根本性的”什么才是好懂“的问题上有不同的理解。我觉得宏的本质是 ”黑盒子“ ,用户
    只要知道黑盒子是做什么的就行了(知道使用方法),并不需要关心黑盒子是怎么实现的。
    换句话说,宏的使用应该让代码逻辑本身更清晰,而隐藏掉不必要的实现细节。也许在你看来
    这种对实现细节的隐藏导致了代码的”晦涩“——这是因为你认为只有知道如何实现才是”容易懂“
    的。我不同意这种观点。

    关于黑盒子和可读性的讨论,我专门写了一篇文章:
    http://mp.weixin.qq.com/s/sF2nAerCBJeVu1Ap_BE-OQ

    希望有所帮助。
3、国内做开发一大悲哀就是,团队技术缺乏统一的体系和传承,即便是在有源代码的情况下,
    往往员工离职,对应的函数代码就成了黑盒子——因为没有人能读懂,或者非常难读懂,或者
    读懂它并不划算——总之就成了无法维护的代码。这种情况长期存在,造成了人们对黑盒子的
    误解,也造成了大家天然喜欢对任何搞不懂的代码一定要搞懂怎么实现的——因为搞不懂就不
    知道怎么维护——天知道哪天维护的脏活就落到自己身上了。

    其实,这是黑盒子背了锅,本质上是因为团队中技术缺乏统一的体系和传承,原本大家技术水
    平就参差不齐,遵循的编码原则也是参差不齐的(也许更糟糕的是编码规范都做不到统一),
    这种情况下,无论有没有源代码都是不好懂得。实际上,对于训练有素,遵循统一技术体系的
    团队,大家尽管水平可能有差距,但通常只体现在大家专注的方向不同——比如你熟悉USB,我
    熟悉WiFi协议栈这种。由于本质上大家都遵守了同样的编码原则,使用了相同的思维方式,有
    良好的文档和交流氛围,在这种情况下,你写的代码和我写的代码看起来甚至区别不开,维护
    就不是一个突出的问题了——谁离职了也无所谓。在这种背景下,黑盒子就有了用武之地,大
    家可以专注于用已有的黑盒子去构建更复杂的系统,而不用去关心黑盒子的实现——黑盒子的
    质量由黑盒子的编写团队负责,你只要信任就可以。

    你们以为我在说一个理想的编码乌托邦么?不,这是我带过的团队的实际情况。所以我不是嘴
    上跑火车,我实际实践过,这样的团队战斗力是惊人的。

Gorgon_Meducer 发表于 2018-3-15 17:43:06

simplorer 发表于 2018-3-15 15:25
原来写过一个菜单,page和item方式实现的。
由于菜单中还牵涉到通讯的问题,一直没找到比较好的解耦方式, ...

关于宏的问题,请看56楼的答案。

关于你说的耦合问题,我很感兴趣,能详细说说你遇到的具体问题是什么?有了具体场景,我才好
具体给出分析和建议。

Gorgon_Meducer 发表于 2018-3-15 17:44:41

Jmhh247 发表于 2018-3-15 09:50
占座学习。

突然奇想,可以取个网红标题《大师十年亲身经历,手把手教你构建数据结构》。。。哈哈 ...

不敢给自己挖坑了……谢邀……标题党无意义。

Gorgon_Meducer 发表于 2018-3-15 17:45:44

本帖最后由 Gorgon_Meducer 于 2018-3-15 17:49 编辑

apeng2012 发表于 2018-3-15 10:30
} else if (NULL == this.ptCurrentMenu) {




这是我的一个疏忽,你吐槽给力!非常感谢!已更新到楼主位。还请继续拍砖。

#ifndef this
#   define this      (*ptThis)
#endif


这不是我说的故意留下的东西,你再仔细看看?

liyfmc 发表于 2018-3-15 17:55:52

楼主,建议再约一下小姐姐,告诉她免费帮她升级菜单程序,再和小姐姐去吃一次大盘鸡!      

Gorgon_Meducer 发表于 2018-3-15 18:00:26

liyfmc 发表于 2018-3-15 17:55
楼主,建议再约一下小姐姐,告诉她免费帮她升级菜单程序,再和小姐姐去吃一次大盘鸡!      ...

兄弟,最近要走桃花运了吧?荷尔蒙水平都影响大脑思考了。

higeo 发表于 2018-3-15 19:59:15

这么长的帖子,我果然没看完

kinsno 发表于 2018-3-15 20:25:13

本帖最后由 kinsno 于 2018-3-15 20:28 编辑

Gorgon_Meducer 发表于 2018-3-15 17:42
之所以没有提供专门的工程,因为我想避免加入过多其它东西干扰菜单数据结构本身的纯粹性。
我这篇文章提 ...

之所以说宏比较晦涩,就是因为第2点的原因,流水的兵。。
每个人面临的编程生态环境不一样,你所说的黑盒子值得信任,实质上是你忽略了团队这个因素。。
不信,你有时间可以做一个调查。。。





dwiller_ARM 发表于 2018-3-15 20:30:33

膜拜下,居然Update了

Gorgon_Meducer 发表于 2018-3-15 21:01:51

本帖最后由 Gorgon_Meducer 于 2018-3-15 21:11 编辑

kinsno 发表于 2018-3-15 20:25
之所以说宏比较晦涩,就是因为第2点的原因,流水的兵。。
每个人面临的编程生态环境不一样,你所说的黑盒 ...

流水兵这个我操作过。我们所有模块都有完善的文档和统一的规范,充分的测试已经保证了质量
所以无所谓。另外,我这里源代码是没啥用的,因为源代码只不过是各种图表的简单机械翻译,
核心的设计都在各种图表里面。所以我们没有办法做对比——要维护的时候大家做两件事:

1、先检查图表和代码之间的翻译是否严格对应
2、直接检查图表(比如状态图,数据流图),看看逻辑上是哪里出现了问题,需要怎样扩展和改进

所以,你说的人员流失导致技术飘变问题在这体系下,根本不是问题。


至于你说的调查,我是清楚的,因为我说了,大环境是这样的,所以我不争辩什么,但因为大家
都是因为缺乏良好的技术体系就说一个操作方式是错的,我实在是不好说什么。换了个环境,你
就知道很多你认知中的规则是不合适的。

另外,现有的情况——凡事都要知道实现而不能拥抱黑盒子,直接影响了系统的规模和程序员自身
的成长。

最后,我不知道为啥,你总觉得我是一个人自HIGH,总觉得我是木有团队经验的……

simplorer 发表于 2018-3-15 21:18:11

Gorgon_Meducer 发表于 2018-3-15 17:43
关于宏的问题,请看56楼的答案。

关于你说的耦合问题,我很感兴趣,能详细说说你遇到的具体问题是什么? ...

你好,我说的耦合问题是这样的:
      我的设计基本是一个变频器的液晶显示,根据功能类,将不同的参数分组,假设是:
      1. 运行参数组;
      2. 配置参数组;
      3. 端子配置组……
      每个一级参数组下面又分为二级参数,拿运行参数组做例子:
      1. 运行参数组:
      1.1 正转速度
      1.2 反转速度
      1.3 正转启动时间
      1.4 反转启动时间……
      然后可能会有三级甚至四级参数,每个参数的最后一级,除非是状态显示,一般都牵涉到参数的读取和修改,
我现在实现方式是菜单和参数通讯参杂在一起,在每个page的回调函数中,定义通讯地址和动作(读写)。
我一直觉得这种实现方式不是太好,可惜又不知道如何用简单的方式分离菜单和通讯。

推而广之,其实菜单显示一般牵涉到按键 通讯和菜单本身,这三类事件要如何组合才能实现如下功能:
1. 功能增加方便,比如说要增加一个长按键功能,不用一个一个修改每个回调函数;
2. 三种操作解耦,尽量牵涉较少。
暂时就想到这么多,谢谢你的回复。

mcu_lover 发表于 2018-3-15 21:30:49

没有考虑可视化编辑器而设计的菜单模型都是耍流氓{:lol:}
视图(文本/图形) + 变量(通信OR回调) 解耦
有兴趣的人,可以好好研究村长早年前公布的一份代码,颇具代表性,虽然功能还不够完善。

Gorgon_Meducer 发表于 2018-3-16 01:59:21

mcu_lover 发表于 2018-3-15 21:30
没有考虑可视化编辑器而设计的菜单模型都是耍流氓
视图(文本/图形) + 变量(通信OR回调) 解耦
有兴趣 ...

好吧,我就耍流氓了,怎么办吧?来打我呀,我是流氓!哈哈哈哈

Gorgon_Meducer 发表于 2018-3-16 02:05:18

simplorer 发表于 2018-3-15 21:18
你好,我说的耦合问题是这样的:
      我的设计基本是一个变频器的液晶显示,根据功能类,将不同的参数 ...

在给出你满意的答案之前,我有几个问题:
- 按键处理上,你有实现按键队列么?难道你在每个菜单的回掉函数里直接调用按键扫描么?
- 你说的通讯和菜单分开,这部分能讲得更详细点么?感觉你先有的描述对我来说还是一头雾水。

先给你说,我觉得你说的这三个部分和这类应用,我是可以做到完全毫不相关的(完全解耦)的。
所以你要让我知道你的问题和困惑在哪里,我才好给你说怎么弄。

apeng2012 发表于 2018-3-16 03:20:38

Gorgon_Meducer 发表于 2018-3-15 17:45
这是我的一个疏忽,你吐槽给力!非常感谢!已更新到楼主位。还请继续拍砖。




this.chCurrentItemIndex--;是这个吧。


有个问题。光标移动(菜单项的上下选择)和子菜单选中怎样显示,没有给出接口。只有菜单项执行的时候有函数接口。

Gorgon_Meducer 发表于 2018-3-16 04:24:40

apeng2012 发表于 2018-3-16 03:20
this.chCurrentItemIndex--;是这个吧。




处理引擎的代码只是一个伪代码,具体显示处理还需要用户自己编写,这个伪代码只是展示了
如何利用已有的最小的数据结构来处理菜单的选择和案件的处理。你可以想象一下,其实只要
每次读取到一个按键都刷新一次显示就可以起到所需的效果了。

bbglx 发表于 2018-3-16 08:34:22


typedef struct __default_menu_item_t  default_menu_item_t;

struct __default_menu_item_t   {

    //! inherit from base class menu_item_t
    menu_item_t;

    //! depends on your application, you can add/remove/change following members
    char                *pchTitle;                      //!< Menu Title
    char                *pchDescription;                //!< Description for this menu item
    char                chShortCutKey;                  //!< Shortcut Key value in current menu
};
大师,是不是menu_item_t; 这行{:lol:}

Jmhh247 发表于 2018-3-16 10:06:49

Gorgon_Meducer 发表于 2018-3-16 02:05
在给出你满意的答案之前,我有几个问题:
- 按键处理上,你有实现按键队列么?难道你在每个菜单的回掉函 ...

看他的描述,我猜测,他的需要求可能是这样的:

在页面1选中一个需要设置的参数,点击确认键后,会进入一个页面2,专门设置这个参数(可能为了用导航键上下加减)。

当在页面2完成设置后,返回到页面1,此时,需要把页面2的数据也返给页面1显示。

页面2和页面1之间的数据交换,就是他所说的通讯?然后是如何解耦这部分?

Jmhh247 发表于 2018-3-16 10:11:32

mcu_lover 发表于 2018-3-15 21:30
没有考虑可视化编辑器而设计的菜单模型都是耍流氓
视图(文本/图形) + 变量(通信OR回调) 解耦
有兴趣 ...

大神,最近也不发技术贴了。。。

Jmhh247 发表于 2018-3-16 10:12:15

论坛改动了吗,打赏功能木有了?

gy54321 发表于 2018-3-16 11:06:38

虽然我还没有看明白,但是看起来很厉害的样子。 帮你顶贴子

didadida 发表于 2018-3-16 11:29:56

菜单一直没玩明白过,学习了大师!!!

卢台长 发表于 2018-3-16 12:42:00

本帖最后由 卢台长 于 2018-3-16 12:59 编辑

1. 结构体中没有为menu_item_t类型定义变量名,什么鬼,居然能不影响编译,话说这不是匿名啊,根本访问不了该成员。
struct __default_menu_item_t   {

    //! inherit from base class menu_item_t
    menu_item_t;                              

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

2. 宏模板中也没有为menu_item_t类型定义变量名;
#define __def_menu_item_template(__NAME)                                        \
    struct __##__NAME {                                                         \
      menu_item_t;
      
3. 看老师的代码,我都看出癖好了,下面menu_task(&s_tMenuDemo); 居然少加了分号。
void main(void)
{
   ...
   while(1) {
         menu_task(&s_tMenuDemo)      
   }
}

思路是能看明白,但是很难想象是如何设计出这个思路的思想,我想应该是对事物模型非常清楚,并了解各层依赖关系等等。

感谢老师带我入嵌入式这一行,话说最近都没弄状态机,好吧我承认是有点懒了,工作上现在都用实时操作系统。{:victory:}

Gorgon_Meducer 发表于 2018-3-16 17:32:37

bbglx 发表于 2018-3-16 08:34
大师,是不是menu_item_t; 这行

还真不是这行,这是匿名结构体。具体看这个帖子:
https://www.amobbs.com/thread-5582609-1-1.html

Gorgon_Meducer 发表于 2018-3-16 17:34:16

Jmhh247 发表于 2018-3-16 10:06
看他的描述,我猜测,他的需要求可能是这样的:

在页面1选中一个需要设置的参数,点击确认键后,会进入 ...

坐等他实际答复了。这种变量传递是应该避免的……

Gorgon_Meducer 发表于 2018-3-16 17:35:03

卢台长 发表于 2018-3-16 12:42
1. 结构体中没有为menu_item_t类型定义变量名,什么鬼,居然能不影响编译,话说这不是匿名啊,根本访问不了 ...

你找的问题,除了最后一个少了分号,其它一个都不在点子上……

工程师030 发表于 2018-3-16 20:58:04

像大神这样用心于技术的人,李克强应该给你个国务院津贴,让你专心搞技术。天天喊科技中国,不如来这个实际。

simplorer 发表于 2018-3-17 13:27:18

Gorgon_Meducer 发表于 2018-3-16 02:05
在给出你满意的答案之前,我有几个问题:
- 按键处理上,你有实现按键队列么?难道你在每个菜单的回掉函 ...

首先谢谢你的回复。
可能是我没说清楚,我现在的应用没有实现按键消息队列,当处于某个菜单层时,会一直调用该菜单层的回调函数(指针函数实现),
在该函数中根据按键消息,进行进一步的处理,比如说进入下一层,返回上一层,修改当前参数值等。
问题是,菜单层特别是最底层的菜单其结构比较复杂。有的是选项(二值或多值),有的数值(正负数都可能),有的是显示(状态显示)。
这样我就需要区分不同的菜单进行不同的显示操作,这个我认为太复杂了。

simplorer 发表于 2018-3-17 13:28:50

mcu_lover 发表于 2018-3-15 21:30
没有考虑可视化编辑器而设计的菜单模型都是耍流氓
视图(文本/图形) + 变量(通信OR回调) 解耦
有兴趣 ...

我找一下村长的帖子,方便的话麻烦给个链接。

mcu_lover 发表于 2018-3-17 14:33:19

simplorer 发表于 2018-3-17 13:28
我找一下村长的帖子,方便的话麻烦给个链接。

https://www.amobbs.com/thread-1205888-1-1.html
主要是思路。大部分人开始难得看明白。

simplorer 发表于 2018-3-17 14:55:37

mcu_lover 发表于 2018-3-17 14:33
https://www.amobbs.com/thread-1205888-1-1.html
主要是思路。大部分人开始难得看明白。 ...

我研究下,多谢!

mcu_lover 发表于 2018-3-17 15:10:07

simplorer 发表于 2018-3-17 14:55
我研究下,多谢!

这个帖子,也可以看,看看完了有多少收获
https://www.amobbs.com/thread-5582451-1-1.html

penny2799 发表于 2018-3-17 16:56:18

收藏,多谢

卢台长 发表于 2018-3-17 18:16:47

本帖最后由 卢台长 于 2018-3-17 18:19 编辑

Gorgon_Meducer 发表于 2018-3-16 17:35
你找的问题,除了最后一个少了分号,其它一个都不在点子上……

搞错了,那个是匿名,由于应用的不多,一时之间没看出来。

那问题确实没找到点子上,太急于表现了{:sweat:} 。


R8C 发表于 2018-3-17 23:12:49

简单菜单,状态机十消息处理,复杂的用专业的GUi

卢台长 发表于 2018-3-18 19:57:47

1. 加一个extern_menu(TopMenu);
2. tState和chCurrentItemIndex应该人为指定初始化比较好吧。
static menu_engine_cb_t s_tMenuDemo = {
    .ptCurrentMenu = &menu(TopMenu),
};


gamep 发表于 2018-3-19 13:31:29

占位认真听讲

Gorgon_Meducer 发表于 2018-3-19 18:32:53

simplorer 发表于 2018-3-17 13:27
首先谢谢你的回复。
可能是我没说清楚,我现在的应用没有实现按键消息队列,当处于某个菜单层时,会一直 ...

我觉得你遇到的问题是几个层面的:

首先说比较根本的,你的系统不是多任务的,这就限制了很多非常好的去耦合的结构无法顺利在你的现有架构
里面铺展开来。比如,GUI常见的消息机制和框架,比如按键的后台处理

其次,从你们的描述中,菜单系统不单纯是个菜单结构了,本质上它在你的系统中扮演者“操作系统”的角色——菜单进行到哪里,处理器
时间就“几乎”全部分配在那里(执行菜单项的处理程序)。菜单引擎本质上被你当作 加强版的单任务前后台系统 在用。实际上,在这种
情况下,每一个菜单处理程序都要独自面对所有的情况——你可以看作是一个APP。这种没有任何后援,只能自己动手丰衣足食的情况,
注定了每一个“APP”都要亲自去“调用”和“组织”所有需要用到的服务——比如按键扫描,比如GUI的绘制等等。虽然说,这种结构下,也有
能力写出耦合度非常低的代码,但总的来说,浪费太大,稍有不注意就会写出高耦合度的系统。

退一步说,从你的问题可以看出:

”菜单层特别是最底层的菜单其结构比较复杂。有的是选项(二值或多值),有的数值(正负数都可能),有的是显示(状态显示)。
这样我就需要区分不同的菜单进行不同的显示操作,这个我认为太复杂了。“

你把菜单赋予了太多不属于它应当承担的职能,除了前面说的”事实上的操作系统“以外,菜单原本只应该是一个类似控制面板一样的抽象
数据结构,应该和具体GUI拆分开来的——每一个菜单处理程序原则上应该是一个 GUI窗体(无论窗体多复杂或者多简单,这里的窗体是
一个抽象概念)——具体用什么控件(Text Box还是DropList还是其它)都应该是窗体负责的。你现在似乎把很多原本具体窗体应该处理的
特殊情况,都想集中到菜单引擎中——这本身就是一个尝试增加耦合度的行为,这也是你现在遇到的问题的本质。

解决方案就是,(假设我们不考虑多任务),让菜单引擎职能单一化,菜单引擎只处理菜单项之间的导航,如果某个子菜单拥有自己的菜单
导航形式,就用定制的菜单引擎。对于你说的数值设置显示,选项,这都是具体菜单项处理程序应该处理的——不是菜单的一部分——你可以
设计或者调用简单的窗体框架来解决。

比如,你设计一些控件(text box,droplist)等等,这些控件在具体的菜单项处理程序里面被组织和调用,因为具体的菜单项处理程序本身
就要处理所有的事情——按键等等,所以它拥有自己的框架和结构是无所谓的。如果你想自己的系统更简洁一些,那么除了菜单框架,你还
需要一个窗体框架。简单说,就是你不应该尝试让菜单框架做所有不属于它应该做的事情。

希望我这样解释对你有所帮助。具体有什么问题,请具体描述后讨论。

simplorer 发表于 2018-3-21 12:27:27

解决方案就是,(假设我们不考虑多任务),让菜单引擎职能单一化,菜单引擎只处理菜单项之间的导航,如果某个子菜单拥有自己的菜单
导航形式,就用定制的菜单引擎。对于你说的数值设置显示,选项,这都是具体菜单项处理程序应该处理的——不是菜单的一部分——你可以
设计或者调用简单的窗体框架来解决。
这一点,你说的太对了,我也注意到了这个问题。
我打算下一步引入一个小的rtos或者简化版的rtos,这样就是实现多任务的合理调度。
另外,菜单就是菜单,只是一个框架。
具体的实现用定制控件实现,这点让我茅塞顿开。
谢谢,下一个项目如果时间允许的话我会将这一思路尝试实现,如有问题,到时候再请教。
再次表示感谢。

simplorer 发表于 2018-3-21 12:32:24

mcu_lover 发表于 2018-3-17 15:10
这个帖子,也可以看,看看完了有多少收获
https://www.amobbs.com/thread-5582451-1-1.html ...

看到了你在这个帖子中的回复,基本跟楼主一个思路。
菜单只是菜单,不要将过多的具体实现跟菜单的架构耦合在一起。

fsmcu 发表于 2018-3-21 15:13:17

菜单好贴,慢慢理解一下思路

fuu 发表于 2018-3-21 17:18:28

好帖,先占位再慢慢看

淋湿的鸡毛 发表于 2018-3-21 20:15:20

真的是傻孩子大神了,好久不见人了

Gorgon_Meducer 发表于 2018-3-22 01:21:30

卢台长 发表于 2018-3-18 19:57
1. 加一个extern_menu(TopMenu);
2. tState和chCurrentItemIndex应该人为指定初始化比较好吧。



这种初始化方式会默认将未指名的member初始化为0。所以也不是问题所在。

hyf88 发表于 2018-3-22 16:33:35

对GUI很熟练哈,

gamep 发表于 2018-3-26 18:18:30

Gorgon_Meducer 发表于 2018-3-15 06:26
故意埋了一个不影响编译和功能的笔误在里面,看看谁真正的读了代码。

UBOUND(c_tTopMenuItems),                            //!< menu item count

是这个吗?
页: [1] 2 3
查看完整版本: [交流][专题]再谈菜单技术 2018-03-14 Update