ITEEDU

第八课 菜单

本课中我们将在我们的应用程序中加入一个菜单。

理论:

菜单可以说是WINDOWS最重要的元素之一。有了它,用户可以方便地选择操作命令.用户只要细读一下所有的菜单项就可以明了应用程序所提供的大概功能,而且可以立即操作,无须去阅读手册了.正因为菜单给了用户一种方便的方式,所以您在应用程序中加入菜单时就要遵守一般的标准.譬如:一般头两个菜单项是"File"和"Edit",最后是"Help",您可以在这中间插入您要定义的菜单项.如果所运行的菜单命令会弹出一个对话框,那么就要在该菜单项后加入省略符(...).菜单是一种资源,除菜单外还有其它像对话框,字符串,图标,位图资源等.在链接时链接程序将把资源加入到可执行程序中去,最后我们的执行程序中就既包括机器指令又包括了资源. 您可以在任何文本编辑器中编写脚本文件,在文件中您可以指定资源呈现出来的外观和其它的一些属性.当然更直观的方法是用资源编辑器,通常资源编辑器都打包在编译环境中,像Visual C++, Borland C++等都带了资源编辑器. 我们可以按以下方式来定义一个菜单资源:
MyMenu MENU
{
[menu list here]
} 这和C语言中的结构体的定义非常相似。 MyMenu类似于被定义的变量,而MENU则类似于关键字。当然您可以用另外一种办法,那就是用BEGIN和END来代替花括号,这和PASCAL语言中的风格相同。
在菜单项的列表中是一大串的MENUITEM和POPUP语句。MENUITEM定义了一个菜单项,当选择后不会激活对话框。它的语法如下: MENUITEM "&text", ID [,options] 它由关键字MENUITEM开头,紧跟在MENUITEM后的是指菜单项的名称字符串,符号“&“后的第一个字符将会带下画线,它也是该菜单项的快捷键。ID的作用当该菜单被选中时,WINDOWS的消息处理过程用来区分菜单项用的。毫无疑问,ID号必须唯一。options有以下可供选择的属性:
  • GRAYED 代表该菜单项处于非激活状态,即当其被选中时不会产生WM_COMMAND消息。该菜单以灰色显示。
  • INACTIVE 代表该菜单项处于非激活状态,即当其被选中时不会产生WM_COMMAND消息。该菜单以正常颜色显示。
  • MENUBREAK 该菜单项和随后的菜单项会显示在新列中。(译者注:比较难描述,请做实验。)
  • HELP 该菜单项和随后的菜单项右对齐。(译者注:我在WINDOWS2000下编译有该标志的菜单项,该标志好像没起作用)
  • 您可以单独使用以上标志位,也可以把它们或在一起。当然INACTIVE和GRAYED不能同时使用。 POPUP的语法如下:
    POPUP "&text" [,options]
    {
    [menu list]
    }
    POPUP定义了一个菜单项当该菜单项被选中时又会弹出一个子菜单。另外有一种特别类型的MENUITEM语句MENUITEM SEPARATOR,它表示在菜单项位置画一条分隔线。 定义完菜单后,您就可以在程序中使用脚本中定义的菜单资源了。 您可以在程序的两个地方(或叫做用两种方式)使用它们:
  • 在WNDCLASSEX结构体的成员lpszMenuName中。譬如,您有一个菜单“FirstMenu“,您可以按如下方法把它联系到您的窗口:
  • .DATA MenuName db "FirstMenu",0 ...........................
    ...........................
    .CODE ...........................
    mov wc.lpszMenuName, OFFSET MenuName
    ...........................
  • 在CreateWindowEx函数中指明菜单的句柄:
  • .DATA MenuName db "FirstMenu",0
    hMenu HMENU ? ........................... 
    ........................... 
    .CODE         ...........................
                  INVOKE    LOADMENU, HINST, OFFSET MENUNAME
                  MOV       HMENU, EAX
                  INVOKE    CREATEWINDOWEX,NULL,OFFSET CLSNAME,\
    OFFSET Caption, WS_OVERLAPPEDWINDOW,\ 
    CW_USEDEFAULT,CW_USEDEFAULT,\ 
    CW_USEDEFAULT,CW_USEDEFAULT,\ 
    NULL,\ 
    hMenu,\ 
    hInst,\ 
    NULL\ 
    ........................... 您也许会问,这两着之间有什么不同呢?当您用第一种方法时,由于是在窗口类中指定,故所有由该窗口类派生的窗口都将有相同的菜单。如果您想要从相同的类中派生的窗口有不同的菜单那就要使用第二中方法,该方法中通过函数CreateWindowEx指定的菜单会“覆盖”WNDCLASSEX结构体中指定的菜单。 接下来我们看看当用户选择了一个菜单项时它是如何通知WINDOWS 窗口过程的: 当用户选择了一个菜单项时,WINDOWS窗口过程会接收到一个WM_COMMAND消息,传进来的参数wParam的底字节包含了菜单项的ID号。 好了,上面就是关于菜单项的一切,下面我们就来实践。

    例子:

    第一个例子显示了指定一个菜单项的第一种方法:
    .386
    .MODEL        FLAT,STDCALL
    option casemap:none 
    
    WinMain proto :DWORD,:DWORD,:DWORD,:DWORD 
    
         INCLUDE  \MASM32\INCLUDE\WINDOWS.INC
         INCLUDE  \MASM32\INCLUDE\USER32.INC
         INCLUDE  \MASM32\INCLUDE\KERNEL32.INC
      INCLUDELIB  \MASM32\LIB\USER32.LIB
      INCLUDELIB  \MASM32\LIB\KERNEL32.LIB
    
    .DATA
       CLASSNAME  DB        "SIMPLEWINCLASS",0
         APPNAME  DB        "OUR FIRST WINDOW",0
        MENUNAME  DB        "FIRSTMENU",0           ; The name of our menu in the resource file.
     TEST_STRING  DB        "YOU SELECTED TEST MENU ITEM",0
                HELLO_STRING  DB        "HELLO, MY FRIEND",0
              GOODBYE_STRING  DB        "SEE YOU AGAIN, BYE",0
    
    .DATA?
    hInstance HINSTANCE ? 
    CommandLine LPSTR ? 
    
    .CONST
        IDM_TEST  EQU       1           ; Menu IDs
       IDM_HELLO  EQU       2
     IDM_GOODBYE  EQU       3
        IDM_EXIT  EQU       4
    
    .CODE
          START:
                  INVOKE    GETMODULEHANDLE, NULL
                  MOV       HINSTANCE,EAX
                  INVOKE    GETCOMMANDLINE
                  MOV       COMMANDLINE,EAX
                  INVOKE    WINMAIN, HINSTANCE,NULL,COMMANDLINE, SW_SHOWDEFAULT
                  INVOKE    EXITPROCESS,EAX
    
         WINMAIN  PROC      HINST:HINSTANCE,HPREVINST:HINSTANCE,CMDLINE:LPSTR,CMDSHOW:DWORD
                  LOCAL     WC:WNDCLASSEX
                  LOCAL     MSG:MSG
                  LOCAL     HWND:HWND
                  MOV       WC.CBSIZE,SIZEOF WNDCLASSEX
                  MOV       WC.STYLE, CS_HREDRAW OR CS_VREDRAW
                  MOV       WC.LPFNWNDPROC, OFFSET WNDPROC
                  MOV       WC.CBCLSEXTRA,NULL
                  MOV       WC.CBWNDEXTRA,NULL
                  PUSH      HINST
                  POP       WC.HINSTANCE
                  MOV       WC.HBRBACKGROUND,COLOR_WINDOW+1
                  MOV       WC.LPSZMENUNAME,OFFSET MENUNAME     ; Put our menu name here
                  MOV       WC.LPSZCLASSNAME,OFFSET CLASSNAME
                  INVOKE    LOADICON,NULL,IDI_APPLICATION
                  MOV       WC.HICON,EAX
                  MOV       WC.HICONSM,EAX
                  INVOKE    LOADCURSOR,NULL,IDC_ARROW
                  MOV       WC.HCURSOR,EAX
                  INVOKE    REGISTERCLASSEX, ADDR WC
                  INVOKE    CREATEWINDOWEX,NULL,ADDR CLASSNAME,ADDR APPNAME,\
    WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ 
    CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ 
    hInst,NULL 
                  MOV       HWND,EAX
                  INVOKE    SHOWWINDOW, HWND,SW_SHOWNORMAL
                  INVOKE    UPDATEWINDOW, HWND
    .WHILE        TRUE
                  INVOKE    GETMESSAGE, ADDR MSG,NULL,0,0
    .BREAK        .IF (!EAX)
                  INVOKE    DISPATCHMESSAGE, ADDR MSG
    .ENDW
                  MOV       EAX,MSG.WPARAM
                  RET
         WINMAIN  ENDP
    
         WNDPROC  PROC      HWND:HWND, UMSG:UINT, WPARAM:WPARAM, LPARAM:LPARAM
    .IF           UMSG==WM_DESTROY
                  INVOKE    POSTQUITMESSAGE,NULL
    .ELSEIF       UMSG==WM_COMMAND
                  MOV       EAX,WPARAM
    .IF           AX==IDM_TEST
                  INVOKE    MESSAGEBOX,NULL,ADDR TEST_STRING,OFFSET APPNAME,MB_OK
    .ELSEIF       AX==IDM_HELLO
                  INVOKE    MESSAGEBOX, NULL,ADDR HELLO_STRING, OFFSET APPNAME,MB_OK
    .ELSEIF       AX==IDM_GOODBYE
                  INVOKE    MESSAGEBOX,NULL,ADDR GOODBYE_STRING, OFFSET APPNAME, MB_OK
    .ELSE
                  INVOKE    DESTROYWINDOW,HWND
    .ENDIF
    .ELSE
                  INVOKE    DEFWINDOWPROC,HWND,UMSG,WPARAM,LPARAM
                  RET
    .ENDIF
                  XOR       EAX,EAX
                  RET
         WNDPROC  ENDP
                  END       START
    **************************************************************************************************************************

    Menu.rc
    **************************************************************************************************************************
    #define IDM_TEST 1 
    #define IDM_HELLO 2 
    #define IDM_GOODBYE 3 
    #define IDM_EXIT 4 
    
    FirstMenu MENU 
    { 
           POPUP  "&POPUP"
    { 
        MENUITEM  "&SAY     HELLO",IDM_HELLO
    MENUITEM "Say &GoodBye", IDM_GOODBYE 
    MENUITEM SEPARATOR 
        MENUITEM  "E&XIT",IDM_EXIT
    } 
        MENUITEM  "&TEST",          IDM_TEST
    } 

    分析:

    我们先来分析资源文件:
    #define IDM_TEST 1 /* equal to IDM_TEST equ 1*/ 
    #define IDM_HELLO 2 
    #define IDM_GOODBYE 3 
    #define IDM_EXIT 4 
    上面的几行定义了菜单项的ID号。只要注意菜单项ID号必须唯一外,您可以给ID号任何值。

    FirstMenu MENU

    用关键字MENU定义菜单。

           POPUP  "&POPUP"
    { 
        MENUITEM  "&SAY     HELLO",IDM_HELLO
    MENUITEM "Say &GoodBye", IDM_GOODBYE 
    MENUITEM SEPARATOR 
        MENUITEM  "E&XIT",IDM_EXIT
    } 

    定义一个有四个菜单项的子菜单,其中第三个菜单项是一个分隔线。

    MENUITEM "&Test", IDM_TEST

    定义主菜单中的一项。下面我们来看看源代码。

    
        MENUNAME  DB        "FIRSTMENU",0           ; The name of our menu in the resource file.
     TEST_STRING  DB        "YOU SELECTED TEST MENU ITEM",0
                HELLO_STRING  DB        "HELLO, MY FRIEND",0
              GOODBYE_STRING  DB        "SEE YOU AGAIN, BYE",0
    MenuName是资源文件中指定的菜单的名字。因为您可以在脚本文件中定义任意多个菜单,所以在使用前必须指定您要使用那一个,接下来的行是在选中菜单项时显示在相关对话框中的字符串。
        IDM_TEST  EQU       1           ; Menu IDs
       IDM_HELLO  EQU       2
     IDM_GOODBYE  EQU       3
        IDM_EXIT  EQU       4
    定义用在WINDOWS窗口过程中的菜单项ID号。这些值必须和脚本文件中的相同。
    .ELSEIF       UMSG==WM_COMMAND
                  MOV       EAX,WPARAM
    .IF           AX==IDM_TEST
                  INVOKE    MESSAGEBOX,NULL,ADDR TEST_STRING,OFFSET APPNAME,MB_OK
    .ELSEIF       AX==IDM_HELLO
                  INVOKE    MESSAGEBOX, NULL,ADDR HELLO_STRING, OFFSET APPNAME,MB_OK
    .ELSEIF       AX==IDM_GOODBYE
                  INVOKE    MESSAGEBOX,NULL,ADDR GOODBYE_STRING, OFFSET APPNAME, MB_OK
    .ELSE
                  INVOKE    DESTROYWINDOW,HWND
    .ENDIF

    在本窗口过程中我们处理WM_COMMAND消息。当用户选择了一个菜单项时,该菜单项的ID放入参数wParam中被同时送到WINDOWS的窗口过程,我们把它保存到eax寄存器中以便和预定义的菜单项ID比较用。前三种情况下,当我们选中Test、Say Hello、Say GoodBye菜单项时,会弹出一个对话框其中显示一个相关的字符串,选择Exit菜单项时,我们就调用函数DestroyWindow,其中的参数是我们窗口的句柄,这样就销毁了窗口。 就像您所看到的,通过在一个窗口类中指定菜单名的方法来给一个应用程序生成一个菜单是简单而直观的。除此方法外您还可以用另一种方法,其中资源文件是一样的,源文件中也只有少数的改动,这些改动如下:

    .DATA?
    hInstance HINSTANCE ? 
    CommandLine LPSTR ? 
    hMenu HMENU ? ; handle of our menu 
    定义了一个变量来保存我们的菜单的句柄,然后:
                  INVOKE    LOADMENU, HINST, OFFSET MENUNAME
                  MOV       HMENU,EAX
                  INVOKE    CREATEWINDOWEX,NULL,ADDR CLASSNAME,ADDR APPNAME,\
    WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ 
    CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\ 
    hInst,NULL 

    调用LoadMenu函数,该函数需要实例句柄和菜单名的字符串,调用的结果返回指向菜单的句柄,然后传给函数CreateWindowEx刚返回的菜单句柄就可以了。