2007. 2. 21. 18:16
DevX Win32 Assembly Tutorial 32: Multiple Document Interface (MDI)

Tutorial 32: Multiple Document Interface (MDI)

이번장에서는 MDI 어플리케이션의 생성 방법을 소개한다 그리 어렵지 않기 때문에 걱정하지 않아도 된다.
 메인 소스    헤더 파일    리소스    실행 결과  

Theory:

복수 문서 인터페이스(MDI)는 동시에 여러개의 문서를 취급할 수 있는 어플리케이션이다. 윈도우 메모장은 싱글 문서 인터페이스(SDI) 어플리케이션이다.
그래서 메모장은 1개의 문서 밖에 다룰수 없으므로, 다른 문서를 편집하고 싶다면, 지금 열려 있는 문서를 닫은 후 편집하고 싶은 문서를 다시 오픈해야 한다.

가장 보편적인 MDI 어플리케이션으로는 MS-Word가 있다. 한 번에 여러개의 문서를 편집할 수 있는 MDI 어플리케이션중 하나인 것이다.

MDI 어플리케이션은 다음과 같은 특징이 있다.

  • 메인 윈도우내의 작업영역에 복수개의 자식윈도우를 생성할 수 있다. 자식윈도우는 작업영역에서 클리핑 된다. (부모윈도우를 벗어나지 못한다)
  • 자식윈도우를 최소화하면, 메인 윈도우의 왼쪽 하단 모서리에 최소화 버튼이 표시된다.
  • 자식윈도우를 최대화하면, 메인 윈도우의 타이틀 바에 자식윈도우의 타이틀이 추가된다.
  • Ctrl+F4로 자식윈도우를 닫을 수가 있고, Ctrl+Tab 버튼으로 자식윈도우간을 이동할 수 있다.

메인 윈도우는프레임 윈도우라 불려진다. 그 작업영역에 자식윈도우가 표시된다.

작업영역에 자식윈도우를 원하는 만큼 생성하기 위해서는,작업윈도우로 불리는 특별한 윈도우가 필요하다. 이 작업윈도우는, 프레임 윈도우의 전체 작업영역을 가리는 숨겨진 윈도우라고 생각할 수가 있다.

그리고, 실제로, 이 작업윈도우가 MDI이며 윈도우의 부모가 된다.



그림 1. MDI 어플리케이션의 계층

   
프레임 윈도우
   
   
|
   
   
작업윈도우
   
   
|
   

|
|
|
|
|
MDI Child 1
MDI Child 2
MDI Child 3
MDI Child 4
MDI Child n

●프레임 윈도우의 작성

그럼 상세한 설명을 하겠다. 먼저 처음으로 프레임 윈도우를 생성할 필요가 있다. 생성하는 방법은 보통의 윈도우와 거의 같아, CreateWindowEx 함수를 호출하면 되며, 이는 2가지의 차이점이 존재한다.

첫번째의 차이점은, 생성한 윈도우에서 처리하고 싶지 않은 메세지가 전송되었을 때에,DefWindowProc이 아니고 DefFrameProc를 호출해야 하는것이다. 만약 DefFrameProc함수를 사용하는 것을 잊게되면, 그 어플리케이션은 MDI가 아니게 되어 버린다. DefFrameProc은 다음과 같다.

DefFrameProc proc hwndFrame:DWORD,
                  hwndClient:DWORD,
                  uMsg:DWORD, 
                  wParam:DWORD, 
                  lParam:DWORD

DefFrameProcDefWindowProc를 비교해보면, 인수가 다른 것을 알 수 있을 것이다. DefFrameProc의 인수는 5개이며 DefWindowProc는 4개이다. 한개 많은 인수는, 작업윈도우의 핸들이다. 이 핸들은 작업윈도우에 MDI 관련의 메세지를 전송하기 위해서 필요하다.

두번째의 차이점은, 프레임 윈도우의 메시지 루프로서 TranslateMDISysAccel함수를 호출해야 하는 것이다. 이것은, Ctrl+F4 / Ctrl+Tab 등과 같은, MDI에 관련된 가속키를 유효하게 하고 싶은 경우에 필요하다. 이것은, 다음과 같다.

TranslateMDISysAccel proc hwndClient:DWORD, lpMsg:DWORD

