2007. 2. 15. 21:37
DevX Win32 Assembly Tutorial 8: Menu

Tutorial 8: Menu

이번 장에서는, 윈도우 메뉴를 어떻게 생성하는지를 설명한다.
 소스 1    소스 2    리소스 스크립트    실행 결과  

Theory:

메뉴는 윈도우즈 프로그램에서 매우 중요한 부분이다. 메뉴는, 유저로부터의 요청을 받아들이고 , 메뉴를 보면 프로그램의 전반적인 기능을 이해할 수 있게 되어, 매뉴얼을 보지 않고서도, 프로그램을 조작할 수 있게된다. 메뉴는 이와같이 사용자에게 매우 유용한 도구중의 하나이므로, 표준을 따라야 한다. 표준이란, 처음은 File, 다음으로 Edit, 그리고, 마지막이 Help 가 되도록 하고 있다. Edit와 Help 사이에 자신만의 메뉴를 추가할 수가 있다. 또한, 메뉴의 문자열에, 생략 기호(...)를 추가하게 되어 있다(현재 사용하는 브라우저의 메뉴를 보기바란다).

메뉴는 리소스의 일종이며, 이외에도 다이얼로그 박스, 문자열 테이블, 아이콘, 비트맵등이 있다. 리소스는 일반적으로, resource file(*. rc)에 기록하게 되어 있고,소스 파일과는 분리되어 있다. 그래서, 링크시에 소스파일과 리소스파일을 결합하는 방식으로, 리소스를 포함한 실행 파일을 생성할 수가 있다. (RC -> CVTRES -> Link)

리소스 스크립트는 텍스트 편집기로도 작성할 수 있지만, 표시하는 좌표값과, 그 외의 여러가지 속성을 다루게 되므로, 매우 번거롭다. 그렇기 때문에 보통, VisualC++나 BorlandC++ 등이 제공하는 통합 환경의, 리소스 에디터라는, 리소스 스크립트를 비주얼 화면으로 자동으로 생성해 주는 툴을 사용한다.

예를 들면, 메뉴 리소스를 표시하는 경우, 리소스 스크립트는 다음과 같이 된다.

MyMenu MENU
{
  [menu list here]
}

C프로그래머에게는,이것은 구조체를 선언하고 있는것과 같이 보일지도 모른다. 메뉴이름은,MENU 키워드의 앞의 문자열인 MyMenu 가 되고, 메뉴에 포함되는 리스트는,{ } 사이에 기록하게 되어 있다. 또는 { } 대신에, BEGIN, END 를 사용해도 상관 없다. BEGIN, END의 방식은 Pascal사용자에게는 익숙할 것이다.

메뉴에 포함되는 리스트는 MENUITEM 혹은, POPUP 키워드를 사용한다.

MENUITEM 구문은 선택되었을 때, 메뉴를 pop-up 하지 않는 도구모음을 정의한다. 다음과 같이 사용한다.

MENUITEM "&text", ID [, options]

MENUITEM 키워드의 다음에 도구모음에 사용하고 싶은문자열이 오게 되지만, &기호에 주의하기 바란다. &기호 다음의 문자는, 언더라인이 붙게 된다. &text 의 다음에 있는 것은, 메뉴 아이템의 ID이다. ID라는 것은, 단순한 정수이지만, 메뉴 아이템이 선택되었을 때에 윈도우 프로시져에 보내지는 메세지이므로, 유일한 값이여야 한다. 마지막의 options 는 문자 그대로, 옵션으로 이용할 수 있는 옵션이며 다음과 같은 것이 있다.

  • GRAYED
    이것으로 지정된 메뉴 아이템은 회색으로 표시가 되어, 선택할 수 없게 되기 때문에, WM_COMMAND 메세지가 생성되지 않는다.
  • INACTIVE
    이것으로 지정된 메뉴 아이템은, WM_COMMAND 메세지는 발생 시킬수 없지만, 문자열은 표시된다.
  • MENUBREAK
    이후의 메뉴 아이템은 새로운 열로 생성되어서 그후에 표시된다.
  • HELP
    이 이후의 메뉴 아이템은 오른쪽정렬상태가 된다.

이런 옵션을 or 연산자로 결합해서 사용할 수 있지만, INACTIVE 옵션과 GRAYED 옵션은 동시에 사용할 수 없다.

POPUP구문은 다음과 같이 되어 있다.

POPUP "&text" [, options]
{
 [menu list]
}

POPUP 구문은 선택되었을 경우, 메뉴 아이템의 리스트가 작은 pop-up 윈도우 형태로 표시되는 도구모음을 정의하는 것이다.

MENUITEM 에는 특수한 문법이 있다.MENUITEM SEPARATOR라는 pop-up 윈도우에 가로라인을 그리는 것이 있다.(구분선)

이런 스크립트 파일을 소스파일에서 참조할려면 2가지 방법이 있다.

  • WNDCLASSEX 구조체의 멤버에 있는, lpszMenuName 에 설정 한다. 즉, 메뉴명을 "FirstMenu" 로 하고 싶은 경우는 다음과 같이 한다. (전역메뉴)

    .DATA
      MenuName db "FirstMenu", 0
    ...........................
    ...........................
    .CODE
      ...........................
      mov  wc.lpszMenuName, OFFSET MenuName
      ...........................

  • CreateWindowsEx 함수의 메뉴 핸들 파라미터를 설정 한다. (지역메뉴)

    .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\
      ...........................

