Tutorial 32: Multiple Document Interface (MDI)
이번장에서는 MDI 어플리케이션의 생성 방법을 소개한다 그리 어렵지 않기 때문에 걱정하지 않아도 된다.
메인 소스 | 헤더 파일 | 리소스 | 실행 결과 |
|
복수 문서 인터페이스(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:DWORDDefFrameProc와 DefWindowProc를 비교해보면, 인수가 다른 것을 알 수 있을 것이다. 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를 반환한다.
프레임 윈도우를 만드는 방법은, 다음과 같다.
- 보통처럼 WNDCLASSEX 구조체를 초기화한다
- RegisterClassEx함수에 의해 프레임 윈도우 클래스를 등록한다
- CreateWindowEx함수에 의해 프레임 윈도우를 생성한다
- 메시지 루프내에서 TranslateMDISysAccel함수를 호출한다
- 윈도우 프로시저내에서 처리하지 않는 메세지는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를 잊어먹게 되면, 정상적으로 작동 되긴 해도 자식윈도우가 안보이게 된다.
작업윈도우의 생성은 다음과 같은 순서가 된다.
- 윈도우 리스트를 더하고 싶은 메뉴의 핸들을 취득한다
- 메뉴 핸들과 함께, 처음으로 생성되는 자식윈도우의 ID를 CLIENTCREATESTRUCT 구조체에 설정한다
- 클래스명을"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_MDIRESTOREWM_MDIMAXIMIZE는 자식윈도우를 최대화하기 위해, WM_MDIRESTORE는 이전 상태로 되돌리기 위해 사용한다. 이런 동작은 항상 이 메세지를 이용하게 되어 있다. 만약, SW_MAXIMIZE를 이용해 ShowWindow 함수를 호출하면, 자식윈도우는 최대화되긴 하지만, 원래의 크기로 되돌리려고 했을 때 문제가 발생한다. 다만, 최소화하는 경우는, ShowWindow 함수를 사용해도 문제 없다. WM_MDINEXT wParam, lParam의 값을 조작해서, 다음의 자식윈도우, 혹은 이전의 자식윈도우를 액티브하게 한다. WM_MDIREFRESHMENU 프레임 윈도우의 메뉴를 갱신한다. 이 메세지를 전송한 후에, 도구모음을 갱신하기 위해 DrawMenuBar함수를 호출 해야 한다. WM_MDISETMENU 프레임 윈도우의 메뉴, 혹은 부메뉴를 교체 한다. SetMenu 대신에 사용해야 한다. 이 메세지를 전송 후,DrawMenuBar함수를 호출해서, 도구모음을 갱신한다. 보통, 액티브한 자식윈도우가 자신의 메뉴를 가지고 있어서 액티브한 동안만, 그 메뉴를 사용하고 싶은 경우에, 이 메세지를 사용한다. 그럼, MDI 어플리케이션의 생성 방법을 정리해 보자.
- 프레임 윈도우와 자식윈도우의 윈도우 클래스를 등록한다
- CreateWindowEx함수로 프레임 윈도우를 생성한다
- MDI에 관련되는 가속키를 처리하기 위해, 메시지 루프내에서 TranslateMDISysAccel함수를 호출한다
- 프레임 윈도우의 윈도우 프로시저로 처리하지 않는 메세지는 DefFrameProc함수를 호출해서 처리한다
- 이미 등록되어 있는 윈도우 클래스명"MDICLIENT"를, lParam에 CLIENTCREATESTRUCT 구조체의 주소를 각각 지정해서,CreateWindowEx함수를 호출해서 작업윈도우를 생성한다. 보통, 프레임 윈도우의 윈도우 프로시저의 WM_CREATE를 처리하는 부분에서 작업윈도우를 생성한다
- 작업윈도우에 WM_MDICREATE를 전송하는지, 혹은 CreateMDIWindow함수를 호출해서 자식윈도우를 생성한다
- 자식윈도우의 윈도우 프로시저에서 처리하지 않는 메세지는 DefMDIChildProc함수를 호출해서 처리한다
- 만약 존재하면, MDI전용의 메세지를 사용한다. 예를 들면, DestroyWindow함수 대신에 WM_MDIDESTROY 메세지를 사용한다
|
.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
|
우선 시작해야할 것은, 프레임 윈도우와 자식윈도우의 윈도우 클래스를 등록하는 것이다. 그 후, 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, eaxGetSubMenu함수를 호출 할 때 필요한 프레임 윈도우의 메뉴 핸들을 얻기위해 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 .endwWindows가 메세지의 처리를 수행하면 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는 자식윈도우를 닫고 프레임 윈도우의 타이틀을 원래의 타이틀로 되돌린다.