첫번째의 파라미터는 작업윈도우를 처리해서 , 모든 MDI 차일드 윈도우의 부모윈도우는 작업윈도우가 되도록 하기위해 한 것이기 때문에 굳이 이상할것도 없다. 두번째 파라미터는, GetMessage함수를 호출하는 것으로써 값을 설정한 MSG 구조체의 주소이다. 작업윈도우에 MSG 구조체를 건네주는 것이 그 목적으로, 그 결과 작업윈도우는 MSG 구조체에 MDI에 관한 키보드 메세지가 포함되어 있는지 어떤지를 조사할 수 있다. 만약 포함되어 있으면, 그 메세지를 작업윈도우 자신이 처리하고,0이 아닌값을 반환한다. 포함되지 않으면, FALSE를 반환한다.

프레임 윈도우를 만드는 방법은, 다음과 같다.

  1. 보통처럼 WNDCLASSEX 구조체를 초기화한다
  2. RegisterClassEx함수에 의해 프레임 윈도우 클래스를 등록한다
  3. CreateWindowEx함수에 의해 프레임 윈도우를 생성한다
  4. 메시지 루프내에서 TranslateMDISysAccel함수를 호출한다
  5. 윈도우 프로시저내에서 처리하지 않는 메세지는DefFrameProc함수에 전달한다

●작업윈도우의 생성

이번에는 작업윈도우를 생성한다. 작업윈도우 클래스는 Windows에 의해 이미 등록되어 있다. 클래스명은 「MDICLIENT」이다. 윈도우를 생성할 때,CreateWindowEx함수에,CLIENTCREATESTRUCT구조체의 주소를 지정할 필요가 있다. 이 구조체는 다음과 같다.

CLIENTCREATESTRUCT struct
hWindowMenu    dd ?
idFirstChild    dd ?
CLIENTCREATESTRUCT ends

  • hWindowMenu는, Windows가 MDI의 자식윈도우명의 리스트를 더하는 서브메뉴의 핸들이다. 이것은 조금 설명이 필요할 것이다.
    MS-Word와 같은 MDI 어플리케이션을 지금까지 사용했던 적이 있다면, 현재 열려 있는 자식윈도우의 리스트를 표시하거나 그런 자식윈도우를 늘어놓고 바꾸거나 어느 자식윈도우를 액티브하게 하는지, 등의 기능을 가지는,"윈도우"라는 메뉴가 있는 것을 알 것이다.
    그 리스트는 Windows에 의해 내부적으로 유지되고 있으므로, 아무것도 하지 않아도 잘 작동하고, 단지 hWindowMenu에 서브의 핸들을 설정만 해주면 된다. 이후는 Windows가 알아서 처리해 준다. 추가로, 리스트를 표시하는 메뉴는 "윈도우"라는 메뉴가 아니라도 상관없다. 윈도우의 리스트가 포함되어있는 메뉴의 핸들을 넘겨주면 된다. 만약 리스트가 필요하지 않으면,hWindowMenu에 NULL을 설정하면 되고,메뉴의 핸들은 GetSubMenu함수를 CALL 하는 것으로써 얻을 수 있다.
  • idFirstChild 1번째의자식윈도우 ID이다. Windows는, 어플리케이션이 새롭게 자식윈도우를 생성할 때마다 ID를 증가시킨다(increment). 예를 들면, 100을 지정했을 경우, 최초로 생성한 자식윈도우의 ID는 100이 되고, 2번째에 작성한 자식윈도우는 101, 그 다음은 102가 된다. 이 ID는, 자식윈도우가 윈도우 리스트로부터 선택되었을 때에, WM_COMMAND 메세지를 통해서 프레임 윈도우에 전송된다. 보통, 처리가 필요 없는 WM_COMMAND 메세지는 DefFrameProc함수에 넘기지만, 윈도우 리스트의 메뉴 아이템은 직접 생성한 것이 아니기 때문에, 그런 ID는 지금 없는것이다. 따라서, 처리할 수 없는 것이다. 이것은 MDI 어플리케이션의 특별한 일면이다.
    만약 윈도우 리스트를 보관 유지하고 있으면, 다음과 같이 WM_COMMAND의 처리를 변경해야 한다.

    .elseif uMsg==WM_COMMAND
          .if lParam==0; this message is generated from a menu
              mov eax, wParam
              .if ax==IDM_CASCADE
                     .....
              .elseif ax==IDM_TILEVERT
                    .....
              .else
                    invoke DefFrameProc, hwndFrame, hwndClient, uMsg, wParam, lParam
                    ret
              .endif


    이것을 무시하는 경우에, MDI로 그렇게 처리하면, 유저가 자식윈도우를 클릭했을 때에 액티브하게 되지 않는다. 그래서,DefFrameProc함수에 맡겨서, 적절히 처리하는 것이다.
    idFirstChild에서 주의 해야 할것이 있다. 0을 사용하지 않는 편이 좋은 것이다. 절대로 정상적으로 작동하지 않게 될 것이다. 예를 들면, 액티브하게 되어 있어도 체크 마크가 표시되지 않는등의 문제가 따른다. 100이거나, 그 이상의 값을 사용하도록 하자.

