概要

MDI(Multiple Document Interface)は下図の様な1つのアプリケーンの中にドキュメントを表示する複数のウィンドウが存在するインターフェースです。
フレームウィンドウ クライアントウィンドウ ドキュメントウィンドウ 最小化状態のドキュメントウィンドウ フレームウィンドウ クライアントウィンドウ ドキュメントウィンドウ ドキュメントウィンドウ SVGの代替画像
MDIをAPIで実現するにはフレームウィンドウと呼ばれる専用のウィンドウを作成しその中であらかじめ用意されているウィンドウクラスであるクライアントウィンドウを作成します。
新規作成等で新たなドキュメントウィンドウを作成するには、クライアントウィンドウに対してドキュメントウィンドウを作成するためのメッセージを送信します。
フレームウィンドウ及びドキュメントウィンドウのプロシージャーのディフォルト処理は専用のAPIを用います。
通常、複数のドキュメントウィンドウをに対して同一のウィンドウプロシージャを用いますが、各ドキュメントウィンドウで固有の値を保存する変数が必要な場合、 ウィンドウごとにユーザーが自由に使える32bitの場合は4バイト、64bitの場合は8バイトのエリアを使用するか、 さらに容量が必要な場合は、ウィンドウクラス登録時に必要なメモリサイズを設定します
本プログラムでは、ドキュメントウィンドウを作成するときにフレームウィンドウから渡すテキストをドキュメントウィンドウのクライアントエリアに表示しています。

テスト環境

コンパイラ

Visual C++ 2008 Standard 32/64bit
Visual C++ 2013 Express 32/64bit

プロジェクトの作成

Win32プロジェクト Windowsアプリケーション

実行環境

Windows 7 EnterPrise Service Pack 1 64bit
Windows 10 Home 32/64bit

プログラムソースの概要

mdi1.cpp

_tWinMain

Windowsから最初に_tWinMain関数が呼び出されます。 MyRegisterWC関数を呼び出し、フレームウィンドウ及びドキュメントウィンドウのクラスを登録します。
LoadMenu API及びGetSubMenuによりメニューを読み込みます。
メニューは、ドキュメントウィンドウが1個もない場合とある場合で内容を使い分けています。
CreateWindow APIによりフレームウィンドウを開きます。
フレームウィンドウのディフォルト処理によりクライアントウィンドウが自動的に作成されます。
後でクライアントウィンドウのハンドルが必要なので、フレームウィンドウの子ウィンドウであるクライアントウィンドウをGetWindow APIにより取得します。
ShowWindow APIでフレームウィンドウを表示、UpdateWindowでフレームウィンドウを更新します。
Windowsは入力等のイベントが発生するとアプリケーションにメッセージを送付します。 メッセージはキューに保管されます。 アプリケーションはメッセージを取り出し、該当ウィンドウにメッセージを配信します。 メッセージの取り出しから配信までループで処理を行いウィンドウから終了メッセージが届くと、ループを抜けるように記述します。 これらの一連の処理は、通常にCreateWindow APIの後に記述しこれをメッセージループと呼んでいます。 詳細は以下を参照してください
メッセージループについて
MDIでドキュメントウィンドウのアクセレータキーの処理はTranslateMDISysAccelを使用する必要があります。
MDIドキュメント固有のアクセレータキーは以下のものがあります。
Alt+ -  MDIドキュメントウィンドウのシステムウィンドウを開く
CTRL+F4 アクティブなMDIドキュメントウィンドウを閉じる
CTRL+F6 次のMDIドキュメントウィンドウをアクティブにする
CTRL+Shift+F6 前のMDIドキュメントウィンドウをアクティブにする

FrameWndProc

