MDIアプリのメニューとアクセラレータ

メニューの作成

前ページでは見かけ上はMDIアプリっぽいものを作成しました。
しかしそのままではまともに子ウィンドウを操作できないので、改善するためにメニューを作成します。

以下のリソーススクリプトを用意し、フレームウィンドウにメニューを追加します。
子ウィンドウは「ウィンドウ」メニュー内の「新規ウィンドウ」から作成するように変更します。


/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

IDR_MENU1 MENU
BEGIN
    POPUP "ファイル(&F)"
    BEGIN
        MENUITEM "終了(&X)",                      ID_QUIT
    END
    POPUP "ウィンドウ(&W)"
    BEGIN
        MENUITEM "新規ウィンドウ(&N)",                 ID_CREATECHILD
    END
END

//グローバル変数
HWND hClientWnd;

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

	HWND hWnd = CreateWindow(
		szWindowClass, szTitle,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL,
		//メニューの追加
		LoadMenu(hInst, MAKEINTRESOURCE(IDR_MENU1)),
		hInstance,
		NULL
	);

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

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

	switch (message)
	{
	case WM_CREATE:
		//サブメニューハンドルを指定
		ccs.hWindowMenu = GetSubMenu(GetMenu(hWnd), 1);
		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_COMMAND:
		switch (LOWORD(wParam))
		{
		case ID_QUIT:
			SendMessage(hWnd, WM_CLOSE, 0, 0);
			return 0;
		case ID_CREATECHILD:
			CreateMDIWindow(szWindowClass_MDIChild, L"MDI子ウィンドウ", 0,
				CW_USEDEFAULT, CW_USEDEFAULT,
				CW_USEDEFAULT, CW_USEDEFAULT,
				hClientWnd, hInst, 0
			);
			return 0;
		}
		break;

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

ポイントは、クライアントウィンドウ作成時にCLIENTCREATESTRUCT構造体hWindowMenuメンバに、「ウィンドウ」サブメニューのハンドルを指定することです。
(→GetSubMenu関数)
これにより、子ウィンドウの一覧が「ウィンドウ」メニューの末尾に自動的に表示されるようになります。
メニューの作成

現在アクティブになっている子ウィンドウ項目にチェックが付けられ、子ウィンドウの追加/削除を行うたびにメニュー項目も自動的に更新されます。
子ウィンドウ項目を選択するとその子ウィンドウがアクティブになります。

メニューを追加すると、子ウィンドウを最大化した場合に子ウィンドウの「最小化」「最大化」「閉じる」ボタンが右上に表示されるようになります。
MDI子ウィンドウのシステムメニュー

また、クライアントウィンドウハンドルを格納する変数(hClientWnd)をstatic変数からグローバル変数に変更しています。
これは後述するアクセラレータで利用するためです。

DefFrameProc関数について

自動的に追加されるメニュー項目を選択することでその子ウィンドウがアクティブになるのはDefFrameProc関数がその機能を提供しているからです。
このメニュー項目を選択した場合も、通常のメニュー項目と同じ様にフレームウィンドウにWM_COMMANDメッセージが送信されます。
この時のコントロールID(WPARAMの下位ワード)は子ウィンドウのIDですが、このメッセージはそのままDefFrameProc関数に渡す必要があります。

以下のメッセージはDefFrameProc関数に渡すようにしてください。
そうしなければデフォルトの機能が使用できなくなります。

メッセージ 説明
WM_COMMAND メニュー項目の選択によりMDI子ウィンドウをアクティブにする。
WM_MENUCHAR 「Alt + -(マイナス)」キーによりアクティブなMDI子ウィンドウのメニューを開く。
WM_SETFOCUS キーボードフォーカスをMDI子ウィンドウに渡す。
WM_SIZE MDI子ウィンドウの新規作成時に、ウィンドウサイズがクライアントウィドウ内に収まるようにサイズを調整する。
(自前でサイズ調整をする場合はDefFrameProc関数を実行しない)

MDIアクセラレータ

MDI子ウィンドウのシステムメニューを開くと以下のようなメニュー項目があります。
MDI子ウィンドウのシステムメニュー

「閉じる」は「Ctrl + F4」キーで、「次のウィンドウに移る」は「Ctrl + F6」キーで実行可能となっていますが、実際にこのキーを押下しても何も反応しません。
(項目をマウスクリックした場合は有効です)
これは子ウィンドウ用のアクセラレータが設定されていないためです。
これらのアクセラレータを有効にするにはTranslateMDISysAccel関数を使用します。

BOOL TranslateMDISysAccel(
 HWND hWndClient,
 LPMSG lpMsg
);
MDIアクセラレータキーの入力(WM_KEYUPおよびWM_KEYDOWNメッセージ)をWM_SYSCOMMANDに変換し、MDIクライアントウィンドウhWndClientの子ウィンドウにメッセージlpMsgを送信する。
成功した場合は0以外を、失敗した場合は0を返す。

MDIアクセラレータキーは以下のものがあります。

キー 説明
Alt + -(マイナス) 子ウィンドウのシステムメニューを開く
Ctrl + F4 アクティブな子ウィンドウを閉じる
Ctrl + F6 次の子ウィンドウをアクティブにする
Ctrl + Shift + F6 前の子ウィンドウをアクティブにする

TranslateMDISysAccel関数はメッセージループ内で以下のように使用します。


//グローバル変数
HWND hClientWnd;

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;
		}
		if (!TranslateMDISysAccel(hClientWnd, &msg)) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;
}

通常のアクセラレータ

MDIアプリで通常のアクセラレータを使用する場合は以下のようにします。


//グローバル変数
HWND hClientWnd;

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	MyRegisterClass(hInstance);
	//フレームウィンドウのハンドルを保存
	HWND hWnd = InitInstance(hInstance, nCmdShow);
	if (!hWnd)
	{
		return 0;
	}

	HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
	MSG msg;
	BOOL ret;

	//メッセージループ
	while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (ret == -1)
		{
			return -1;
		}
		if (!TranslateMDISysAccel(hClientWnd, &msg) &&
			!TranslateAccelerator(hWnd, hAccel, &msg))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;
}

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

	HWND hWnd = CreateWindow(
		szWindowClass, szTitle,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL,
		LoadMenu(hInst, MAKEINTRESOURCE(IDR_MENU1)),
		hInstance,
		NULL
	);

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

アクセラレータはフレームウィンドウに対して設定されるので、フレームウィンドウのウィンドウハンドルを保存しておきTranslateAccelerator関数に渡す必要があります。

キー入力は現在フォーカスを持っているウィンドウに送られます。
MDIアプリの場合、子ウィンドウがある場合は子ウィンドウがフォーカス持ちます。
子ウィンドウが無い場合はクライアントウィンドウがフォーカスを持ちます。
そのためMSG構造体のhwndメンバにフレームウィンドウのハンドルが格納されることはないため、ウィンドウ作成時に保存しておく必要があります。
(→アクセラレータ#ウィンドウ上にコントロール類がある場合も参照)