CLIENTCREATESTRUCT 구조체의 초기화가 끝났으므로, CLIENTCREATESTRUCT 구조체의 주소를 lParam로 지정해서 이미 등록되어 있는 윈도우 클래스"MDICLIENT"를 CreateWindowEx함수로 생성하자. 그 때, hWndParent에 프레임 윈도우의 핸들을 지정해야하며, 그것에 의해 Windows는 프레임 윈도우와 작업윈도우의 부모와 자식 관계를 파악할 수 있게 된다. 지정해야 할 윈도우 스타일은 WS_CHILD, WS_VISIBLE, WS_CLIPCHILDREN 이다. 만약, WS_VISIBLE를 잊어먹게 되면, 정상적으로 작동 되긴 해도 자식윈도우가 안보이게 된다.

작업윈도우의 생성은 다음과 같은 순서가 된다.

  1. 윈도우 리스트를 더하고 싶은 메뉴의 핸들을 취득한다
  2. 메뉴 핸들과 함께, 처음으로 생성되는 자식윈도우의 ID를 CLIENTCREATESTRUCT 구조체에 설정한다
  3. 클래스명을"MDICLIENT"로 해서, 설정을 마친 CLIENTCREATESTRUCT 구조체의 주소를 지정하고, CreateWindowEx 함수를 CALL 한다

●MDI자식 윈도우의 생성

지금까지의 설명으로, 프레임 윈도우와 작업윈도우가 생성되므로, 자식윈도우를 생성할 준비가 되었다. 자식윈도우의 생성 방법에는 2가지 있다.

  • 작업윈도우에 WM_MDICREATE 메세지를 전송하고, 동시에 wParam에 MDICREATESTRUCT 구조체의 포인터를 설정한다. 이것이 가장 간단하며, 일반적인 방법이다.

    .data?
        mdicreate MDICREATESTRUCT <>
        ....
    .code
        .....
        [fill the members of mdicreate]
        ......
        invoke SendMessage, hwndClient, WM_MDICREATE, addr mdicreate, 0


    SendMessage함수는, 성공적으로 수행하면 새롭게 생성된 자식윈도우의 핸들을 반환한다. 하지만, 그 핸들은 별로 중요하지 않다.왜냐하면, 다른 방법으로 얻을 수 있기 때문이다. MDICREATESTRUCT는 다음과 같이 정의되고 있다.

    MDICREATESTRUCT STRUCT
    szClass   DWORD ?
    szTitle    DWORD ?
    hOwner    DWORD ?
    x               DWORD ?
    y               DWORD ?
    lx              DWORD ?
    ly              DWORD ?
    style         DWORD ?
    lParam     DWORD ?
    MDICREATESTRUCT ENDS

    szClass 자식윈도우의 템플릿으로 사용하는 윈도우 클래스 주소
    szTitle 자식윈도우의 타이틀 바에 표시되는 문자열
    hOwner 어플리케이션의 인스턴스 핸들
    x, y, lx, ly 자식윈도우의 좌상단의 좌표값과 폭과 높이
    style 자식윈도우의 스타일로, MDIS_ALLCHILDSTYLES를 지정해서 작업윈도우를 생성하면, 어떤 윈도우 스타일에서도 사용할 수 있다
    lParam 어플리케이션 고유하게 정의할 수 있는 32 비트의 값으로, MDI 윈도우 전체에서 공유할 수 있다. 필요없다면 NULL을 설정한다
  • CreateMDIWindow함수를 CALL 한다. 이 함수는 다음과 같이 되어 있다.

    CreateMDIWindow proto lpClassName:DWORD
     lpWindowName:DWORD
     dwStyle:DWORD
     x:DWORD
     y:DWORD
     nWidth:DWORD
     nHeight:DWORD
     hWndParent:DWORD
     hInstance:DWORD
     lParam:DWORD


    이런 파라미터를 가만히 살펴보면,hWndParent이외, MDICREATESTRUCT 구조체와 동일하다는 것을 알 수 있다. 그래서 결국, WM_MDICREATE 메세지에서 필요한 파라미터는 같게된다. MDICREATESTRUCT에 hWndParent멤버는 없다. 왜냐하면 SendMessage로 메세지를 전송하는 앞의 윈도우가 작업윈도우이기 때문이다.

