MDI


複数の作業ウィンドウ

これまで、子ウィンドウといえばボタンやリストボックスのような
メインウィンドウに付属させ、ユーザーへの入力や出力を補助するものがほとんどでした

しかし、この他に一つのアプリケーションが同時に複数のドキュメントを扱えるように
Windows は MDI(Multiple Document Interface) をサポートしています
これは、可変に作成できるメインウィンドウのような作業領域を持つ子ウィンドウです

MDI ウィンドウ

Microsoft Excel は MDI を最初に導入したアプリケーションです
当時の Windows では面倒な計算や設定が多かったようですが
現在では、多くのビジネスアプリケーションがこれを実装するようになりました

上の図を見て分かるように、MDI 子ウィンドウはメインウィンドウのように振舞うことができます
しかし、メニューバーは存在せず、最大化、最小化はメインウィンドウの範囲で行われます

例えば、ツール関連のアプリケーションであれば、まず間違いなく MDI を導入するべきです
画像処理ソフトは同時に複数の画像を編集できて良いはずですし
申告書の作成ソフト、音楽作成ソフトも同様に、同時に複数のファイルを編集し
必要であればそれらをクリップボードを介して編集しているデータを通信させられるべきです

MDI を実現するには、MDI クライアントウィンドウというものを最初に作る必要があります
MDI アプリケーションにおいて、最初のメインウィンドウのことをフレームウィンドウと呼び
クライアント領域だった場所に、子ウィンドウを制御するためにクライアントウィンドウを作ります
MDI の子ウィンドウは、クライアントウィンドウの子として生成されます

フレームウィンドウ -クライアントウィンドウ -子ウィンドウ1
-子ウィンドウ2
-子ウィンドウ3

クライアントウィンドウを作るには CreateWindow() 関数 (または CreateWindowEx() 関数)で
システムに登録されている "MDICLIENT" ウィンドウをクラスを指定して作ります
ただし、本来追加情報となっていた最後の引数には
必ず CLIENTCREATESTRUCT 構造体へのポインタを指定します
typedef struct tagCLIENTCREATESTRUCT { // ccs 
    HANDLE hWindowMenu; 
    UINT   idFirstChild; 
} CLIENTCREATESTRUCT;
hWindowMenu には、MDI アプリケーションのメニューのハンドルを
idFirstChild には最初の MDI 子ウィンドウの識別子を指定します
ここで指定する ID は、どのメニュー ID とも重複しない様にしましょう
また、クライアントウィンドウの位置とサイズは、全て 0 を指定します

次に、MDI を実装するメインウィンドウ、すなわちフレームウィンドウの注意点ですが
背景色はこれまでのように直接ブラシを指定してもかまいませんが
通常は、WNDCLASS 用に定義されている COLOR_APPWORKSPACE + 1 を指定します
これを指定すれば、ウィンドウの再描画の時にちらつく現象を防げます

また、メッセージのデフォルト処理には DefFrameProc() を使います
MDI フレームウィンドウのデフォルト処理専用の関数です
LRESULT DefFrameProc(
	HWND hWnd , HWND hWndMDIClient ,
	UINT uMsg ,
	WPARAM wParam , LPARAM lParam
);
DefWindowProc() 関数と比べ、引数が一つ増えています
hWnd には、MDI フレームウィンドウのハンドルを
hWndMIDClient には MDI クライアントウィンドウのハンドルを指定します
uMsg にはメッセージを、wParam と lParam にはそれぞれ追加情報を指定します
戻り値の意味は、メッセージの処理によって異なります
#include <windows.h>

#define TITLE TEXT("Kitty on your lap")
#define ID_CHILDWND 0x100
HINSTANCE hIns;

LRESULT CALLBACK FrameProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;
	CLIENTCREATESTRUCT ccsClient;
	static HWND hClientWindow;

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		ccsClient.hWindowMenu = NULL;
		ccsClient.idFirstChild = 50000;
		hClientWindow = CreateWindow(TEXT("MDICLIENT") , NULL ,
			WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE ,
			0 , 0 , 0 , 0 , hWnd , (HMENU)1 , hIns , &ccsClient
		);
		return 0;
	}
	return DefFrameProc(hWnd , hClientWindow , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;

	hIns = hInstance;

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= FrameProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)(COLOR_APPWORKSPACE + 1);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			TEXT("KITTY") , TITLE ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}


さて、これでフレームウィンドウとクライアントウィンドウを作成できました
本来ならば、これにメニューがあるべきなのですが、今は省略します

さて、次に主役となる MDI 子ウィンドウを作成しましょう
MDI 子ウィンドウの作成には CreateWindow() や CreateWindowEx() は使えません
これを作成する方法は2つありますが、順に紹介しましょう

