首页 > C/C++语言 > C/C++数据结构 > [原创] 使用消息映射表处理菜单
2005
12-15

[原创] 使用消息映射表处理菜单

    江南以南在测试题区发了个题目,后面是他的代码。题目是个综合作业,具体什么内容就不说了。其中最主要的部分,是要在程序运行之后,执行下列功能:

A.程序运行时,首先显示主菜单如下:
1.新建数据
2.添加数据
3.删除数据
4.排序
5.查询
6.退出
请输入序号选择相应操作


用户输入序号后,程序进行相应操作。


B.在主菜单中选择序号4,弹出子菜单选择排序方式,子菜单如下:
1.数学成绩排序
2.程序设计成绩排序
3.总分排序
4.返回主菜单
请按序号选择相应操作


选择子菜单的序号后,程序能正确运行并在屏幕上显示按要求排序后的相关信息。
C.在主菜单中选择序号5,弹出子菜单选择查询方式,子菜单如下:
1.学号查询
2.姓名查询
3.数学成绩查询
4.程序设计成绩查询
5.总分查询
6.返回主菜单
请按序号选择相应操作


    这种先显示一个菜单,然后根据用户的不同输入执行不同操作的方式被称为菜单驱动。过去DOS时代70%的程序是这样运行的。今天,Windows平台上虽然不再使用那种很古老的菜单,但任何可见的操作,包括菜单选择、点击工具栏、鼠标拖放在本质上都可以看成用户在使用某种变形的菜单。程序运行时还有大量用户不可见的系统信号被传入,应用程序也必须做出处理。所有这些用户操作和系统信号都可以看成是一种消息,根据不同的消息内容,对应有不同的动作,我们就称这种方式为消息驱动。下面我们就以上面的作业为例来讲讲消息驱动的两种最常见的实现方式:switch/case结构 和 消息映射表机制。


    为了方便,我们只考虑上面例题中的主菜单,主菜单会处理了,子菜单也是很方便的事。首先我们应该想到,为了程序清晰和便于调试,把菜单中的每一项功能都做成单独的函数是非常好的。于是我们声明以下的函数,使之对应上述六项功能:
int new_data( void );
int add_data( void );
int del_data( void );
int sort_data( void );
int query_data( void );
int quit_prog( void );
    通常建议,把函数声明和函数定义分开,函数的具体实现尽可能的放在main函数的后面,这既方便阅读修改,也符合结构化编程逐层细化的要求。所以这里,我们不给出这些函数的实现,同时,这些功能所需的参数完全可以在函数内部运行时去取得,因此所有函数形参表均为void,这会在后面带来些好处。
    然后我们就来实现,完全可以把所有与menu相关的代码放在一个函数里,代码如下:
menu( void )
{
    char ch; //用于获得输入
    draw_menu(); //画出菜单
    while((ch=getchar())!=’0′)
    {
 ……
 ……  // 菜单处理
 draw_menu();
    }
}
    中间两行省略号的地方,就是我们要实现的消息驱动。
    先考虑最简单的一种方式,用if语句。代码如下:
if ( ch == ’1′ ) new_data( );
if ( ch == ’2′ ) add_data( );
if ( ch == ’3′ ) del_data( );
if ( ch == ’4′ ) sort_data( );
if ( ch == ’5′ ) query_data( );
if ( ch == ’6′ ) quit_prog( );
最基本的方法,直观明确,而且它是其他方法的根本。但是非常呆滞,同时打字量很大,不常用。我们看看常见的方案。


一.switch/case结构
switch/case结构是C/C++语言内置语法,专用于这种地方。代码如下:
switch (ch)
{
    case ’1′:
 new_data();
 break;
    case ’2′:
 add_data();
 break;
    case ’3′:
 del_data();
 break;
    case ’4′:
 sort_data();
 break;
    case ’5′:
 query_data();
 break;
    case ’6′:
 quit_prog();
 break;
}
没啥可说的,语法很明白。本质上和用if没什么区别,但是switch可以形成一个逻辑上的整体,便于理解。


二.消息映射表
    switch的确很好用,但也有缺点。如果遇到选项非常多的情况(比如几百个),必然导致一个极大的switch结构,windows编程这种情况是常见的。所以更流行的是消息映射表,我写作本文的目的,主要就是为了向大家介绍这种方法。
    菜单/消息驱动的本质是什么。可以看到,任何消息,最终都可以看作是一个整数,而任何操作,不过是一个函数。所谓消息驱动,不过是在一个整数和一个函数之间建立对应关系。先看代码:


struct message_entry //定义消息结构
{
    char message;
    int (*operate)(void); //指向 返回值为int,参数为void的函数的指针
}