이것을 보고 이상하게 생각할 수도 있다. 도대체 어느 쪽을 사용하면 좋은지?  2개의 차이는 무엇인지? 이에 대한 해답은 다음과 같다.

WM_MDICREATE 메세지를 전송하는 방법은, CALL 하는 코드가 포함되는 thread와 같은 thread 밖에서는 윈도우를 생성할 수 없다. 예를 들면, 2개의 thread가 있다면, 다른 한쪽의 thread로 프레임 윈도우를 생성하고, 다른 한쪽의 thread로 자식윈도우를 생성하고 싶다면, CreateMDIChild 함수를 CALL 하는 방법을 이용해야한다. 처음의 thread에 WM_MDICREATE 메세지를 전송해도 생각대로 작동하지 않기 때문이다.싱글 쓰레드라면 어떤 방법을 사용해도 상관없다.

자식윈도우의 윈도우 프로시저에 관해서 좀 더 자세하게 설명해 두자. 프레임 윈도우에서 처리하지 않는 메세지를 DefWindowProc함수를 호출해서 처리하지만, 자식 윈도우에서는 DefMDIChildProc함수를 사용해야 한다. 파라미터는DefWindowProc때와 같다.

WM_MDICREATE 외에, MDI 관련의 윈도우 메세지를 잠시 소개한다.

WM_MDIACTIVATE 이 메세지는, 자식윈도우를 액티브하게 만들기위해, 어플리케이션으로부터 작업윈도우에 전송된다. 작업윈도우가 이 메세지를 받게되면, 지정된 자식윈도우를 액티브하게 만들며, 자식윈도우에 WM_MDIACTIVATE를 전송한다.
이 메세지는, 어플리케이션이 자식윈도우를 액티브하게 만드는데 사용하는 경우와 자신이 액티브한지를 판단하기 위해, 자식윈도우 자신으로부터 전송되는 경우가 있다. 예를 들면, 자식윈도우가 각각 다른 메뉴를 가지고 있을 경우, 프레임 윈도우의 메뉴를 변경하기 위해서, 이 메세지를 사용할 수 있다.
WM_MDICASCADE
WM_MDITILE
WM_MDIICONARRANGE
자식윈도우를 정렬시키는데 사용한다. 예를 들면, 자식윈도우를 여기저기 흩어놓고 싶은 경우, 작업윈도우에 WM_MDICASCADE 메세지를 전송한다.
WM_MDIDESTROY 자식윈도우를 종료시킬 때, 작업윈도우에 이 메세지를 전송한다. DestroyWindow함수를 호출하는 대신에 이 메세지를 사용해야 한다. 왜냐하면, 자식윈도우가 최대화되어 있으면, 프레임 윈도우의 타이틀이 자식윈도우의 타이틀로 되어 버리기 때문이다.
WM_MDIGETACTIVE 현재 액티브 되어 있는 자식윈도우의 핸들을 구한다
WM_MDIMAXIMIZE
WM_MDIRESTORE
WM_MDIMAXIMIZE는 자식윈도우를 최대화하기 위해, WM_MDIRESTORE는 이전 상태로 되돌리기 위해 사용한다. 이런 동작은 항상 이 메세지를 이용하게 되어 있다. 만약, SW_MAXIMIZE를 이용해 ShowWindow 함수를 호출하면, 자식윈도우는 최대화되긴 하지만, 원래의 크기로 되돌리려고 했을 때 문제가 발생한다. 다만, 최소화하는 경우는, ShowWindow 함수를 사용해도 문제 없다.
WM_MDINEXT wParam, lParam의 값을 조작해서, 다음의 자식윈도우, 혹은 이전의 자식윈도우를 액티브하게 한다.
WM_MDIREFRESHMENU 프레임 윈도우의 메뉴를 갱신한다. 이 메세지를 전송한 후에, 도구모음을 갱신하기 위해 DrawMenuBar함수를 호출 해야 한다.
WM_MDISETMENU 프레임 윈도우의 메뉴, 혹은 부메뉴를 교체 한다. SetMenu 대신에 사용해야 한다. 이 메세지를 전송 후,DrawMenuBar함수를 호출해서, 도구모음을 갱신한다. 보통, 액티브한 자식윈도우가 자신의 메뉴를 가지고 있어서 액티브한 동안만, 그 메뉴를 사용하고 싶은 경우에, 이 메세지를 사용한다.