フレームウィンドウのプロシージャーです。
RegisterClass APIにより登録することによりフレームウィンドウ作成時にWindowsから呼び出されます。
第2引数にメッセージの種類が格納されていますので、switchステートメントによりメッセージごとの処理を振り分けます。 自分で処理しないメッセージはDefFrameProc API(通常のウィンドウではDefWindowProc)に渡せばWindowsが標準的な処理を行ってくれます。
メニューコマンドの処理はフレームウィンドウで行います。
WM_CREATE
フレームウィンドウの初期化時に呼び出されます。
CLIENTCREATESTRUCT構造体のメンバーを設定して、CreateWindow APIにMDICLIENTクラスを設定してクライアントウィンドウを開きます。
MDICLIENTクラスはウィンドウズが用意しているのでウィンドウプロシージャを記述する必要はありません。
WM_COMMAND
メニューが選択されたときに呼び出されます。 LOWORD(wp)により選択されたメニューのIDが取得できます。
IDM_NEW
ファイルメニューの新規作成を選択したときに呼び出されます。
MDICREATESTRUCT構造体のメンバーを設定して、SendMessage APIによりクライアントウィンドウにWM_MDICREATEメッセージを送信しドキュメントウィンドウを開きます。
ドキュメントウィンドウに表示する文字列を作成し、_tcsdup関数によりヒープ上に文字列をコピーしMDICREATESTRUCT構造体のlParamメンバーにポインタを渡します。
IDM_CLOSE
ファイルメニューの閉じるを選択すると現在アクティブなウィンドウを閉じる動作を行うようにします。
SendMessage APIによりクライアントウィンドウにWM_MDIGETACTIVEメッセージを送信し現在アクティブなドキュメントウィンドウのハンドルを取得します。
ハンドルが0の場合は、アクティブなドキュメントウィンドウが無いので無視します。
アクティブなウィンドウがある場合は、 SendMessage APIによりクライアントウィンドウにWM_MDIDESTROYEメッセージを送信し先ほど取得したハンドルのウィンドウを閉じます。
IDM_EXIT
ファイルメニューの終了を選択した場合呼び出されます。
SendMessage APIにより自分自身にWM_CLOSEメッセージを送信しプログラムを終了させます。
IDM_CLOSEALL
ウィンドウメニューのすべて閉じるを選択すると呼び出されます。
ウィンドウを列挙するEnumChildWindows APIを呼び出します。このAPIにクライアントウィンドウのハンドルを渡すと子ウィンドウであるドキュメントウィンドウが見つかるたびにCloseAllProc関数を呼び出します。
IDM_TILE
ウィンドウメニューの並べて表示を選択すると呼び出されます。
SendMessage APIによりクライアントウィンドウにWM_MDITILEEメッセージを送信し最小化されていないドキュメントウィンドウを並べて表示します。
SendMessage APIの第3引数 wParamに渡す値により動作を以下の様に変更することができます。なお0を指定した場合はMDITILE_VERTICALとなります。
SendMessage(クライアントウィンドウのハンドル, WM_MDITILE, wParam, 0);
wParam動作動作例
MDITILE_HORIZONTAL縦に並べる
MDITILE_VERTICAL横に並べる
MDITILE_SKIPDISABLED使用禁止状態の MDI 子ウィンドウを除外する
IDM_CASCADE
ウィンドウメニューの並べて表示を選択すると呼び出されます。
SendMessage APIによりクライアントウィンドウにWM_MDITILEEメッセージを送信し最小化されていないドキュメントウィンドウを並べて表示します。
SendMessage APIの第3引数 wParamに渡す値により動作を以下の様に変更することができます。なお0を指定した場合はMDITILE_VERTICALとなります。
SendMessage(クライアントウィンドウのハンドル, WM_MDITILE, wParam, 0);
IDM_ARRANGE
ウィンドウメニューのアイコンの整列を選択すると最小化されたドキュメントウィンドウのアイコンを整列します。
整列前整列後
default
ディフォルト処理を行います。
SendMessage APIによりクライアントウィンドウにWM_MDIGETACTIVEメッセージを送信しアクティブなドキュメントウィンドウのハンドルを取得します。
IsWindow APIによりウィンドウハンドルをチェックし有効な場合は、アクティブなドキュメントウィンドウにメッセージをそのまま転送します。
WM_CLOSE
SendMessage APIにより自身のウィンドウにIDM_CLOSEALLメッセージを送信しドキュメントウィンドウをすべて閉じます。
GetWindow APIによりクライアントウィンドウに子ウィンドウ(ドキュメントウィンドウ)があるか確認し、ない場合は戻り値0を指定します。
子ウィンドウがある場合は、DefFrameProc APIに処理を任せます。
WM_DESTROY

