MDIアプリ

SDIとMDI

今まで作成してきたウィンドウアプリはSDIと呼ばれる方式のものです。
SDIはSingle Document Interfaceの略で、ひとつのアプリケーションウィンドウでひとつのドキュメントを操作するタイプのアプリです。
例えばWindows付属の「メモ帳」はSDIアプリです。
メモ帳で複数のテキストファイルを同時に編集するにはメモ帳を複数同時に立ち上げる必要があります。

これに対してMDIという方式のアプリがあります。
MDIはMultiple Document Interfaceの略で、ひとつのアプリケーションウィンドウ内に複数の子ウィンドウを作成し、複数のドキュメントを同時に編集可能なタイプのアプリです。
Microsoft Excelなどが代表的なMDIアプリです。

SDIアプリでも複数のウィンドウを同時に表示することは可能ですが、それぞれのウィンドウは独立しています。
MDIアプリは親ウィンドウの領域内にすべての子ウィンドウが含められ、はみ出したりすることあはりません。

MDIアプリの例

MDIのウィンドウ構造

MDIアプリは親ウィンドウ内に子ウィンドウを持つことになりますが、プログラム上では三種類のウィンドウが使用されます。

まずトップレベル(最上位)のフレームウィンドウを作成します。
これはタイトルバーや閉じるボタンなどを持つ通常のウィンドウです。

次に、フレームウィンドウのクライアント領域にクライアントウィンドウを作成します。
クライアントウィンドウは子ウィンドウを配置する専用の特殊なウィンドウで、フレームウィンドウを親とします。
このウィンドウの作成にはMDICLIENTというシステム定義済みのウィンドウクラスを使用します。

最後に、クライアントウィンドウを親ウィンドウとする子ウィンドウを作成します。
各ウィンドウで独立したデータ(文書)を扱うのでドキュメントウィンドウとも言います。
子ウィンドウは必要なだけいくつでも作成することが出来ます。
子ウィンドウ用のウィンドウクラスはあらかじめシステムに登録する必要があります。
「クライアントウィンドウ」の「子ウィンドウ」であって、トップレベルウィンドウから見れば孫ウィンドウに当たります。
(「フレームウィンドウ」→「クライアントウィンドウ」→「子ウィンドウ」という関係)

まずはごく初歩的なMDIアプリを作成してみます。
随所にサンプルコードを掲載してるのでページが長くなっていますが、それほど複雑ではありません。

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

フレームウィンドウはアプリケーションのトップレベルウィンドウです。
SDIアプリのウィンドウと基本は同じですが、ウィンドウプロシージャの記述方法が少し異なります。


//ウィンドウクラスの登録
BOOL MyRegisterClass(HINSTANCE hInstance)
{
	//フレームウィンドウのウィンドウクラス
	WNDCLASSEX wcex;
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = FrameWndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1); //背景色
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szWindowClass;
	wcex.hIconSm = NULL;

	if (!RegisterClassEx(&wcex))
		return FALSE;

	return TRUE;
}

//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HWND hClientWnd;
	
	switch (message)
	{
	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		return 0;
	}
	return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}

ウィンドウプロシージャはフレームウィンドウ用と後に作成する子ウィンドウ用とが必要になるので、分かりやすいように名前を「FrameWndProc」に変更します。
背景色を「COLOR_WINDOW + 1」から「COLOR_APPWORKSPACE + 1」に変更し、濃いグレーにしています。
(実際の色はユーザーの設定により異なる)

最も重要な点は、デフォルトウィンドウプロシージャをDefWindowProc関数からDefFrameProc関数に変更していることです。
これはフレームウィンドウの決まりです。

LRESULT DefFrameProcW(
 HWND hWnd,
 HWND hWndMDIClient,
 UINT uMsg,
 WPARAM wParam,
 LPARAM lParam
);
MDIフレームウィンドウのデフォルトの動作を提供するウィンドウプロシージャ。

DefFrameProc関数との違いは、第二引数にhWndMDIClientの指定が増えている点です。
これは次に作成するクライアントウィンドウのハンドルを指定します。
その他の引数はDefFrameProc関数と同じです。