그럼, MDI 어플리케이션의 생성 방법을 정리해 보자.

  1. 프레임 윈도우와 자식윈도우의 윈도우 클래스를 등록한다
  2. CreateWindowEx함수로 프레임 윈도우를 생성한다
  3. MDI에 관련되는 가속키를 처리하기 위해, 메시지 루프내에서 TranslateMDISysAccel함수를 호출한다
  4. 프레임 윈도우의 윈도우 프로시저로 처리하지 않는 메세지는 DefFrameProc함수를 호출해서 처리한다
  5. 이미 등록되어 있는 윈도우 클래스명"MDICLIENT"를, lParam에 CLIENTCREATESTRUCT 구조체의 주소를 각각 지정해서,CreateWindowEx함수를 호출해서 작업윈도우를 생성한다. 보통, 프레임 윈도우의 윈도우 프로시저의 WM_CREATE를 처리하는 부분에서 작업윈도우를 생성한다
  6. 작업윈도우에 WM_MDICREATE를 전송하는지, 혹은 CreateMDIWindow함수를 호출해서 자식윈도우를 생성한다
  7. 자식윈도우의 윈도우 프로시저에서 처리하지 않는 메세지는 DefMDIChildProc함수를 호출해서 처리한다
  8. 만약 존재하면, MDI전용의 메세지를 사용한다. 예를 들면, DestroyWindow함수 대신에  WM_MDIDESTROY 메세지를 사용한다

Example:

.386 
.model flat, stdcall 
option casemap:none 
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 

WinMain proto :DWORD, :DWORD, :DWORD, :DWORD    

.const 
IDR_MAINMENU  equ 101 
IDR_CHILDMENU equ 102 
IDM_EXIT    equ 40001 
IDM_TILEHORZ  equ 40002 
IDM_TILEVERT  equ 40003
IDM_CASCADE equ 40004 
IDM_NEW     equ 40005 
IDM_CLOSE equ 40006 

.data 
ClassName   db "MDIASMClass", 0 
MDIClientName db "MDICLIENT", 0 
MDIChildClassName db "Win32asmMDIChild", 0 
MDIChildTitle db "MDI Child", 0 
AppName   db "Win32asm MDI Demo", 0 
ClosePromptMessage  db "Are you sure you want to close this window? ", 0

.data?  
hInstance   dd ?  
hMainMenu   dd ?  
hwndClient  dd ?  
hChildMenu  dd ?  
mdicreate   MDICREATESTRUCT <> 
hwndFrame   dd ?  

.code 
start: 
  invoke GetModuleHandle, NULL    
  mov hInstance, eax 
  invoke WinMain, hInstance, NULL, NULL, SW_SHOWDEFAULT 
  invoke ExitProcess, eax 

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD    
  LOCAL wc:WNDCLASSEX 
  LOCAL msg:MSG 
  ;=============================================    
  ; Register the frame window class 
  ;=============================================    
  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 hInstance
  pop wc.hInstance 
  mov wc.hbrBackground, COLOR_APPWORKSPACE 
  mov wc.lpszMenuName, IDR_MAINMENU
  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 
  ;================================================    
  ; Register the MDI child window class 
  ;================================================    
  mov wc.lpfnWndProc, offset ChildProc 
  mov wc.hbrBackground, COLOR_WINDOW+1 
  mov wc.lpszClassName, offset MDIChildClassName 
  invoke RegisterClassEx, addr wc 
  invoke CreateWindowEx, NULL, ADDR ClassName, ADDR AppName,\ 
      WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN, CW_USEDEFAULT,\    
      CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, 0,\ 
      hInst, NULL 
  mov hwndFrame, eax    
  invoke LoadMenu, hInstance, IDR_CHILDMENU 
  mov hChildMenu, eax 
  invoke ShowWindow, hwndFrame, SW_SHOWNORMAL 
  invoke UpdateWindow, hwndFrame 
  .while TRUE 
    invoke GetMessage, ADDR msg, NULL, 0,0 
    .break .if (! eax) 
    invoke TranslateMDISysAccel, hwndClient, addr msg 
    .if ! eax 
      invoke TranslateMessage, ADDR msg 
      invoke DispatchMessage, ADDR msg 
    .endif 
  .endw 
  invoke DestroyMenu, hChildMenu 
  mov eax, msg.wParam 
  ret 