方法の一つは CreateMDIWindow() 関数を使います
作成方法が CreateWindow() 等と似ていて解りやすいので、筆者はこの方法が好きです
HWND CreateMDIWindow(
	LPTSTR lpClassName , LPTSTR lpWindowName , DWORD dwStyle ,
	int X , int Y , int nWidth , int nHeight ,
	HWND hWndParent , HINSTANCE hInstance , LPARAM lParam
);
lpClassName は、MDI ウィンドウクラス名を指定します
lpWindowName はタイトルバーに表示されるウィンドウの名前を指定します

dwStyle はウィンドウのスタイルを指定します
MDI ウィンドウの場合、通常は 0 または以下の定数を組み合わせて指定します
ただし、MDI クライアントウィンドウに MDIS_ALLCHILDSTYLES スタイルを指定していれば
CreateWindow() 関数のスタイルで用いることができるウィンドウスタイルを使えます

定数解説
WS_MINIMIZE 最小化された状態のMDI子ウィンドウを作成します
WS_MAXIMIZE 最大化された状態のMDI子ウインドウを作成します
WS_HSCROLL 水平スクロールバーを持つMDI子ウインドウを作成します
WS_VSCROLL 垂直スクロールバーを持つMDI子ウインドウを作成します

X と Y にはそれぞれ MDI 子ウィンドウの X 座標と Y 座標をクライアント座標で、
nWidth と nHeight には幅と高さをそれぞれ指定します
これらも CW_USEDEFAULT を指定すればデフォルトの値になります

hWndParent にはクライアントウィンドウのハンドルを指定します
hInstance は MDI 子ウィンドウを作成するアプリケーションのハンドルを指定します
lParam には、アプリケーション定義の値を指定します
関数が成功すると作成されたウィンドウのハンドルが、失敗すれば NULL が返ります

MDI の子ウィンドウをこの関数で作成するには
その子ウィンドウ専用のウィンドウクラスをあらかじめ作成する必要があります
ウィンドウクラスの作り方は、これまでのメインウィンドウを作る時と同じでかまいませんが
ウィンドウプロシージャは、デフォルトで DefMDIChildProc() を使います
LRESULT DefMDIChildProc(
	HWND hWnd , UINT uMsg ,
	WPARAM wParam , LPARAM lParam
);
hWnd にはウィンドウのハンドル、uMsg にはメッセージ
wParam と lParam にはそれぞれメッセージの追加情報を指定します
戻り値は、メッセージによって異なります
渡されたメッセージで、興味のないものはこの関数に渡して処理を委託します
本来使う DefWindowProc() は MDI 子ウィンドウで使ってはいけません
#include <windows.h>

#define TITLE TEXT("Kitty on your lap")
#define MDI_FRAME TEXT("FRAMEWINDOW")
#define MDI_CHILD TEXT("MDICHILD")

#define ID_CHILDWND 0x100

HINSTANCE hIns;

LRESULT CALLBACK FrameProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;
	CLIENTCREATESTRUCT ccsClient;
	static HWND hClientWindow;

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		ccsClient.hWindowMenu = NULL;
		ccsClient.idFirstChild = ID_CHILDWND;

		hClientWindow = CreateWindow(TEXT("MDICLIENT") , NULL ,
			WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE ,
			0 , 0 , 0 , 0 , hWnd , (HMENU)1 , hIns , &ccsClient
		);
		CreateMDIWindow(MDI_CHILD , TITLE , 0 ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			hClientWindow , hIns , 0
		);			
		return 0;
	}
	return DefFrameProc(hWnd , hClientWindow , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;

	hIns = hInstance;

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= FrameProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)(COLOR_APPWORKSPACE + 1);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= MDI_FRAME;

	if (!RegisterClass(&winc)) return 1;

	winc.lpfnWndProc	= DefMDIChildProc;
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszClassName	= MDI_CHILD;

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			MDI_FRAME , TITLE ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}


このプログラムでは、クライアントウィンドウに 1 つの MDI 子ウィンドウが生成されます
この子ウィンドウは最小化するとクライアントウィンドウの左下に配置されます
このプログラムでは、まだメニューを作っていないので最大化すると元に戻せなくなります

MDI 子ウィンドウを作成するもう一つの手段は、メッセージを使います
MDI クライアントウィンドウに WM_MDICREATE メッセージを送ると
上のプログラムのように、情報を元にクライアントウィンドウが子ウィンドウを生成します

WM_MDICREATE は LPARAM に MDICREATESTRUCT 構造体のポインタを指定します
クライアントウィンドウはこの構造体の情報から MDI 子ウィンドウを生成します
MDICREATESTRUCT 構造体は次のように定義されています
typedef struct tagMDICREATESTRUCT { // mdic 
    LPCTSTR szClass; 
    LPCTSTR szTitle; 
    HANDLE  hOwner; 
    int     x; 
    int     y; 
    int     cx; 
    int     cy; 
    DWORD   style; 
    LPARAM  lParam; 
} MDICREATESTRUCT;
szClass は MDI 子ウィンドウが使用するウィンドウクラスの名前を表す文字列へのポインタ
szTitle は MDI 子ウィンドウのタイトルをあらわす文字列へのポインタを表します
hOwner は MDI 子ウィンドウを作成するアプリケーションのハンドルを表します

