MDI子ウィンドウの処理

子ウィンドウプロシージャ

MDI子ウィンドウでは専用のウィンドウプロシージャを定義して処理を行います。
定義方法はウィンドウプロシージャと基本的に同じで、定義したウィンドウプロシージャをMDI子ウィンドウ用のウィンドウクラスに登録します。
MDI子ウィンドウのデフォルト動作はDefMDIChildProc関数を使用します。

まずは簡単にウィンドウプロシージャを書いてみます。
メニューリソースの追加はMDIアプリのメニューとアクセラレータを参照してください。


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

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

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

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

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	//省略
}

//ウィンドウクラスの登録
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 = DocProc; //子ウィンドウプロシージャを指定
	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;
}

//ウィンドウの作成
HWND InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	//省略
}

//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	//省略
}

//子ウィンドウプロシージャ
LRESULT CALLBACK DocProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;

	int id;
	WCHAR strID[8];

	switch (message)
	{
	case WM_PAINT:
		id = GetDlgCtrlID(hWnd);
		StringCchPrintf(strID, 8, L"%d", id);

		hdc = BeginPaint(hWnd, &ps);
		TextOut(hdc, 10, 10, strID, lstrlen(strID));
		EndPaint(hWnd, &ps);
		return 0;
	}
	return DefMDIChildProc(hWnd, message, wParam, lParam);
}

MDI子ウィンドウプロシージャとしてDocProcを定義しています。
ウィンドウプロシージャ内ではGetDlgCtrlID関数を使用して、子ウィンドウのウィンドウIDを表示しています。
MDI子ウィンドウのウィンドウプロシージャ

DefMDIChildProc関数について

DefMDIChildProc関数はMDI子ウィンドウの基本的な動作を提供する関数です。
特殊な処理を行う場合を除き、以下のメッセージは(必要ならば何らかの処理を行った後に)DefMDIChildProc関数に渡す必要があります。
そうしなければデフォルトの機能が動作しなくなります。

メッセージ 説明
WM_CHILDACTIVATE MDI子ウィンドウのサイズ変更、移動、アクティブ化などを行う。
WM_GETMINMAXINFO MDI子ウィンドウの最大化時のサイズを計算する。
WM_MENUCHAR フレームウィンドウにメッセージを送信する。
WM_MOVE クライアントウィンドウにスクロールバーが存在する場合に再計算する。
WM_SETFOCUS MDI子ウィンドウをアクティブにする。
WM_SIZE MDI子ウィンドウを最大化、または復元する場合に必要な操作を行う。
WM_SYSCOMMAND システムメニューのコマンドを実行する。

ウィンドウ毎に独立したデータを扱う

MDI子ウィンドウは同時に何枚も作成することができますが、ウィンドウプロシージャは共通のものを使用します。
(ウィンドウクラスが共通のため)
プロシージャ内で扱う変数等も共有なので、そのままでは全てのウィンドウで同じ処理しかできません。

ウィンドウにはSetWindowLongPtr関数に定数GWLP_USERDATAを指定することでそのウィンドウ固有のデータを関連付けることができます。
関連付けたデータはGetWindowLongPtr関数で取得できます。


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

#define IDC_BUTTON 100
#define ID_MDICHILDFIRST 200

//ウィンドウの生成等は省略

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

	//テスト用データ
	static WCHAR* testData[] = {
		L"あいうえお",
		L"かきくけこ",
	};
	static int testDataIndex;

	switch (message)
	{
	case WM_CREATE:
		ccs.hWindowMenu = GetSubMenu(GetMenu(hWnd), 1);
		ccs.idFirstChild = ID_MDICHILDFIRST;

		//クライアントウィンドウの作成
		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_COMMAND:
		switch (LOWORD(wParam))
		{
		case ID_QUIT:
			SendMessage(hWnd, WM_CLOSE, 0, 0);
			return 0;

		case ID_CREATECHILD: //子ウィンドウ作成
			h = CreateMDIWindow(szWindowClass_MDIChild, testData[testDataIndex], 0,
				CW_USEDEFAULT, CW_USEDEFAULT,
				CW_USEDEFAULT, CW_USEDEFAULT,
				hClientWnd, hInst, NULL);

			//子ウィンドウにテストデータを関連付ける
			SetWindowLongPtr(h, GWLP_USERDATA, testData[testDataIndex]);

			if (++testDataIndex >= sizeof(testData) / sizeof(testData[0]))
				testDataIndex = 0;
			return 0;
		}
		break;

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

//子ウィンドウプロシージャ
LRESULT CALLBACK DocProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_CREATE:
		//ボタンを作る
		CreateWindow(
			L"BUTTON", L"Click",
			WS_CHILD | WS_VISIBLE,
			10, 10, 80, 25,
			hWnd, (HMENU)IDC_BUTTON, hInst, NULL);
		return 0;

	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDC_BUTTON:
			MessageBox(
				hWnd,
				//ウィンドウに関連付けられたデータを取り出す
				(WCHAR*)GetWindowLongPtr(hWnd, GWLP_USERDATA),
				L"情報",
				MB_OK);
			return 0;
		}
		return 0;
	}
	return DefMDIChildProc(hWnd, message, wParam, lParam);
}