WinMain endp 

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM 
  LOCAL ClientStruct:CLIENTCREATESTRUCT    
  .if uMsg==WM_CREATE 
    invoke GetMenu, hWnd 
    mov hMainMenu, eax 
    invoke GetSubMenu, hMainMenu, 1    
    mov ClientStruct.hWindowMenu, eax 
    mov ClientStruct.idFirstChild, 100 
    INVOKE CreateWindowEx, NULL, ADDR MDIClientName, NULL,\ 
        WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN, CW_USEDEFAULT,\
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hWnd, NULL,\ 
        hInstance, addr ClientStruct    
    mov hwndClient, eax 
    ;======================================= 
    ; Initialize the MDICREATESTRUCT 
    ;======================================= 
    mov mdicreate.szClass, offset MDIChildClassName 
    mov mdicreate.szTitle, offset MDIChildTitle 
    push hInstance    
    pop mdicreate.hOwner 
    mov mdicreate.x, CW_USEDEFAULT 
    mov mdicreate.y, CW_USEDEFAULT    
    mov mdicreate.lx, CW_USEDEFAULT 
    mov mdicreate.ly, CW_USEDEFAULT 
  .elseif uMsg==WM_COMMAND    
    .if lParam==0 
      mov eax, wParam 
      .if ax==IDM_EXIT 
        invoke SendMessage, hWnd, WM_CLOSE, 0,0    
      .elseif ax==IDM_TILEHORZ 
        invoke SendMessage, hwndClient, WM_MDITILE, MDITILE_HORIZONTAL, 0 
      .elseif ax==IDM_TILEVERT 
        invoke SendMessage, hwndClient, WM_MDITILE, MDITILE_VERTICAL, 0    
      .elseif ax==IDM_CASCADE 
        invoke SendMessage, hwndClient, WM_MDICASCADE, MDITILE_SKIPDISABLED, 0     
      .elseif ax==IDM_NEW 
        invoke SendMessage, hwndClient, WM_MDICREATE, 0, addr mdicreate   
      .elseif ax==IDM_CLOSE 
        invoke SendMessage, hwndClient, WM_MDIGETACTIVE, 0,0 
        invoke SendMessage, eax, WM_CLOSE, 0,0 
      .else 
        invoke DefFrameProc, hWnd, hwndClient, uMsg, wParam, lParam     
        ret
      .endif 
    .endif 
  .elseif uMsg==WM_DESTROY 
    invoke PostQuitMessage, NULL 
  .else 
    invoke DefFrameProc, hWnd, hwndClient, uMsg, wParam, lParam 
    ret 
  .endif 
  xor eax, eax 
  ret 
WndProc endp 

ChildProc proc hChild:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD 
  .if uMsg==WM_MDIACTIVATE    
    mov eax, lParam 
    .if eax==hChild 
      invoke GetSubMenu, hChildMenu, 1 
      mov edx, eax 
      invoke SendMessage, hwndClient, WM_MDISETMENU, hChildMenu, edx 
    .else 
      invoke GetSubMenu, hMainMenu, 1    
      mov edx, eax 
      invoke SendMessage, hwndClient, WM_MDISETMENU, hMainMenu, edx 
    .endif 
    invoke DrawMenuBar, hwndFrame 
  .elseif uMsg==WM_CLOSE   
    invoke MessageBox, hChild, addr ClosePromptMessage, addr AppName, MB_YESNO 
    .if eax==IDYES    
      invoke SendMessage, hwndClient, WM_MDIDESTROY, hChild, 0 
    .endif 
  .else 
    invoke DefMDIChildProc, hChild, uMsg, wParam, lParam    
    ret 
  .endif 
  xor eax, eax 
  ret 