DocProc

ドキュメントウィンドウのプロシージャーです。
RegisterClass APIにより登録することによりドキュメントウィンドウ作成時にWindowsから呼び出されます。
第2引数にメッセージの種類が格納されていますので、switchステートメントによりメッセージごとの処理を振り分けます。 自分で処理しないメッセージはDefMDIChildProc API(通常のウィンドウではDefWindowProc)に渡せばWindowsが標準的な処理を行ってくれます。
WM_CREATE
ドキュメントウィンドウの初期化時に呼び出されます。
本プログラムではウィンドウ作成時にパラメータ(MDICREATESTRUCT構造体のlParamメンバー)として渡されたヒープ上の文字列へのポインタをウィンドウごとにユーザーが自由に使用できるメモリに保存しています。
パラメータは、LPARAM lpから以下の手順で取り出します。
LPCREATESTRUCT lpcs=(LPCREATESTRUCT)lp;
LPMDICREATESTRUCT lpmdis=(LPMDICREATESTRUCT)lpcs->lpCreateParams;
TEXT* str=(TCHAR*)lpmdis->lParam;
取り出した値は、SetWindowLongPtr APIにより保存します。
SetWindowLongPtrに渡すデータはコンパイルするビット数により型が異なるためコンパイルエラーが発生します。エラーを回避するためにWINDOW_LONG_PTRという型を定義しキャストに使用しています。
#ifdef _WIN64
	typedef LONG_PTR WINDOW_LONG_PTR;
#else
	typedef LONG WINDOW_LONG_PTR;
#endif
WM_PAINT
ウィンドウを再描画する必要があるときに呼び出されます。 他のウィンドウに隠れ再びフォアグラウンドになった場合などウィンドウの再描画はWindowsが面倒を見ないのでプログラマの仕事となっています。 BeginPaint APIを呼び出して、描画に必要なデバイスコンテキストのハンドルを取得します。 BeginPaint APIの第2引数のポインタにはPAINTSTRUCT構造体のポインタを渡します。 BeginPaint API終了後、PAINTSTRUCT構造体には再描画が必要な領域の座標等の値が格納されています。 GetWindowLongPtr APIによりウィンドウごとに確保されている拡張メモリの値を取得します。 TextOut APIにより拡張メモリに格納されている文字列へのポインタを用いてウィンドウに文字を描画します。 文字の種類や大きさ等はディフォルトの値が使われます。 変更したい場合はCreateFont APIで新たにフォントを作成しSelectObjectでフォントを選択し、フォントが不要になったら、DeleteObject APIでフォントを削除します。 再描画が終了したことをWindowsに知らせるためにEndPaint APIを使用してます。このAPIを呼び出さないと何度もWM_PAINTメッセージが発生し暴走します。
WM_SIZE
ウィンドウサイズが変化した時に呼び出されます。本プログラムでは特に何もしませんのでディフォルト処理としています。
WM_MDIACTIVATE
ドキュメントウィンドウがアクティブになった時に呼び出されます。
本プログラムではメニューを切り替えるタイミングに使用しています。
LPARAMにはアクティブになるウィンドウのハンドル、WPARAMには非アクティブになるウィンドウのハンドルが格納されています。
LPARAMと自身のウィンドウハンドルを比較します。比較結果により以下のメニューを設定します。
ウィンドウメニューのドキュメント名は自動的に登録及び削除されます。
メニューに登録される位置は、_tWinMain関数のドキュメントウィンドウメニュの呼び出しGetSubMenu APIの第2引数で決まります。
ここでは1を指定していますので、ファイルメニューが0個目、ウィンドウメニューが1個目なのでウィンドウメニューに登録されます。
同一
異なる場合