struct message_entry message_map[]= //消息映射表
{
    { ’1′, new_data() },
    { ’2′, add_data() },
    { ’3′, del_data() },
    { ’4′, sort_data() },
    { ’5′, query_data() },
    { ’6′, quit_prog() }
}


const int size_message_map =
      sizeof(message_map) / sizeof(message_entry);   //求得表长


int i = 0;
for( i = 0; i < size_message_map; i++)
{
    if( message_entry[i].message == message_input )
         (*(message_entyr[i].operate) )(void)
}


    所谓消息和函数的对应,可以用一个struct来表达。第一个成员是消息,在本例,他就是字符变量。第二个成员用来代表函数。怎么把函数作为成员呢,用指向函数的指针。任何函数在定义后就是内存中的一段代码,可以用一个指针来指向,这就是函数指针。他可以实现在不提供函数名的情况下,运行函数的工作。函数指针的具体语法请自行参阅教材,这里不多说了。
    这样定义好的struct就可以用来声明消息映射。但显然不止一个映射,所以我们用一个数组来保存所有的消息映射,这个数组就是消息映射表。上述代码的第二部分就是声明和初始化这个数组的过程。这时可以看到刚才把函数的形参表和返回值都设置成一样类型的好处,都可以用同一个指针表达。
    之后的工作其实非常简单了,用一个for循环,依次查找这个数组的每一项,只要输入消息与数组某一项内的消息相同,就执行该项内函数指针对应的函数。这里只有一个小小的问题,循环需要整个数组的长度,我们用sizeof(message_map) / sizeof(message_entry) 来求得即可。
   
    第一次编写消息映射表似乎相当繁琐,但消息映射表的最大好处是可以重用。想想看,今天你完成了这个项目,下一次你又有一个菜单驱动的程序,如果使用switch结构,基本上整个部分都要重写。如果是消息映射表,基本上只有数组初始化那个部分是需要修改的。其次,不管有多少消息,都只集中出现在初始化那个部分,逻辑清晰。
    使用消息映射表,固定不变的部分相当多。可以用宏把这些全部包含起来放入头文件。代码如下:


#define DECLARE_MESSAGE_MAP() \
struct message_entry \
{
    char message; \
    int (*operate)(void); \
}


#define BEGIN_MESSAGE_MAP \
struct message_entry message_map[]= {


#define ON_MENU(menu_id, Fxn) \
{ menu_id, Fxn() },


#define END_MESSAGE_MAP \
 {0, 0} \
 };


#define size_message_map \
      ( sizeof(message_map) / sizeof(message_entry) – 1 )


   等到使用时,就只要这样干:


DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP


    ON_MENU( ’1′, new_data)
    ON_MENU( ’2′, add_data)
    ON_MENU( ’3′, del_data)
    ON_MENU( ’4′, sort_data)


END_MESSAGE_MAP


    用过VC的朋友有没有觉得很眼熟呢,MFC的消息处理部分和这里几乎是一样的。实际上,大部分的消息驱动型程序都使用宏配合消息映射表来解决问题,不论是pc平台还是嵌入式系统,这几乎是一种通用的方案。甚至是C++的虚函数,也是类似消息映射表的机制。消息映射表如此有趣,我们还有什么理由不学习他呢。


[原创] 使用消息映射表处理菜单》有 5 条评论

  1. VC爱好者 说:

    哇~~兄弟真有时间。去分析了???????/

  2. xstar 说:

    有些地方尚可改进一下。
    原文中既然已经实现了message_map[],则在后面完全可以拿来用!
    更改一下结构会更加直观有效!

    struct message_entry //定义消息结构
    {
        int iMsg;
        int (*operate)(void); //指向 返回值为int,参数为void的函数的指针
    }

    struct message_entry message_map[]= //消息映射表
    {
        { 0, new_data() },
        { 1, add_data() },
        { 2, del_data() },
        { 3, sort_data() },
        { 4, query_data() },
        { 5, quit_prog() }
    }

    const int size_message_map =
          sizeof(message_map) / sizeof(message_entry);   //求得表长

    int iMsgID;
    iMsgID = getinput();                                     //得到int iMsg的值,或从其他的函数转换而来;

    message_entyr[iMsgID].operate();  //然后直接调用就可以了!或判断一下是否超出界限;

    后面的宏同样改一下:
        ON_MENU( 1, new_data)
        ……

    多维数组(表)的灵活使用并结合一个高效的查表方法!可以实现很多功能!

    xstar38737.0322222222

  3. lensing01 说:

    好帖

  4. zwind_fancy 说:

    恩 真的很好啊 支持耳朵

  5. hiroki 说:

    支持支持楼主

留下一个回复