子ウィンドウの作成時にそのウィンドウハンドルを取得し、SetWindowLongPtr関数で特定のデータを関連付けます。
上のコードでは文字列のアドレス("あいうえお"または"かきくけこ")を関連付けています。

子ウィンドウプロシージャでは、GetWindowLongPtr関数でウィンドウハンドルからデータを取り出しています。
「あいうえお」ウィンドウのボタンを押せば「あいうえお」が、「かきくけこ」ウィンドウのボタンを押せば「かきくけこ」がメッセージボックスで表示されます。
MDI子ウィンドウにデータを関連付ける

子ウィンドウ側でデータを管理する

「ウィンドウにデータを関連付ける」というのは、ウィンドウ毎にあらかじめ確保されている領域にデータをコピーすることです。
この領域は32bitアプリなら4バイト、64bitアプリなら8バイトで、これはポインタを格納可能なサイズです。
(最初は「0」で初期化されています)

上記コードは文字列リテラルのアドレスをウィンドウに関連付けています。
文字列リテラルはプログラムの開始から終了までメモリ上に存在し、書き換えられないデータなので、上記コードでも問題は起こりません。
しかし実際のプログラムでは動的に生成した文字列や、文字列以外のデータを扱うことが多くあります。
これらはデータに寿命があったり中身が書き換わったりするため、問題が起こる可能性があります。

データはコピーされるため、単純に数値を関連付ける場合は問題は起こりません。
(ただし上記サイズに収まる範囲の値でなければならない)

この問題を避けるには、データを適切に管理する仕組みが必要です。
フレームウィンドウ側で管理しても良いですが、子ウィンドウで必要なデータは子ウィンドウ側で管理したほうが分かりやすいでしょう。


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

#define WM_SETMDIUSERDATA (WM_APP + 1)
#define IDC_BUTTON 100
#define ID_MDICHILDFIRST 200
#define BUFFERLENGTH 20

//ウィンドウの生成等は省略

//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static const WCHAR *format =
		L"%04d/%02d/%02d "
		L"%02d:%02d:%02d";

	CLIENTCREATESTRUCT ccs;
	HWND h;
	SYSTEMTIME st;
	WCHAR dateTime[BUFFERLENGTH];

	switch (message)
	{
	case WM_CREATE:
		//クライアントウィンドウの作成
		ccs.hWindowMenu = GetSubMenu(GetMenu(hWnd), 1);
		ccs.idFirstChild = ID_MDICHILDFIRST;
		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_COMMAND:
		switch (LOWORD(wParam))
		{
		case ID_QUIT:
			SendMessage(hWnd, WM_CLOSE, 0, 0);
			return 0;

		case ID_CREATECHILD: //子ウィンドウ作成
			//現在の日時文字列を生成
			GetLocalTime(&st);
			StringCchPrintf(dateTime, BUFFERLENGTH, format,
				st.wYear, st.wMonth, st.wDay,
				st.wHour, st.wMinute, st.wSecond);

			//子ウィンドウ作成
			h = CreateMDIWindow(szWindowClass_MDIChild, L"MDI子ウィンドウ", 0,
				CW_USEDEFAULT, CW_USEDEFAULT,
				CW_USEDEFAULT, CW_USEDEFAULT,
				hClientWnd, hInst, NULL);

			//子ウィンドウにデータを関連付ける命令を出す
			SendMessage(
				h,
				WM_SETMDIUSERDATA,
				(WPARAM)dateTime,
				(LPARAM)((lstrlen(dateTime) + 1) * sizeof(WCHAR)));
			return 0;
		}
		break;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}