ChildProc endp 
end start

Example:

우선 시작해야할 것은, 프레임 윈도우와 자식윈도우의 윈도우 클래스를 등록하는 것이다. 그 후, CreateWindowEx 함수를 호출해서 프레임 윈도우를 생성한다. WM_CREATE 처리 블록에서 작업윈도우를 생성한다.

  LOCAL ClientStruct:CLIENTCREATESTRUCT 
  .if uMsg==WM_CREATE 
    invoke GetMenu, hWnd    
    mov hMainMenu, eax 
    invoke GetSubMenu, hMainMenu, 1 
    mov ClientStruct.hWindowMenu, eax    
    mov ClientStruct.idFirstChild, 100 
    invoke CreateWindowEx, NULL, ADDR MDIClientName, NULL,\    
      WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN, CW_USEDEFAULT,\ 
      CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hWnd, NULL,\    
      hInstance, addr ClientStruct 
    mov hwndClient, eax

GetSubMenu함수를 호출 할 때 필요한 프레임 윈도우의 메뉴 핸들을 얻기위해 GetMenu함수를 호출한다. GetSubMenu함수의 인수에 1을 지정한 것은 윈도우 리스트에 표시되는 부메뉴 번호가 2번째이기 때문이다. 그리고, CLIENTCREATESTRUCT 구조체 멤버에 값을 설정한다.
다음에, MDICLIENTSTRUCT 구조체를 초기화한다. 다만, 예제에서는 별로 중요하지 않다.

  mov mdicreate.szClass, offset MDIChildClassName 
  mov mdicreate.szTitle, offset MDIChildTitle 
  push hInstance 
  pop mdicreate.hOwner 
  mov mdicreate.x, CW_USEDEFAULT    
  mov mdicreate.y, CW_USEDEFAULT 
  mov mdicreate.lx, CW_USEDEFAULT 
  mov mdicreate.ly, CW_USEDEFAULT

프레임 윈도우와 작업윈도우를 생성 한 후,LoadMenu함수를 호출해서 resource file로부터 자식윈도우의 메뉴를 로드한다. 이 메뉴는은 자식윈도우가 표시되면 프레임 윈도우의 메뉴와 바꾸기 위해서 필요하다. 그리고, 어플리케이션이 종료하기 전에 반드시 DestroyMenu함수를 호출해야한다. 보통, Windows는 윈도우에 관련된 메뉴를 자동으로 해제하지만, 이 경우, 자식윈도우 메뉴는 어느 윈도우와도 관련되지 않기 때문에 해제할 수 없는 것이다. 그래서, 어플리케이션이 종료한 후에도 메모리에 남아 버리게 된다.

  invoke LoadMenu, hInstance, IDR_CHILDMENU 
  mov hChildMenu, eax 
  ........
  invoke DestroyMenu, hChildMenu

메시지 루프에서 TranlateMDISysAccel함수를 호출한다

  .while TRUE 
    invoke GetMessage, ADDR msg, NULL, 0,0 
    .break .if (! eax) 
    invoke TranslateMDISysAccel, hwndClient, addr msg 
    .if ! eax 
      invoke TranslateMessage, ADDR msg 
      invoke DispatchMessage, ADDR msg 
    .endif 
  .endw

Windows가 메세지의 처리를 수행하면 TranlateMDISysAccel함수는 0이 아닌값을 반환하므로, 아무것도 할 것은 없다. 0을 반환하면 MDI에 관련한 메세지가 아니므로, 그냥 처리하면 된다.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM 
  .....
  .else 
    invoke DefFrameProc, hWnd, hwndClient, uMsg, wParam, lParam 
    ret 
  .endif
  xor eax, eax
  ret
WndProc endp

프레임 윈도우의 윈도우 프로시저에서, 처리할 필요가 없는 메세지는 DefFrameProc 함수에 전달한다.

윈도우 프로시저의 대부분은 WM_COMMAND 메세지의 처리이다. 「File」메뉴로부터 「New」를 선택하면 새로운 자식윈도우를 생성한다.

  .elseif ax==IDM_NEW 
    invoke SendMessage, hwndClient, WM_MDICREATE, 0, addr mdicreate