CloseAllProc

指定のウィンドウを閉じます。
SendMessage APIにより指定ウィンドウにWM_MDIDESTROYメッセージを送信し、ドキュメントウィンドウを閉じます。
ウィンドウを列挙するEnumChildWindows APIにこの関数のポインタを渡していますので、ドキュメントウィンドウを全部閉じることができます。

MyRegisterWC

ウィンドウクラスを登録します。
フレームウィンドウとドキュメントウィンドウのクラスの定義に使用しています。

プログラムソース

mdi1.cpp

//	MDI(Multiple Document Interface)サンプル

#include <windows.h>
#include <tchar.h>
#include "resource.h"

#define IDM_FIRSTCHILD 100	//ドキュメントウィンドウの最初のID番号 以降自動的に1ずつ加算される


#ifdef _WIN64
	typedef LONG_PTR WINDOW_LONG_PTR;
#else
	typedef LONG WINDOW_LONG_PTR;
#endif

LRESULT CALLBACK FrameWndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK DocProc(HWND, UINT, WPARAM, LPARAM);
BOOL CALLBACK CloseAllProc(HWND, LPARAM);
int MyRegisterWC(WNDPROC, LPCTSTR, HBRUSH);

TCHAR szFrameClassName[] = _TEXT("mdi_org");    //フレームウィンドウクラス
TCHAR szChildDoc[] = _TEXT("document_org"); //ドキュメント

HMENU hMenuFirst, hMenuDoc;
HMENU hMenuFirstWnd, hMenuDocWnd;
HINSTANCE hInst;

int doc_no=0;	//	ドキュメント(子ウィンドウ)番号

int WINAPI _tWinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst,
                   TCHAR* lpsCmdLine, int nCmdShow){
	MSG msg;
	HWND hFrame, hClient;

	::hInst = hCurInst;
//	フレームウィンドウのクラスの登録   
	if(!MyRegisterWC(FrameWndProc, szFrameClassName,(HBRUSH)(COLOR_APPWORKSPACE + 1)))
		return FALSE;
//	ドキュメントウィンドウのクラスの登録   
	if(!MyRegisterWC(DocProc, szChildDoc, (HBRUSH)GetStockObject(WHITE_BRUSH)))
		return FALSE;
//	フレームウィンドウのメニューを読み込む
	::hMenuFirst = LoadMenu(hInst, _TEXT("MYMENU"));
	::hMenuFirstWnd = GetSubMenu(hMenuFirst, 0);
//	ドキュメントウィンドウのメニューを読み込む
	::hMenuDoc = LoadMenu(hInst, _TEXT("MYDOCUMENT"));
	::hMenuDocWnd = GetSubMenu(hMenuDoc, 1);
//	フレームウィンドウの作成
	hFrame = CreateWindow(
		szFrameClassName,
		_TEXT("Multiple Document Interface"),	//	ウィンドウタイトル
		WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		NULL,
		::hMenuFirst,
		::hInst,
		NULL);
	hClient = GetWindow(hFrame, GW_CHILD);
	ShowWindow(hFrame, nCmdShow);
	UpdateWindow(hFrame);

	while (GetMessage(&msg, NULL, 0, 0)) {
		if (!TranslateMDISysAccel(hClient, &msg)) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}
	DestroyMenu(hMenuDoc);
	return int(msg.wParam);
}

//	フレームウィンドウのプロシージャー

LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp){
	static HWND hClient;
	HWND hChild;
	switch (msg) {
		case WM_CREATE:{
			// クライアントウィンドウの作成
			CLIENTCREATESTRUCT ccs;
			ccs.hWindowMenu = hMenuFirstWnd;
			ccs.idFirstChild = IDM_FIRSTCHILD;//ドキュメントウィンドウの最初のID番号
			hClient = CreateWindow(
			_TEXT("MDICLIENT"),
			NULL,
			WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
			0, 0, 0, 0,
			hWnd,
			(HMENU)1,
			::hInst,
			(LPSTR)&ccs);
			return 0;
		}
		case WM_COMMAND:
			switch (LOWORD(wp)) {
				case IDM_NEW:{	//	新規作成メニュー選択時 子ウィンドウの作成
					MDICREATESTRUCT mdic;
					TCHAR str[64];

					wsprintf(str, _TEXT("テキスト %d"), ::doc_no);
					TCHAR* text=_tcsdup(str);

					wsprintf(str, _TEXT("DOCUMENT[%d]"), ::doc_no);
					mdic.szClass = szChildDoc;
					mdic.szTitle = str;
					mdic.hOwner = ::hInst;
					mdic.x = CW_USEDEFAULT;
					mdic.y = CW_USEDEFAULT;
					mdic.cx = CW_USEDEFAULT;
					mdic.cy = CW_USEDEFAULT;
					mdic.style = 0;
					mdic.lParam = (LPARAM)text; //ドキュメントウィンドウ作成時に渡すパラメータ
					hChild = (HWND)SendMessage(hClient, WM_MDICREATE, 0,
					(LPARAM)&mdic);
					++ ::doc_no;
					return 0;
				}
				case IDM_CLOSE:	//	アクティブなドキュメントウィンドウを閉じる
					hChild = (HWND)SendMessage(hClient, WM_MDIGETACTIVE, 0, 0);
					if (hChild)
						SendMessage(hClient, WM_MDIDESTROY, (WPARAM)hChild, 0);
					return 0;
				case IDM_EXIT:	//	プログラムを終了
					SendMessage(hWnd, WM_CLOSE, 0, 0);
					return 0;
				case IDM_CLOSEALL:	//	全てのドキュメントウィンドウを閉じる
					EnumChildWindows(hClient, &CloseAllProc, 0);
					return 0;
				case IDM_TILE:	//	ドキュメントウィンドウを並べて表示
					SendMessage(hClient, WM_MDITILE, 0, 0);
					return 0;
				case IDM_CASCADE:	//	ドキュメントウィンドウを重ねて表示
					SendMessage(hClient, WM_MDICASCADE, 0, 0);
					return 0;
				case IDM_ARRANGE:	//	アイコンの整列(最小化されたドキュメントウィンドウのアイコンを整列)
					SendMessage(hClient, WM_MDIICONARRANGE, 0, 0);
					return 0;
				default:
					hChild = (HWND)SendMessage(hClient, WM_MDIGETACTIVE, 0, 0);
					if (IsWindow(hChild))
						SendMessage(hChild, WM_COMMAND, wp, lp);
					break;
			}
			break;
		case WM_CLOSE:	//	フレームウィンドウの終了時に呼び出される。
			SendMessage(hWnd, WM_COMMAND, IDM_CLOSEALL, 0);
			if (GetWindow(hClient, GW_CHILD))
				return 0;
			break;
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
	}
	return DefFrameProc(hWnd, hClient, msg, wp, lp);
}

// ドキュメントウィンドウのプロシージャー