今の時点では変数hClientWndは空なのでこのコードは実行できません。

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

クライアントウィンドウはウィンドウプロシージャのWM_CREATEメッセージ内で、CreateWindow関数またはCreateWindowEx関数で作成します。


//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HWND hClientWnd; 
	CLIENTCREATESTRUCT ccs;

	switch (message)
	{
	case WM_CREATE: //ウィンドウの作成
		ccs.hWindowMenu = NULL;
		ccs.idFirstChild = 100;

		//クライアントウィンドウの作成
		hClientWnd = CreateWindow(L"MDICLIENT", NULL,
			WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL,
			0, 0, 0, 0, hWnd, (HMENU)1, hInst, &ccs);
		);
		return 0;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		return 0;
	}
	return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}

ウィンドウクラスにはMDICLIENTという文字列を指定します。
これはシステム定義のウィンドウクラスです。
(ボタンコントロールの作成にBUTTONクラスを使用するのと同じ)

ウィンドウスタイルはWS_CHILDWS_CLIPCHILDRENが必要です。
WS_CLIPCHILDRENは、子ウィンドウが配置されている領域は描画しないという指定です。
(子ウィンドウは独自に描画をするため、裏に隠れる領域は描画する必要がない)
WS_VISIBLEは必須ではありませんが、通常は同時に指定しておきます。

クライアントウィンドウの位置とサイズはすべて0を指定します。
親ウィンドウには引数hWnd(フレームウィンドウのウィンドウハンドルが格納されている)を指定します。
ウィンドウIDには1を指定します。
(何でも良い)

CLIENTCREATESTRUCT構造体

引数の最後はCLIENTCREATESTRUCT構造体のポインタを渡します。

typedef struct tagCLIENTCREATESTRUCT {
 HANDLE hWindowMenu;
 UINT idFirstChild;
} CLIENTCREATESTRUCT, *LPCLIENTCREATESTRUCT;
MDIクライアントウィンドウ作成時に使用される情報を格納する構造体。

hWindowMenuメンバはフレームウィンドウのウィンドウメニューハンドルを指定します。
今はメニューを作成していないためNULLを指定していますが、MDIアプリはメニューの作成はほぼ必須です。
ここは次ページで変更することにします。

idFirstChildメンバは子ウィンドウのID(識別子)を指定します。
ここで指定した数値が最初の子ウィンドウのIDとして使用され、子ウィンドウが増えるたびに1ずつ加算されたIDが使用されます。
このIDは他のコマンドIDやメニューIDと重複しないようにする必要があります。


最後に、戻り値をstatic変数に格納します。
これは先ほど説明したDefFrameProc関数で使用します。

子ウィンドウの作成

子ウィンドウは、作成する前に子ウィンドウ用のウィンドウクラスを作成して登録する必要があります。


//グローバル変数
HINSTANCE hInst;	//インスタンス
WCHAR szTitle[] = L"テストアプリ";		//タイトル
WCHAR szWindowClass[] = L"MyTestApp";	//ウィンドウクラス名
WCHAR szWindowClass_MDIChild[] = L"MyTestApp_MDIChild";	//MDI子ウィンドウクラス名

//ウィンドウクラスの登録
BOOL MyRegisterClass(HINSTANCE hInstance)
{
	//フレームウィンドウのウィンドウクラス
	WNDCLASSEX wcex;
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_HREDRAW | CS_VREDRAW;
	//wcex.lpfnWndProc = DefWindowProc;
	wcex.lpfnWndProc = FrameWndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szWindowClass;
	wcex.hIconSm = NULL;

	if (!RegisterClassEx(&wcex))
		return FALSE;

	//子ウィンドウのウィンドウクラス
	wcex.lpfnWndProc = (WNDPROC)DefMDIChildProc;
	wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szWindowClass_MDIChild;

	if (!RegisterClassEx(&wcex))
		return FALSE;

	return TRUE;
}

