Tutorial 32: Multiple Document Interface (MDI)

MDIアプリケーションの作成方法を紹介しよう。それ程難しくないので安心してください。
 メインソース   ヘッダファイル   リソース   実行結果 

Theory:

複数ドキュメントインターフェース(MDI)は同時に複数のドキュメントを扱えるアプリケーションのことだ。使い慣れているアプリケーションの1つにNotepadの名前が挙がると思うが、あれはシングルドキュメントインターフェース(SDI)アプリケーションだ。
なのでNotepadは1つのドキュメントしか扱えないことになり、他のドキュメントを編集したければ、今開いているドキュメントを閉じてから編集したいドキュメントをオープンしなければならない。

想像しただけで扱いづらいものだ。対して、MS-Wordは一度に複数のドキュメントを編集でき、MDIアプリケーションの一つである。

MDIアプリケーションは以下のような特徴がある。

メインウィンドウはフレームウィンドウと呼ばれており、そのクライアントエリアには子ウィンドウが表示される。
このフレームウィンドウの仕事量は、子ウィンドウの連携をとらなければいけない分、少しだけ通常のウィンドウより多くなる。

クライアントエリアに子ウィンドウを好きなだけ生成するためには、クライアントウィンドウと呼ばれる特別なウィンドウが必要となる。このクライアントウィンドウは、フレームウィンドウの全クライアントエリアを覆う見えないウィンドウと考えることができる。


そして、本当のところは、このクライアントウィンドウがMDI子ウィンドウの親となる。



図1.MDIアプリケーションの階層

   
フレームウィンドウ
   
   
|
   
   
クライアントウィンドウ
   
   
|
   

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

●フレームウィンドウの作成

では詳細な説明に移ろう。まず始めにフレームウィンドウを作成する必要がある。 作り方は通常のウィンドウとほとんど同じで、CreateWindowEx関数をCALLすればよいが、2つの大きな違いがある。

1つ目の違いは、作成したウィンドウで処理したくないメッセージが送信された際には、DefWindowProcではなくDefFrameProcをCALLしなければならないことだ。 もしDefFrameProc関数を使用するのを忘れたら、そのアプリケーションはMDIではなくなってしまう。DefFrameProcは以下のようになっている。

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

DefFrameProcDefWindowProcを比較すると、引数が違うことに気付くだろう。DefFrameProcは5つでDefWindowProcは4つである。 1つ多い引数は、クライアントウィンドウのハンドルである。このハンドルはクライアントウィンドウへMDI関連のメッセージを送信するために必要である。

2つ目の違いは、フレームウィンドウのメッセージループでTranslateMDISysAccel関数をCALLしなければならないことである。 これは、Ctrl+F4 とか、Ctrl+Tab といった、MDIに関するアクセラレータキーを有効にしたい場合に必要である。これは、以下のようになっている。

TranslateMDISysAccel proc hwndClient:DWORD, lpMsg:DWORD

一つ目のパラメータはクライアントウィンドウを処理するもので、 全てのMDIチャイルドウィンドウの親ウィンドウはクライアントウィンドウになっているため何も驚くようなことでは無い。 2つ目のパラメータは、GetMessage関数をCALLすることにより値をセットしたMSG構造体のアドレスである。 クライアントウィンドウへMSG構造体を渡すのがその目的で、その結果クライアントウィンドウはMSG構造体にMDIに関するキーボードメッセージが含まれているかどうかを調査できる。 もし含まれていれば、そのメッセージをクライアントウィンドウ自身で処理し、非0の値を返す。含まれていなければ、FALSEを返す。

フレームウィンドウの作り方は、以下のようになる。

  1. いつものようにWNDCLASSEX構造体を初期化する
  2. RegisterClassEx関数によりフレームウィンドウクラスを登録する
  3. CreateWindowEx関数によりフレームウィンドウを作成する
  4. メッセージループ内でTranslateMDISysAccel関数をCALLする
  5. ウィンドウプロシージャ内で処理しないメッセージはDefFrameProc関数に引き渡す

●クライアントウィンドウの作成

今度はクライアントウィンドウを作成する。クライアントウィンドウクラスはWindowsにより事前に登録されている。クラス名は「MDICLIENT」である。 ウィンドウを作成する際、CreateWindowEx関数に、CLIENTCREATESTRUCT構造体のアドレスを指定する必要がある。この構造体は以下のように定義されている。

CLIENTCREATESTRUCT struct hWindowMenu dd ? idFirstChild dd ? CLIENTCREATESTRUCT ends

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通りある。

これを見て、不思議に思うかもしれない。一体どちらを使えばいいのか?2つの違いは何なのだろうか? と。それに対する回答は以下の通りだ。

WM_MDICREATEメッセージを送信する方法は、CALLするコードが属するスレッドと同じスレッドにしか子ウィンドウを作成できない。例えば、2つのスレッドがあり、片方のスレッドでフレームウィンドウを作成し、もう片方のスレッドで子ウィンドウを作成したければ、CreateMDIChild関数をCALLする方法を用いなければならない。最初のスレッドにWM_MDICREATEメッセージを送信しても思うように動作しないからだ。
シングルスレッドであればどちらの方法を使用しても構わない。