x、y は子ウィンドウの初期の X 及び Y 座標を
cs と cy は、それぞれ幅と高さを表しています
style には MDI 子ウィンドウのスタイルを格納します
このメンバの条件は CreateMDIWindow() の dwStyle と同様です
lParam はアプリケーション定義の追加情報を表します

WPARAM は存在しないので 0 を指定します
このメッセージを送信すると、生成した子ウィンドウのハンドルが、失敗すると NULL が返ります
#include <windows.h>

#define TITLE TEXT("Kitty on your lap")
#define MDI_FRAME TEXT("FRAMEWINDOW")
#define MDI_CHILD TEXT("MDICHILD")

#define ID_CHILDWND 0x100

HINSTANCE hIns;

LRESULT CALLBACK FrameProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;
	CLIENTCREATESTRUCT ccsClient;
	MDICREATESTRUCT mdic;
	static HWND hClientWindow;

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		ccsClient.hWindowMenu = NULL;
		ccsClient.idFirstChild = ID_CHILDWND;

		hClientWindow = CreateWindow(TEXT("MDICLIENT") , NULL ,
			WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE ,
			0 , 0 , 0 , 0 , hWnd , (HMENU)1 , hIns , &ccsClient
		);

		mdic.szClass = MDI_CHILD;
		mdic.szTitle = TITLE;
		mdic.x = mdic.y = mdic.cx = mdic.cy = CW_USEDEFAULT;
		mdic.style = mdic.lParam = 0;
		SendMessage(hClientWindow ,
			WM_MDICREATE , 0 , (LPARAM)&mdic);		
		return 0;
	}
	return DefFrameProc(hWnd , hClientWindow , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;

	hIns = hInstance;

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= FrameProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)(COLOR_APPWORKSPACE + 1);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= MDI_FRAME;

	if (!RegisterClass(&winc)) return 1;

	winc.lpfnWndProc	= DefMDIChildProc;
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszClassName	= MDI_CHILD;

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			MDI_FRAME , TITLE ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}
プログラムの結果は、先ほどのものとまったく同じですが
メッセージによって MDI 子ウィンドウを生成している点で異なっています


DefFrameProc()

LRESULT DefFrameProc(
	HWND hWnd , HWND hWndMDIClient ,
	UINT uMsg ,
	WPARAM wParam , LPARAM lParam
);
MDI フレームウィンドウのデフォルトのメッセージ処理を行います

hWnd - MDI フレームウィンドウのハンドルを指定しますhWndMIDClient - MDI クライアントウィンドウのハンドルを指定します
uMsg - メッセージを指定します
wParam - メッセージの追加情報を指定します
lParam - メッセージの追加情報を指定します

戻り値 - 意味はメッセージの処理によって異なります

CreateMDIWindow()

HWND CreateMDIWindow(
	LPTSTR lpClassName , LPTSTR lpWindowName , DWORD dwStyle ,
	int X , int Y , int nWidth , int nHeight ,
	HWND hWndParent , HINSTANCE hInstance , LPARAM lParam
);
MDI 子ウィンドウを生成します

lpClassName - MDI ウィンドウクラス名を指定します
lpWindowName - タイトルバーに表示されるウィンドウの名前を指定します
dwStyle - ウィンドウのスタイルを指定します
X - 子ウィンドウの初期位置 X 座標をクライアント座標で指定します
Y - 子ウィンドウの初期位置 Y 座標をクライアント座標で指定します
nWidth - 子ウィンドウの幅を指定します
nHeight - 子ウィンドウの高さを指定します
hWndParent - クライアントウィンドウのハンドルを指定します
hInstance - MDI 子ウィンドウを作成するアプリケーションのハンドルを指定します
lParam - アプリケーション定義の値を指定します

戻り値 - 作成されたウィンドウのハンドル、失敗すれば NULL

dwStyle には 0 または以下の定数を組み合わせて指定します
ただし、クライアントウィンドウに MDIS_ALLCHILDSTYLE スタイルを指定している場合は
CreateWindow() 関数のスタイルで用いる定数を指定します

定数解説
WS_MINIMIZE 最小化された状態のMDI子ウィンドウを作成します
WS_MAXIMIZE 最大化された状態のMDI子ウインドウを作成します
WS_HSCROLL 水平スクロールバーを持つMDI子ウインドウを作成します
WS_VSCROLL 垂直スクロールバーを持つMDI子ウインドウを作成します

DefMDIChildProc()

LRESULT DefMDIChildProc(
	HWND hWnd , UINT uMsg ,
	WPARAM wParam , LPARAM lParam
);
MDI 子ウィンドウのデフォルトメッセージ処理を行います

hWnd - MDI 子ウィンドウのハンドルを指定します
uMsg - メッセージを指定します
wParam - メッセージの追加情報を指定します
lParam - メッセージの追加情報を指定します

戻り値 - 意味はメッセージの処理によって異なります



前のページへ戻る次のページへ