MyRegisterClass関数に子ウィンドウのウィンドウクラスの登録処理を追加しています。
ウィンドウプロシージャにはDefMDIChildProc関数を指定しています。
これはMDI子ウィンドウのデフォルトの動作を提供する関数です。
実際には独自定義した子ウィンドウ用のウィンドウプロシージャをここに指定します。

複数の種類の子ウィンドウを作成する場合はウィンドウクラスも複数登録する必要があります。

CreateMDIWindow関数

子ウィンドウの作成方法は二通りあります。
ひとつはCreateMDIWindow関数を使用する方法です。

HWND CreateMDIWindowW(
 LPCWSTR lpClassName,
 LPCWSTR lpWindowName,
 DWORD dwStyle,
 int X,
 int Y,
 int nWidth,
 int nHeight,
 HWND hWndParent,
 HINSTANCE hInstance,
 LPARAM lParam
);
MDI子ウィンドウを作成する。
戻り値は作成したウィンドウのハンドル。
失敗した場合はNULLを返す。

引数が多いですが、基本的にCreateWindow関数と同じと考えて大丈夫です。
メニューハンドルの指定(子ウィンドウIDの指定)がありませんが、MDI子ウィンドウのIDはクライアントウィンドウ作成時に使用したCLIENTCREATESTRUCT構造体のidFirstChildメンバから自動的に生成されます。


//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HWND hClientWnd; 
	CLIENTCREATESTRUCT ccs;

	switch (message)
	{
	case WM_CREATE:
		ccs.hWindowMenu = NULL;
		ccs.idFirstChild = 100;

		//クライアントウィンドウの作成
		hClientWnd = CreateWindow(L"MDICLIENT", NULL,
			WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE,
			0, 0, 0, 0, hWnd, (HMENU)1, hInst, &ccs);

		//子ウィンドウの作成
		CreateMDIWindow(szWindowClass_MDIChild, L"MDI子ウィンドウ", 0,
			CW_USEDEFAULT, CW_USEDEFAULT,
			300, 200,
			hClientWnd, hInst, 0
		);
		return 0;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		return 0;
	}
	return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}

今回はクライアントウィンドウ作成の直後に子ウィンドウを作成しています。
ウィンドウクラス名は先ほどシステムに登録したものを指定します。
親ウィンドウはクライアントウィンドウ(hClientWnd)を指定します。

ここまでのコードでMDIアプリのごく基本的な部分が完成します。
一通りのコードを掲載します。


#include <windows.h>

//グローバル変数
HINSTANCE hInst;	//インスタンス
WCHAR szTitle[] = L"テストアプリ";		//タイトル
WCHAR szWindowClass[] = L"MyTestApp";	//ウィンドウクラス名
WCHAR szWindowClass_MDIChild[] = L"MyTestApp_MDIChild";	//MDI子ウィンドウクラス名

//関数プロトタイプ宣言
BOOL MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);

//ウィンドウプロシージャのプロトタイプ宣言
LRESULT CALLBACK FrameWndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	MyRegisterClass(hInstance);
	if (!InitInstance(hInstance, nCmdShow))
	{
		return 0;
	}

	MSG msg;
	BOOL ret;

	//メッセージループ
	while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (ret == -1)
		{
			return -1;
		}

		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

//ウィンドウクラスの登録
BOOL MyRegisterClass(HINSTANCE hInstance)
{
	//フレームウィンドウのウィンドウクラス
	WNDCLASSEX wcex;
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = FrameWndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szWindowClass;
	wcex.hIconSm = NULL;

	if (!RegisterClassEx(&wcex))
		return FALSE;

	//子ウィンドウのウィンドウクラス
	wcex.lpfnWndProc = (WNDPROC)DefMDIChildProc;
	wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szWindowClass_MDIChild;

	if (!RegisterClassEx(&wcex))
		return FALSE;

	return TRUE;
}

//ウィンドウの作成
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	hInst = hInstance;

	HWND hWnd = CreateWindow(
		szWindowClass, szTitle,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL,
		NULL,
		hInstance,
		NULL
	);

	if (!hWnd) {
		return FALSE;
	}
	ShowWindow(hWnd, nCmdShow);
	return TRUE;
}