子ウィンドウのウィンドウプロシージャに関してもう少し詳しく説明しておこう。フレームウィンドウでは、処理しないメッセージをDefWindowProc関数をCALLに任せていたが、子ウィンドウではDefMDIChildProc関数を使用しなければならない。パラメータはDefWindowProcと同じである。

WM_MDICREATEのほかに、MDI関連のウィンドウメッセージを紹介しよう。

WM_MDIACTIVATE このメッセージは、子ウィンドウをアクティブにするため、アプリケーションからクライアントウィンドウに送信される。クライアントウィンドウがこのメッセージを受け取ると、指定された子ウィンドウをアクティブにし、子ウィンドウにWM_MDIACTIVATEを送信する。
このメッセージは、アプリケーションが子ウィンドウをアクティブにさせるのに使用する場合と、自分がアクティブか非アクティブかを判断するため、子ウィンドウ自身から送信される場合がある。例えば、子ウィンドウがそれぞれ違うメニューを持っていた場合、フレームウィンドウのメニューを変更するために、このメッセージを使用できる。
WM_MDICASCADE
WM_MDITILE
WM_MDIICONARRANGE
これらのメッセージは子ウィンドウを整列させるのに使用する。例えば、子ウィンドウを並べて表示させたい場合は、クライアントウィンドウにWM_MDICASCADEメッセージを送信する。
WM_MDIDESTROY 子ウィンドウを終了させる時はクライアントウィンドウにこのメッセージを送信する。DestroyWindow関数をCALLする代わりにこのメッセージを使用すべきで、なぜなら、子ウィンドウが最大化されていたら、フレームウィンドウのタイトルがそのままとなってしまうからだ。
WM_MDIGETACTIVE 現在アクティブになっている子ウィンドウのハンドルを取得する
WM_MDIMAXIMIZE
WM_MDIRESTORE
WM_MDIMAXIMIZEは子ウィンドウを最大化するため、WM_MDIRESTOREは直前の状態に戻すために使用する。これらの動作は常にこのメッセージを使用することになっている。もし、SW_MAXIMIZEを用いてShowWindow関数をCALLすると、子ウィンドウは確かに最大化されるのだが、元のサイズに戻そうとしたときに問題が発生する。ただし、最小化する分には、ShowWindow関数を使用しても問題ない。
WM_MDINEXT wParam、lParamの値を操作して、次の子ウィンドウ、もしくは直前の子ウィンドウをアクティブにする。
WM_MDIREFRESHMENU フレームウィンドウのメニューをリフレッシュする。このメッセージを送信した後に、メニューバーを更新するためDrawMenuBar関数をCALLしなければならない。
WM_MDISETMENU フレームウィンドウのメニュー、もしくはサブメニューをリプレイスする。SetMenuの代わりに使用しなければならない。このメッセージを送信後、DrawMenuBar関数をCALLし、メニューバーを更新する。通常、アクティブな子ウィンドウが自身のメニューを持っており、アクティブな間だけ、そのメニューを使用したい場合に、このメッセージを使用する。

では、MDIアプリケーションの作成方法をまとめてみよう。

  1. フレームウィンドウと子ウィンドウのウィンドウクラスを登録する
  2. CreateWindowEx関数でフレームウィンドウを作成する
  3. MDIに関連するアクセラレータキーを処理するため、メッセージループ内でTranslateMDISysAccel関数をCALLする
  4. フレームウィンドウのウィンドウプロシージャで処理しないメッセージはDefFrameProc関数をCALLする
  5. 既に登録されているウィンドウクラス名"MDICLIENT"を、lParamにCLIENTCREATESTRUCT構造体のアドレスをそれぞれ指定して、CreateWindowEx関数をCALLしてクライアントウィンドウを作成する。通常、フレームウィンドウのウィンドウプロシージャのWM_CREATEを処理するブロック内でクライアントウィンドウを作成する
  6. クライアントウィンドウにWM_MDICREATEを送信するか、もしくはCreateMDIWindow関数をCALLして子ウィンドウを作成する
  7. 子ウィンドウのウィンドウプロシージャ内で処理しないメッセージはDefMDIChildProc関数をCALLする
  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関数をCALLしてフレームウィンドウを作成する。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関数をCALLする際必要となるフレームウィンドウのメニューハンドルを取得するためGetMenu関数をCALLしている。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関数をCALLしてリソースファイルから子ウィンドウのメニューをロードする。 このメニューはは子ウィンドウが表示されればフレームウィンドウのメニューと取り替えるために必要となる。そして、アプリケーションが終了する前に必ずDestroyMenu関数をCALLしなければならない。 通常、Windowsはウィンドウに関連するメニューを自動的に解放するのだが、この場合、子ウィンドウメニューはどのウィンドウにも関連していないので解放できないのである。 なので、アプリケーションが終了した後もメモリ上に残ることになる。

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

メッセージループ内でTranlateMDISysAccel関数をCALLする

.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関数をCALLしてサブメニューのハンドルを取得しなければならない。
そして、wParamには交換したいメニューのハンドルを指定し、lParamにはウィンドウリストを表示したいサブメニューのハンドルを指定して、WM_MDISETMENUメッセージを送信する。 WM_MDISETMENUメッセージの送信直後、DrawMenuBar関数をCALLしてメニューの更新を行っている。

.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は子ウィンドウを閉じ、フレームウィンドウのタイトルを元に戻す。



[戻る]