당연히, 이들의 차이점이 무엇인지 궁금할 것이다. WNDCLASSEX 구조체에서 설정하는 방법의 경우, 윈도우 클래스의 디폴트 메뉴가 되므로, 그 윈도우 클래스로부터 작성한 모든 윈도우는 같은 메뉴를 사용하게 된다.(전역메뉴)

같은 윈도우 클래스로부터 생성한 윈도우에서, 다른 메뉴를 사용하고 싶은 경우에는, 2번째 방법을 사용한다. 이 경우, WNDCLASSEX 구조체에서 이미 메뉴를 설정했어도, CreateWindowEx 함수에서 설정한 메뉴가 표시된다.

다음으로, 유저가 메뉴를 선택했을 때, 어떻게 윈도우 프로시저가 이것을 구별할 수 있는지를 설명한다.
유저가 메뉴를 선택하면, 윈도우 프로시저로 WM_COMMAND 메세지가 보내지고 wParam의 하위 워드에 선택된 메뉴의 ID가 저장되어 있다.(메뉴구분)

그렇다면 이제 실제로 메뉴를 사용해 보자.

Example:

처음의 예제에서는, 윈도우 클래스에 메뉴를 설정하는 방법(전역메뉴)으로, 어떻게 메뉴를 사용하는지 보여준다.

**************************************************************************************************************************
소스 파일
**************************************************************************************************************************

.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

**************************************************************************************************************************
리소스 파일
**************************************************************************************************************************

#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
}

Analysis:

먼저 리소스파일에 대해 살펴 보자.
파일명은 반드시 rsrc.rc여야한다 : 이유는 RC.exe파일로 생성한 RES파일이 CVTRES로 컴파일하면 소스파일과 같은 OBJ파일이 생성되므로 중복된다. 그러므로 MASM32에서는 rsrc.rc로 권장한다.

#define IDM_TEST    1 /* equal to IDM_TEST equ 1*/
#define IDM_HELLO   2
#define IDM_GOODBYE 3
#define IDM_EXIT    4

이것은, 메뉴 스크립트에서 사용하는 메뉴 ID를 정의하고 있다. 메뉴에서 유일한 어떤값이라도 상관없다.

FirstMenu MENU

메뉴이름을 MENU 키워드로 정의하고 있다.

POPUP "&PopUp"
      {
       MENUITEM "&Say Hello", IDM_HELLO
       MENUITEM "Say &GoodBye", IDM_GOODBYE
       MENUITEM SEPARATOR
       MENUITEM "E&xit", IDM_EXIT
      }

팝업메뉴를 선언하고,4개의 메뉴를 표시하도록 하고 있다. 3번째의 아이템은 메뉴 separator다.(구분줄)

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 은 리소스파일에 정의되어 있는 메뉴이름이다. 리소스파일에는, 복수개의 메뉴를 정의할 수 있으므로, 어떤 메뉴를 사용하는지를 선택해야 한다. 나머지의 3라인은, 메뉴가 선택되었을 때에 메시지박스에 표시하고자 하는 문자열이다.

IDM_TEST    equ 1 ; Menu IDs
IDM_HELLO   equ 2
IDM_GOODBYE equ 3
IDM_EXIT    equ 4

윈도우 프로시저가 사용하는 메뉴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 메세지 대한 처리를 하고있다. 유저가 메뉴를 선택했을 때, WM_COMMAND 메세지가 발생함과 동시에, wParam의 하위 바이트에는 선택된 메뉴 ID가 들어가게 된다. 그렇기 때문에, wParam를 eax 레지스터에 저장하고, ax레지스터의 값이 어떤 메뉴 ID와 일치하는지를 비교해서, 그 메뉴 ID에 대한 적절한 처리를 해주면 된다. IF문에서 각각의 메세지를 표시하는 처리하고 있다.

유저가 Exit 메뉴를 선택하면,해당 윈도우 핸들을 인수로 해서 DestroyWindows 함수를 호출 한다. 보다시피 , 윈도우 클래스에 메뉴를 설정하는것은 간단하다. 하지만, 메뉴를 사용하는 방법이 하나 더 존재한다. 전체 코드를 보여주기는 번거로우므로 해당 코드만 설명한다.리소스파일은 같은것을 사용한다.

.data?
hInstance   HINSTANCE ?
CommandLine LPSTR     ?
hMenu       HMENU     ?  ; handle of our menu

메뉴 핸들을 저장하기 위한 HEMNU형 변수를 정의하고 있다.

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

CreateWindowEx 함수를 호출 하기 전에, 인스턴스 핸들과 메뉴명의 포인터를 인수로 해서 LoadMenu 함수를 호출해서, 리소스파일의 메뉴 핸들을 구하고, 그 메뉴 핸들을 CreateWindowEx 함수에 넘겨주면 된다.


Posted by openserver