//子ウィンドウプロシージャ
LRESULT CALLBACK DocProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	void* mem;

	switch (message)
	{
	case WM_CREATE:
		//ボタンを作る
		CreateWindow(
			L"BUTTON", L"Click",
			WS_CHILD | WS_VISIBLE,
			10, 10, 80, 25,
			hWnd, (HMENU)IDC_BUTTON, hInst, NULL);
		return 0;

	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDC_BUTTON:
			MessageBox(
				hWnd,
				//ウィンドウに関連付けられたデータを取り出す
				(WCHAR*)GetWindowLongPtr(hWnd, GWLP_USERDATA),
				L"情報",
				MB_OK);
			return 0;
		}
		return 0;

	case WM_SETMDIUSERDATA: //ウィンドウにデータを関連付ける
		//wParam: 関連付けるデータのアドレス
		//lParam: データのバイト数

		mem = (void*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
		if (mem)
		{
			//すでにメモリが確保されている場合は消去
			free(mem);
		}

		if ((size_t)lParam == 0)
		{
			//lParamが0なら関連付けを解除
			//(「0」を関連付ける)
			SetWindowLongPtr(hWnd, GWLP_USERDATA, 0);
			return 0;
		}

		//lParamバイト分のメモリ確保
		mem = malloc((size_t)lParam);
		if (!mem) {
			MessageBox(hWnd, L"メモリ確保エラー。", L"エラー", MB_OK);
			SetWindowLongPtr(hWnd, GWLP_USERDATA, 0);
			return 0;
		}
		//データコピー
		memcpy(mem, (void*)wParam, (size_t)lParam);

		//確保したメモリ領域のポインタをウィンドウに関連付ける
		SetWindowLongPtr(hWnd, GWLP_USERDATA, mem);
		return 0;


	case WM_DESTROY:
		//ウィンドウにデータが関連付けられている場合は消去
		mem = (void*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
		if (mem)
		{
			free(mem);
		}
		return 0;
	}
	return DefMDIChildProc(hWnd, message, wParam, lParam);
}

子ウィンドウを作成した日時(文字列)を、子ウィンドウ側に保存しておくコードです。
子ウィンドウ側でデータを管理する

今回は子ウィンドウにデータを関連付けるための独自メッセージ「WM_SETMDIUSERDATA」を定義しています。
(→WM_APPを参照)
このメッセージを送信することで任意のタイミングでデータの関連付け(データの更新)を行うことができます。
(今回は子ウィンドウの作成時でしか使用していません)

子ウィンドウ側では、WM_SETMDIUSERDATAメッセージを受け取ったらmalloc関数で必要なメモリ領域を確保し、データをコピーします。
SetWindowLongPtr関数でウィンドウに関連付けるデータは、このメモリ領域のアドレスです。
ウィンドウ側に保存するのはアドレス(ポインタ)なので、確保するデータサイズがいくつでも対応可能です。
ただしメモリ確保&データコピーの処理があるのでデータが大きいほどパフォーマンスは悪くなります。

送信元とは別の領域にデータを保存するので、フレームウィンドウ側の元データが書き換わったり消去されたりしても影響を受けません。
このメモリ領域はウィンドウの破棄時に解放します。

今回定義した「WM_SETMDIUSERDATA」メッセージはSendMessage関数で送信する必要があります。
SendMessage関数は送信先の処理が終わるまで呼び出し元に制御が返らないので、送信したデータのコピーは確実に行われます。

PostMessage関数関数はメッセージを送信したら、送信先の処理を待つことなくすぐに制御が戻ります。
そのため、送信先でデータのコピー処理が終わる前に送信元データが消去されてしまう恐れがあります。

今回のデータは文字列配列なので、SendMessage関数にはそのまま変数名を記述しています。
配列やポインタ以外のデータを渡す場合は&演算子でアドレスを渡す必要がある点に注意してください。


//SYSTEMTIME構造体を関連付ける場合
SYSTEMTIME st;
SendMessage(h, WM_SETMDIUSERDATA, &st, sizeof(SYSTEMTIME));