LRESULT CALLBACK DocProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp){
	static HWND hClient, hFrame;
	TCHAR* str;

	switch (msg) {
		case WM_CREATE:{
			hClient = GetParent(hWnd);
			hFrame = GetParent(hClient);
			LPCREATESTRUCT lpcs=(LPCREATESTRUCT)lp;
			LPMDICREATESTRUCT lpmdis=(LPMDICREATESTRUCT)lpcs->lpCreateParams;

			str=(TCHAR*)lpmdis->lParam;	//	子ウィンドウ作成時に指定されたパラメータを取得
			// 各ウィンドウごとに個別に使用できるメモリにフレームウィンドウから渡された文字列を保存
			SetWindowLongPtr(hWnd, GWLP_USERDATA, (WINDOW_LONG_PTR)str); 
			return 0;
		}
		case WM_PAINT:{
			PAINTSTRUCT ps;
			HDC hdc=BeginPaint(hWnd,&ps); 
			// 各ウィンドウごとに個別に使用できるメモリから文字列を取得
			str=(TCHAR*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
			TextOut(hdc,0,0,str,int(_tcslen(str)));
			EndPaint(hWnd,&ps); 
			break;
		}
		case WM_SIZE:
			break;
		case WM_MDIACTIVATE:
			if (lp == (LPARAM)hWnd)
				SendMessage(hClient, WM_MDISETMENU, (WPARAM)::hMenuDoc, (LPARAM)::hMenuDocWnd);
			else
				SendMessage(hClient, WM_MDISETMENU, (WPARAM)::hMenuFirst, (LPARAM)::hMenuFirstWnd);
			DrawMenuBar(hFrame);
			return 0;
	}
	return (DefMDIChildProc(hWnd, msg, wp, lp));
}

//	指定するMDIドキュメントウィンドウを閉じる(EnumChildWindowsから呼び出される)

BOOL CALLBACK CloseAllProc(HWND hWnd, LPARAM lp){
	SendMessage(GetParent(hWnd), WM_MDIDESTROY, (WPARAM)hWnd, 0);
	return TRUE;
}

//	ウィンドウクラスを登録

int MyRegisterWC(WNDPROC lpfnWndProc, LPCTSTR lpszClassName, HBRUSH hbrBack){
	WNDCLASSEX wc;

	wc.cbSize = sizeof(WNDCLASSEX);
	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = lpfnWndProc;
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = hInst;        //インスタンス
	wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground = hbrBack;
	wc.lpszMenuName = NULL;    //メニュー名
	wc.lpszClassName = lpszClassName;
	wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
	return(RegisterClassEx(&wc));
}

resource.h

#define IDM_NEW	1000
#define IDM_EXIT	1001
#define IDM_CLOSE	1003
#define IDM_CASCADE	1005
#define IDM_TILE	1006
#define IDM_CLOSEALL	1007
#define IDM_ARRANGE	1008

mdi1.rc

#include <windows.h>
#include "resource.h"

MYMENU MENU DISCARDABLE 
BEGIN
    POPUP "ファイル(&F)"
    BEGIN
        MENUITEM "新規作成(&N)",                IDM_NEW
        MENUITEM "終了(&X)",                    IDM_EXIT
    END
END

MYDOCUMENT MENU DISCARDABLE 
BEGIN
    POPUP "ファイル(&F)"
    BEGIN
        MENUITEM "新規作成(&N)",                IDM_NEW
        MENUITEM "閉じる(&C)",                  IDM_CLOSE
        MENUITEM SEPARATOR
        MENUITEM "終了(&X)",                    IDM_EXIT
    END
    POPUP "ウィンドウ(&W)"
    BEGIN
        MENUITEM "重ねて表示(&C)",              IDM_CASCADE
        MENUITEM "並べて表示(&T)",              IDM_TILE
		MENUITEM "すべて閉じる(&L)",            IDM_CLOSEALL
        MENUITEM "アイコンの整列(&A)",          IDM_ARRANGE
    END
END

ソースファイルと実行ファイルのダウンロード

ダウンロード mdi1.zip(25.4kByte)
ZIPファイルに含まれるファイル
mdi1.cpp
resource.h
mdi1.rc
mdi1.exe