이 예제에서는, 작업윈도우에 lParam에 MDICREATESTRUCT의 주소를 지정해서 WM_MDICREATE 메세지를 전송하는 방법으로, 자식윈도우를 생성한다.

ChildProc proc hChild:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD 
  .if uMsg==WM_MDIACTIVATE    
    mov eax, lParam 
    .if eax==hChild 
      invoke GetSubMenu, hChildMenu, 1 
      mov edx, eax 
      invoke SendMessage, hwndClient, WM_MDISETMENU, hChildMenu, edx 
    .else 
      invoke GetSubMenu, hMainMenu, 1    
      mov edx, eax 
      invoke SendMessage, hwndClient, WM_MDISETMENU, hMainMenu, edx 
    .endif    
    invoke DrawMenuBar, hwndFrame 

자식윈도우가 생성되었을 때, 액티브 윈도우인지를 체크하기 위해 WM_MDIACTIVATE 메세지를 감시하고 있다. 이런 체크는, lParam의 값이 액티브한 자식윈도우의 핸들인지를 검사한다. 만약 같으면, 액티브 윈도우이므로, 다음의 과정인 메뉴의 교환을 처리한다. 원래의 메뉴는 옮겨놓을 수 있으므로, Windows에 다시 윈도우 리스트의 메뉴를 설정해야 한다. 그렇기 때문에, 한번 더GetSubMenu함수를 호출해서 부메뉴의 핸들을 얻어야 한다. 그리고, wParam에는 교환하고 싶은 메뉴의 핸들을 지정하고, lParam에는 윈도우 리스트를 표시하고 싶은 부메뉴의 핸들을 지정한 후, WM_MDISETMENU 메세지를 전송한다. WM_MDISETMENU 메세지의 전송 직후, DrawMenuBar함수를 호출해서 메뉴의 갱신을 처리하고 있다.

  .else 
    invoke DefMDIChildProc, hChild, uMsg, wParam, lParam 
    ret 
  .endif 

자식윈도우의 윈도우 프로시저에서, 처리하지 않는 메세지는 DefWindowProc이 아니고 DefMDIChildProc함수에 전달해야만 한다.

  .elseif ax==IDM_TILEHORZ 
    invoke SendMessage, hwndClient, WM_MDITILE, MDITILE_HORIZONTAL, 0  
  .elseif ax==IDM_TILEVERT 
    invoke SendMessage, hwndClient, WM_MDITILE, MDITILE_VERTICAL, 0  
  .elseif ax==IDM_CASCADE 
    invoke SendMessage, hwndClient, WM_MDICASCADE, MDITILE_SKIPDISABLED, 0

유저가 부메뉴의 메뉴 아이템을 선택하면, 거기에 대응된 메세지를 작업윈도우에 전송한다. 만약 유저가 윈도우를 나란히 정렬하기 원한다면, 작업윈도우에 WM_MDITILE를 전송하고, wParam에 어떻게 윈도우를 정렬되는지를  지정한다. WM_CASCADE의 경우도 마찬가지이다.

  .elseif ax==IDM_CLOSE 
    invoke SendMessage, hwndClient, WM_MDIGETACTIVE, 0,0    
    invoke SendMessage, eax, WM_CLOSE, 0,0 

유저가 「Close」를 선택했을 경우, 우선 작업윈도우에 WM_MDIGETACTIVE 메세지를 전송해서, 액티브한 자식윈도우의 핸들을 얻어야 한다. 그 후, 그 윈도우에 WM_CLOSE 메세지를 전송한다.

  .elseif uMsg==WM_CLOSE 
    invoke MessageBox, hChild, addr ClosePromptMessage, addr AppName, MB_YESNO 
    .if eax==IDYES 
      invoke SendMessage, hwndClient, WM_MDIDESTROY, hChild, 0    
    .endif

자식윈도우의 윈도우 프로시저에서 WM_CLOSE 메세지를 받게되면, 유저에게 정말로 닫고 싶은 것인지를 확인한다. 만약 YES가 선택되면 작업윈도우에 WM_MDIDESTROY를 전송한다. WM_MDIDESTROY는 자식윈도우를 닫고 프레임 윈도우의 타이틀을 원래의 타이틀로 되돌린다.



Posted by openserver