//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HWND hClientWnd; 
	CLIENTCREATESTRUCT ccs;

	switch (message)
	{
	case WM_CREATE:
		ccs.hWindowMenu = NULL;
		ccs.idFirstChild = 100;

		//クライアントウィンドウの作成
		hClientWnd = CreateWindow(L"MDICLIENT", NULL,
			WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL,
			0, 0, 0, 0, hWnd, (HMENU)1, hInst, &ccs);

		//子ウィンドウの作成
		CreateMDIWindow(szWindowClass_MDIChild, L"MDI子ウィンドウ", 0,
			CW_USEDEFAULT, CW_USEDEFAULT,
			300, 200,
			hClientWnd, hInst, 0
		);
		return 0;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		return 0;
	}
	return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}

見た目はMDIアプリになっていますが、まだ不十分です。
特に、子ウィンドウを最大化すると元に戻せなくなるという致命的な欠陥があります。
サンプルコードの実行結果

WM_MDICREATEメッセージ

MDI子ウィンドウは、クライアントウィンドウにWM_MDICREATEメッセージを送信することでも作成できます。
このメッセージではMDICREATESTRUCT構造体を使用します。

typedef struct tagMDICREATESTRUCTW {
 LPCWSTR szClass;
 LPCWSTR szTitle;
 HANDLE hOwner;
 int x;
 int y;
 int cx;
 int cy;
 DWORD style;
 LPARAM lParam;
} MDICREATESTRUCTW, *LPMDICREATESTRUCTW;
MDI子ウィンドウに関する情報を格納する構造体。

メンバは親ウィンドウの指定が無い以外はCreateMDIWindow関数と同じです。
親ウィンドウはWM_MDICREATEメッセージを送信したウィンドウ(つまりクライアントウィンドウ)に設定されます。

メッセージはSendMessage関数で送信します。
送信先はクライアントウィンドウを、メッセージの種類にはWM_MDICREATEを指定します。
WPARAMは使用しません。
LPARAMには必要な情報を格納したMDICREATESTRUCT構造体のポインタを指定します。


//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HWND hClientWnd; 
	CLIENTCREATESTRUCT ccs;
	MDICREATESTRUCT mcs;

	switch (message)
	{
	case WM_CREATE:
		ccs.hWindowMenu = NULL;
		ccs.idFirstChild = 100;

		//クライアントウィンドウの作成
		hClientWnd = CreateWindow(L"MDICLIENT", NULL,
			WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL,
			0, 0, 0, 0, hWnd, (HMENU)1, hInst, &ccs);

		//子ウィンドウの作成
		//CreateMDIWindow(szWindowClass_MDIChild, L"MDI子ウィンドウ", 0,
		//	CW_USEDEFAULT, CW_USEDEFAULT,
		//	300, 200,
		//	hClientWnd, hInst, 0
		//);

		//子ウィンドウの作成
		mcs.szClass = szWindowClass_MDIChild;
		mcs.szTitle = L"MDI子ウィンドウ";
		mcs.style = 0;
		mcs.x = CW_USEDEFAULT;
		mcs.y = CW_USEDEFAULT;
		mcs.cx = 300;
		mcs.cy = 200;
		mcs.hOwner = hInst;
		mcs.lParam = 0;
		SendMessage(hClientWnd, WM_MDICREATE, 0, (LPARAM)&mcs);
		return 0;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		return 0;
	}
	return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}

実行結果はCreateMDIWindow関数の時と同じです。

子ウィンドウのスタイル

子ウィンドウのスタイルには以下の定数の組み合わせが指定できます。

定数 説明
WS_MINIMIZE 子ウィンドウを最小化された状態で作成する
WS_MAXIMIZE 子ウィンドウを最大化された状態で作成する
WS_HSCROLL 水平スクロールバーを表示する
WS_VSCROLL 垂直スクロールバーを表示する

また、クライアントウィンドウのスタイルにMDIS_ALLCHILDSTYLESを指定すると、CreateWindow関数で使用できるウィンドウスタイルをMDI子ウィンドウでも使用できるようになります。