Tutorial 31: Listview Control | Tutorial 32: Multiple Document Interface (MDI) | Tutorial 33: RichEdit Control: Basics |
This tutorial shows you how to create MDI application. It's actually not too difficult to do. Download the example.
Multiple Document Interface (MDI) is a specification for applications that handle multple documents at the same time. You are familiar with Notepad: It's an example of Single Document Interface (SDI). Notepad can handle only one document at a time. If you want to open another document, you have to close the previous one first. As you can imagine, it's rather cumbersome. Contrast it with Microsoft Word: Word can open arbitrary documents at the same time and let the user choose which document to use. Microsoft Word is an example of Multiple Document Interface (MDI).
MDI application has several characteristics that are distinctive. I'll list some of them:
The main window that contains the child windows is called a frame window. Its client area is where the child windows live, hence the name "frame". Its job is a little more elaborate than a usual window because it needs to handle some coordination for MDI.
To control an arbitrary number of child windows in your client area, you need a special window called client window. You can think of this client window as a transparent window that covers the whole client area of the frame window. It's this client window that is the actual parent of those MDI child windows. The client window is the real supervisor of the MDI child windows.
|
|||||||||
| | |||||||||
|
|||||||||
| | |||||||||
| | | | | | | | | | |||||
|
|
|
|
|
figure 1. the hierachy of an mdi application
Now we can turn our attention to the detail. First of all you need to create a frame window. It's created the same way as the normal window: by calling CreateWindowEx. There are two major differences from a normal window.
The first difference is that you must call DefFrameProc instead of DefWindowProc to process the Windows messages your window don't want to handle. This is one way to let Windows do the dirty job of maintaining MDI application for you. If you forget to use DefFrameProc, your application won't get the MDI feature. Period. DefFrameProc has the following syntax:
DefFrameProc PROC hwndFrame:DWORD,
hwndClient:DWORD,
uMsg:DWORD,
wParam:DWORD,
lParam:DWORD
If you compare DefFrameProc with DefWindowProc, you'll notice that the only difference between them is that DefFrameProc has 5 parameters while DefWindowProc has only 4. The extra parameter is the handle to the client window. This handle is necessary so Windows can send MDI-related messages to the client window.
The second difference is that, you must call
TranslateMDISysAccel in the message loop of your frame
window. This is necessary if you want Windows to handle MDI-related
accelerator key strokes such as Ctrl+F4, Ctrl+Tab for you.
It has the following syntax:
TranslateMDISysAccel PROC hwndClient:DWORD, lpMsg:DWORD
The first parameter is the handle to the client window. This should not come as a surprise to you because it's the client window that is the parent of all MDI child windows. The second parameter is the address of the MSG structure you filled by calling GetMessage. The idea is to pass the MSG structure to the client window so it could examine if the MSG structure contains the MDI-related keypresses. If so, it processes the message itself and returns a non-zero value, otherwise it returns FALSE.
The steps in creating the frame window can be summarized as follows:
Now that we have the frame window, we can create the client window. The client window class is pre-registered by Windows. The class name is "MDICLIENT". You also need to pass the address of a CLIENTCREATESTRUCT structure to CreateWindowEx. This structure has the following definition:
CLIENTCREATESTRUCT STRUCT
hWindowMenu dd ?
idFirstChild dd ?
CLIENTCREATESTRUCT ENDS
hWindowMenu is the handle to the submenu that Windows will append the list of MDI child window names. This feature requires a little explanation. If you ever use an MDI application like Microsoft Word before, you'll notice that there is a submenu named "window" which, on activation, displays various menuitems related to window management and at the bottom, the list of the MDI child window currently opened. That list is internally maintained by Windows itself: you don't have to do anything special for it. Just pass the handle of the submenu you want the list to appear in hWindowMenu and Windows will handle the rest. Note that the submenu can be any submenu:it doesn't have to be the one that is named "window". The bottom line is that, you should pass the handle to the submenu you want the window list to appear. If you don't want the list, just put NULL in hWindowMenu. You get the handle to the submenu by calling GetSubMenu.
idFirstChild is the ID of the first
MDI child window. Windows increments the ID for each new MDI child window the
application created. For example, if you pass 100 to this field, the first MDI
child window will have the ID of 100, the second one will have the ID of 101
and so on. This ID is sent to the frame window via WM_COMMAND when the MDI child
window is selected from the window list. Normally you'll pass this "unhandled"
WM_COMMAND messages to DefFrameProc. I use the word "unhandled" because
the menuitems in the window list are not created by your application thus your
application doesn't know their IDs and doesn't have the handler for them. This
is another special case for the MDI frame window: if you have the window list,
you must modify your WM_COMMAND handler a bit like this:
.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
Normally, you would just ignore the messages from unhandled cases. But In the MDI case, if you ignore them, when the user clicks on the name of an MDI child window in the window list, that window won't become active. You need to pass them to DefFrameProc so they can be handled properly.
A caution on the value of idFirstChild: you should not use 0. Your window list will not behave properly, ie. the check mark will not appear in front of the name of the first MDI child even though it's active. Choose a safe value such as 100 or above.
Having filled in the CLIENTCREATESTRUCT structure, you can create the client window by calling CreateWindowEx with the predefined class name,"MDICLIENT", and passing the address of the CLIENTCREATESTRUCT structure in lParam. You must also specify the handle to the frame window in the hWndParent parameter so Windows knows the parent-child relationship between the frame window and the client window. The window styles you should use are: WS_CHILD, WS_VISIBLE and WS_CLIPCHILDREN. If you forget WS_VISIBLE, you won't see the MDI child windows even if they were created successfully.
The steps in creating the client window are as follows:
Now you have both the frame window and the client window. The stage is now ready for the creation of the MDI child window. There are two ways to do that.
.data?
mdicreate MDICREATESTRUCT <>
.....
.code
.....
[fill the members of mdicreate]
......
invoke SendMessage, hwndClient, WM_MDICREATE,ADDR mdicreate,0
SendMessage will return the handle of the newly created MDI child window if successful. You don't need to save the handle though. You can obtain it by other means if you want to. MDICREATESTRUCT has the following definition.
MDICREATESTRUCT STRUCT
szClass DWORD ?
szTitle DWORD ?
hOwner DWORD ?
x DWORD ?
y DWORD ?
lx DWORD ?
ly DWORD ?
style DWORD ?
lParam DWORD ?
MDICREATESTRUCT ENDS
szClass | the address of the window class you want to use as the template for the MDI child window. |
---|---|
szTitle | the address of the text you want to appear in the title bar of the child window |
hOwner | the instance handle of the application |
x,y,lx,ly | the upper left coordinate and the width and height of the child window |
style | child window style. If you create the client window with MDIS_ALLCHILDSTYLES, you can use any window style. |
lParam | an application-defined 32-bit value. This is a way of sharing values among MDI windows. If you don't need to use it, set it to NULL |
CreateMDIWindow PROTO lpClassName:DWORD, \
lpWindowName:DWORD, \
dwStyle:DWORD, \
x:DWORD, \
y:DWORD, \
nWidth:DWORD, \
nHeight:DWORD, \
hWndParent:DWORD, \
hInstance:DWORD, \
lParam:DWORD
If you look closely at the parameters, you'll find that they are identical to the members of MDICREATESTRUCT structure, except for the hWndParent. Essentially it's the same number of parameters you pass with WM_MDICREATE. MDICREATESTRUCT doesn't have the hWndParent field because you must pass the whole structure to the correct client window with SendMessage anyway.
At this point, you may have some questions: which method should I use ? What is the difference between the two ? Here is the answer:
The WM_MDICREATE method can only create the MDI child window in the same thread as the calling code. For example, if your application has 2 threads, and the first thread creates the MDI frame window, if the second thread wants to create an MDI child, it must do so with CreateMDIChild: sending WM_MDICREATE message to the first thread won't work. If your application is single-threaded, you can use either method. (Thanks yap for the correction - 04/24/2002)
A little more detail needs to be covered about the window procedure of the MDI child. As with the frame window case, you must not call DefWindowProc to handle the unprocessed messages. Instead, you must use DefMDIChildProc. This function has exactly the same parameters as DefWindowProc.
In addition to WM_MDICREATE, there are other MDI-related window messages. I'll list them below:
WM_MDIACTIVATE | This message can be sent by the application to the client window to instruct the client window to activate the selected MDI child. When the client window receives the message, it activates the selected MDI child window and sends WM_MDIACTIVATE to the child being deactivated and activated. The use of this message is two-fold: it can be used by the application to activate the desired child window. And it can be used by the MDI child window itself as the indicator that it's being activated/deactivated. For example, if each MDI child window has different menu, it can use this opportunity to change the menu of the frame window when it's activated/deactivated. |
---|---|
WM_MDICASCADE WM_MDITILE WM_MDIICONARRANGE |
These messages handle the arrangement of the MDI child windows. For example, if you want the MDI child windows to arrange themselves in cascading style, send WM_MDICASCADE to the client window. |
WM_MDIDESTROY | Send this message to the client window to destroy an MDI child window. You should use this message instead of calling destroywindow because if the MDI child window is maxmized, this message will restore the tile of the frame window. If you use destroywindow, the title of the frame window will not be restored. |
WM_MDIGETACTIVE | Send this message to retrieve the handle of the currently active MDI child window. |
WM_MDIMAXIMIZE WM_MDIRESTORE |
Send WM_MDIMAXIMIZE to maximize the MDI child window and WM_MDIRESTORE to restore it to previous state. Always use these messages for the operations. If you use ShowWindow with SW_MAXIMIZE, the MDI child window will maximize fine but it will have the problem when you try to restore it to previous size. You can minimize the MDI child window with ShowWindow without problem, however. |
WM_MDINEXT | Send this message to the client window to activate the next or the previous MDI child window according to the values in wParam and lParam. |
WM_MDIREFRESHMENU | Send this message to the client window to refresh the menu of the frame window. Note that you must call drawmenubar to update the menu bar after sending this message. |
WM_MDISETMENU | Send this message to the client window to replace the whole menu of the frame window or just the window submenu. You must use this message instead of setmenu. After sending this message, you must call drawmenubar to update the menu bar. Normally you will use this message when the active MDI child window has its own menu and you want it to replace the menu of the frame window while the MDI child window is active. |
I'll review the steps in creating an MDI application for you again below.
.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
The first thing the program does is to register the window classes of the frame window and the MDI child window. After that, it calls CreateWindowEx to create the frame window. Within the WM_CREATE handler of the frame window, we create the client window:
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
It calls GetMenu to obtain the handle to the menu of the
frame window, to be used in the GetSubMenu call. Note
that we pass the value 1 to GetSubMenu because the submenu
we want the window list to appear is the second submenu. Then we
fill the members of the CLIENTCREATESTRUCT structure.
Next, we initialize the MDICLIENTSTRUCT structure. Note
that we don't need to do it here. It's only convenient to do it in
WM_CREATE.
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
After the frame window is created (and also the client window), we call LoadMenu to load the child window menu from the resource. We need to get this menu handle so we can replace the menu of the frame window with it when an MDI child window is present. Don't forget to call DestroyMenu on the handle before the application exits to Windows. Normally Windows will free the menu associated with a window automatically when the application exits but in this case, the child window menu is not associated with any window thus it will still occupy valuable memory even after the application exits.
invoke LoadMenu,hInstance, IDR_CHILDMENU
mov hChildMenu,eax
........
invoke DestroyMenu, hChildMenu
Within the message loop, we call translatemdisysaccel.
.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
If TranslateMDISysAccel returns a non-zero value, it means the message was already handled by Windows itself so you don't need to do anything to the message. If it returns 0, the message is not MDI-related and thus should be handled as usual.
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
Note that within the window procedure of the frame window, we call DefFrameProc to handle the messages we are not interested in.
The bulk of the window procedure is the WM_COMMAND handler. When the user selects "New" from the File menu, we create a new MDI child window.
.ELSEIF ax==IDM_NEW
invoke SendMessage,hwndClient,WM_MDICREATE,0,ADDR mdicreate
In our example, we create the MDI child window by sending WM_MDICREATE to the client window, passing the address of the MDICREATESTRUCT structure in lParam.
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
When the MDI child window is created, it monitors WM_MDIACTIVATE to see if it's the active window. It does this by comparing the value of the lParam which contains the handle of the active child window with its own handle. If they match, it's the active window and the next step is to replace the menu of the frame window to its own. Since the original menu will be replaced, you have to tell Windows again in which submenu the window list should appear. That's why we must call GetSubMenu again to retrieve the handle to the submenu. We send WM_MDISETMENU message to the client window to achieve the desired result. wParam of WM_MDISETMENU contains the handle of the menu you would like to replace the original menu. lParam contains the handle of the submenu you want the window list to appear. Right after sending WM_MDISETMENU, we call DrawMenuBar to refresh the menu else your menu will be a mess.
.ELSE
invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
ret
.ENDIF
Within the window procedure of the MDI child window, you must pass all unhandled messages to DefMDIChildProc instead of DefWindowProc.
.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
When the user selects one of the menuitems in the window submenu, we send the corresponding message to the client window. If the user chooses to tile the windows, we send WM_MDITILE to the client window, specifying in wParam what kind of tiling we want. WM_CASCADE is similar.
.ELSEIF ax==IDM_CLOSE
invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
invoke SendMessage,eax,WM_CLOSE,0,0
If the user chooses "Close" menuitem, we must obtain the handle of the currently active MDI child window first by sending WM_MDIGETACTIVE to the client window. The return value in eax is the handle of the currently active MDI child window. After that, we send WM_CLOSE to that window.
.ELSEIF uMsg==WM_CLOSE
invoke MessageBox,hChild,ADDR ClosePromptMessage, \
ADDR AppName,MB_YESNO
.IF eax==IDYES
invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
.ENDIF
Within the window procedure of the MDI child, when WM_CLOSE is received, it displays a message box asking the user if he really wants to close the window. If the answer is yes, we send WM_MDIDESTROY to the client window. WM_MDIDESTROY closes the MDI child window and restores the title of the frame window.
Tutorial 31: Listview Control | Overview | Tutorial 33: RichEdit Control: Basics |