EternalWindows
メニュー / 独自のシステムメニュー

メニューというのは、そもそも項目を自由に追加できる時点で独自なものであり、 前節のようなオーナードローを用いれば、より一層カスタマイズできます。 しかし、システムメニューを独自なものにしようと考えた場合、 なかなかよい策が見つからないと思われます。 確かに、項目を明示的に追加したりオーナードローしたりすることによって、 ある程度のカスタマイズはできると思われますが、 システムメニューが既に作成されているメニューである以上、 それを後からカスタマイズするというのは、効率の悪さを否めません。 どうせなら、予め独自のメニューを作っておき、 それをシステムメニューの代わりに表示するほうが、明らかに効率的ではないでしょうか。 実は、ポップアップメニューを表示しようとしたときにはWM_INITMENUPOPUPが送られ、 このメッセージのwParamには表示されようとしているメニューのハンドルが格納されています。

case WM_INITMENUPOPUP:
	if (HIWORD(lParam) == 1) {
		// 独自のメニューを表示する。
	}
	return 0;

WM_INITMENUPOPUPのlParamの上位ワードが1である場合は、 そのメニューがシステムメニューであることを意味しているため、 これを判定に使うことができます。 WM_INITMENUPOPUPを自ら処理した場合は、0を返すべきとされています。

さて、上記コードが上手く機能するのかどうかですが、 結論から述べると期待通りにはいきません。 そもそも、今回の目的は本来のシステムメニューの代わりに独自のメニューを表示するというものですが、 このWM_INITMENUPOPUPが送られている時点で、 既にメニューは作成され表示段階に入っているため、 メニューの表示をここでキャンセルするようなことはできないのです。 もし、このメッセージに「DefWindowProcに処理させなければメニューの表示を防げる」 というような機能があれば、恐らく期待通りに進んだかと思われます。

上記の点から分かるように、本当に処理すべきメッセージはWM_INITMENUPOPUPよりも前に送られ、 さらにメニュー作成の可否を調整するようなメッセージでなければなりません。 これはあまり知られていない話ですが、実はシステムメニューを表示しようとすると、 0x313という値を持つメッセージが送られてくることになっています。 このメッセージは完全に非公開であり、WM_XXXという形で定義されていませんが、 どうやらこのメッセージをDefWindowProcに処理させなかった場合、 システムメニューが表示されることはないようです。 よって、このメッセージを捕らえればシステムメニューの代わりに 独自のメニューを表示できることになります。

case 0x313:
	// 独自のメニューを表示する。
	return 0;

このメッセージで0を返すということが何を意味するかは分かりませんが、 重要なのはDefWindowProcに処理させないということであるため、 返す値は自由でよいと思われます。

上記の0x313というメッセージは確かにシステムメニューの表示を通知しますが、 それはタスクバー上におけるシステムメニューに限られます。 つまり、ウインドウの左上隅にあるアイコンから表示できるシステムメニューは検出することができません。 このメニューは、アイコン上で左クリックを行うか右クリックを行うかで表示されると思われますが、 よく考えるとこの2つの動作はどちらもマウスを経由して行われています。 これはつまり、システムメニューの表示がマウスメッセージから派生しているということですから、 そのマウスメッセージを捕らえて後続の処理を無効にできれば、 独自のシステムメニューを表示できるのではないでしょうか。 この点についてもう少し言及していきましょう。

一般にマウスメッセージといえば、WM_LBUTTONDOWNやWM_MOUSEMOVEを想像しますが、 実はこれらはWM_NCHITTESTの処理結果として生成されるメッセージに過ぎません。 WM_NCHITTESTは、あらゆるマウスメッセージに先立って送られるメッセージで、 ここで返した値によって生成されるメッセージが決定されます。 ヒットテストとは、いわば現在カーソルがどこにあるかを表すもので、 DefWindowProcはそれをヒットテストコードとして返すことになっています。 以下に、ヒットテストコードとして定義されている値の一部を示します。

ヒットテストコード 意味
HTCAPTION タイトルバー
HTCLIENT クライアント領域
HTSYSMENU システムメニュー
HTMAXBUTTON 最大化ボタン
HTMINBUTTON 最小化ボタン

たとえば、ヒットテストコードがHTSYSMENUであるならば、 それはカーソルがウインドウ左上隅のアイコン上にあることを示しています。 よって、このときにマウスの左ボタンが押されていれば、 ウインドウ左上隅のアイコンをクリックしたと判断できます。

case WM_NCHITTEST: {
	LRESULT lr;
	
	lr = DefWindowProc(hwnd, uMsg, wParam, lParam);
	
	if (lr == HTSYSMENU) {
		// 独自のメニューを表示して、HTSYSMENUでない値を返す。
	}

	return lr;
}

WM_NCHITTESTが、全てのマウスメッセージに先立って送られることを思い出してください。 ボタンを押下したことや、カーソルを動かしたなどという実質的な動作は、 このメッセージの戻り値によって開始されるわけですから、 ここでHTSYSMENUでない値を返すことは、ウインドウ左上隅のアイコン上に カーソルが存在することをなかったことにするのと同じことです。 後は上記のif文にマウスの左ボタンが押下されたことを調べるコードを追加すれば、 アイコンの押下をなかったことにできるわけですから、 システムメニューの表示は結果的に防げることになります。 条件式に掛からなかったときにreturn lr;としているのは、 既にヒットテストコードの取得のためDefWindowProcを呼び出しているためです。 breakとしてしまっては、DefWindowProcが2回呼び出すことになってしまいます。

今回のプログラムは、システムメニューを独自に表示します。

#include <windows.h>

#define ID_SIZE  10
#define ID_CLOSE 20

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HMENU hmenuPopup = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		hmenuPopup = CreatePopupMenu();

		InitializeMenuItem(hmenuPopup, TEXT("通常サイズ(&S)"), ID_SIZE);
		InitializeMenuItem(hmenuPopup, NULL, 0);
		InitializeMenuItem(hmenuPopup, TEXT("閉じる(&C)"), ID_CLOSE);		

		return 0;
	}
	
	case 0x313:
		TrackPopupMenu(hmenuPopup, 0, LOWORD(lParam), HIWORD(lParam), 0, hwnd, NULL);
		return 0;
	
	case WM_NCHITTEST: {
		LRESULT lr;
		
		lr = DefWindowProc(hwnd, uMsg, wParam, lParam);
		
		if (GetAsyncKeyState(VK_LBUTTON) < 0 && lr == HTSYSMENU) {
			POINT pt = {0, 0};

			ClientToScreen(hwnd, &pt);
			TrackPopupMenu(hmenuPopup, 0, pt.x, pt.y, 0, hwnd, NULL);

			return HTHELP + 1;
		}
		else if (GetAsyncKeyState(VK_RBUTTON) < 0) {
			if (lr == HTCAPTION || lr == HTSYSMENU || lr == HTMAXBUTTON || lr == HTMINBUTTON) {
				TrackPopupMenu(hmenuPopup, 0, LOWORD(lParam), HIWORD(lParam), 0, hwnd, NULL);
				return HTHELP + 1;
			}
		}
		else
			;
		
		return lr;
	}

	case WM_COMMAND: {
		int nId = LOWORD(wParam);

		if (nId == ID_SIZE) {
			if (IsIconic(hwnd))
				OpenIcon(hwnd);
			else if (IsZoomed(hwnd))
				ShowWindow(hwnd, SW_RESTORE);
			else
				;
		}
		else if (nId == ID_CLOSE)
			PostMessage(hwnd, WM_CLOSE, 0, 0);
		else
			;

		return 0;
	}

	case WM_DESTROY:
		DestroyMenu(hmenuPopup);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId)
{
	MENUITEMINFO mii;
	
	mii.cbSize = sizeof(MENUITEMINFO);
	mii.fMask  = MIIM_ID | MIIM_TYPE;
	mii.wID    = nId;
	
	if (lpszItemName != NULL) {
		mii.fType = MFT_STRING;
		mii.dwTypeData = lpszItemName;
	}
	else
		mii.fType = MFT_SEPARATOR;
	
	InsertMenuItem(hmenu, nId, FALSE, &mii);
}

システムメニューの代わりとして表示するメニューは、WM_CREATEで作成しています。

case WM_CREATE: {
	hmenuPopup = CreatePopupMenu();

	InitializeMenuItem(hmenuPopup, TEXT("通常サイズ(&S)"), ID_SIZE);
	InitializeMenuItem(hmenuPopup, NULL, 0);
	InitializeMenuItem(hmenuPopup, TEXT("閉じる(&C)"), ID_CLOSE);		

	return 0;
}

このように個性がなく少ない項目では、 何のために独自のメニューを表示するのかが問われるため、 実際の開発ではもっと面白い項目を追加したいものです。 項目が選択された場合はWM_COMMANDが送られます。

case WM_COMMAND: {
	int nId = LOWORD(wParam);

	if (nId == ID_SIZE) {
		if (IsIconic(hwnd))
			OpenIcon(hwnd);
		else if (IsZoomed(hwnd))
			ShowWindow(hwnd, SW_RESTORE);
		else
			;
	}
	else if (nId == ID_CLOSE)
		PostMessage(hwnd, WM_CLOSE, 0, 0);
	else
		;

	return 0;
}

「通常サイズ」が選択された場合は、現在のウインドウの状態に応じて行うべき内容が決定します。 IsIconicはウインドウが最小化されている場合は0以外を返す関数で、 もしそうであればOpenIconで通常のサイズに戻します。 IsZoomedはウインドウが最大化されている場合は0以外を返す関数で、 もしそうであればSW_RESTOREを指定したShowWindowで通常のサイズに戻します。 既に通常のサイズである場合は何も行われません。 「閉じる」が選択された場合は、WM_CLOSEをポストしてウインドウを閉じるようにします。

タスクバー上でのシステムメニューが表示される際には、0x313というメッセージが送られます。

case 0x313:
	TrackPopupMenu(hmenuPopup, 0, LOWORD(lParam), HIWORD(lParam), 0, hwnd, NULL);
	return 0;

メニューの表示位置を表す引数にlParamを使っていることに注目してください。 0x313というメッセージのlParamには、タスクバー上でのカーソルの位置がスクリーン座標で格納されているため、 明示的にアプリケーションがGetCursorPosなどでカーソルの位置を取得する必要はないのです。

ウインドウ左上隅のアイコンからも独自のシステムメニューを表示するためには、 WM_NCHITTESTを処理することになります。

if (GetAsyncKeyState(VK_LBUTTON) < 0 && lr == HTSYSMENU) {
	POINT pt = {0, 0};

	ClientToScreen(hwnd, &pt);
	TrackPopupMenu(hmenuPopup, 0, pt.x, pt.y, 0, hwnd, NULL);

	return HTHELP + 1;
}

GetAsyncKeyStateは、第2引数の仮想キーコードで示されるキーが 押下されている場合、0以下の値を返します。 今回調べるのはキーではなくマウスのボタンですが、ボタンにも専用の仮想キーコードが 定義されているので、GetAsyncKeyStateで調べることができます。 HTSYSMENUの際は、システムメニューの表示が常にクライアント領域の左上隅になるため、 その位置を定義し、ClientToScreenでスクリーン座標に変換して表示します。 戻り値はHTSYSMENUであってはならないのは当然ですが、 HTSYSMENU以外であればなんでもよいというわけではありません。 ここで返す値はその後発行されるマウスメッセージに直結するわけですから、 意味を持たないヒットテストコードを返さなくてはなりません。 HTHELPはヒットテストコードとして定義されている最後の定数で、 これ以上の値を返した場合はマウスメッセージが発行されないと思われます。

else if (GetAsyncKeyState(VK_RBUTTON) < 0) {
	if (lr == HTCAPTION || lr == HTSYSMENU || lr == HTMAXBUTTON || lr == HTMINBUTTON) {
		TrackPopupMenu(hmenuPopup, 0, LOWORD(lParam), HIWORD(lParam), 0, hwnd, NULL);
		return HTHELP + 1;
	}
}

システムメニューは、タイトル上で右ボタンが押されたときにも表示しなければなりません。 HTCAPTIONはタイトルバーを示していますが、最大化ボタンの上にカーソルがある場合などは、 そちらのヒットテストコードが返ることになっています。 よって、タイトル上における全てのヒットテストコードを比較することになります。 WM_HITTESTのlParamは現在のカーソルの位置がスクリーン座標で格納されているため、 メニューの表示でそのまま利用できます。

システムメニューの表示を促す方法

0x313というメッセージは、今回のようにそれを捕らえて0を返すという方法以外に、 システムメニューを明示的に表示するという使い道があります。 このメッセージのデフォルトの処理はシステムメニューの表示ですから、 それをポストすることは、当然システムメニューが表示されることを意味しています。

case WM_KEYDOWN: // 何らかのキーが押された
	if (wParam == VK_RETURN) { // 押されたキーはEnterキーか
		POINT pt;
		
		GetCursorPos(&pt);
		
		PostMessage(hwnd, 0x313, 0, MAKELPARAM(pt.x, pt.y));
	}
	return 0;

0x313を送るべきときはwParamを0とし、lParamはマウスカーソルの位置を指定します。 この推測は、0x313がシステムから送られてきたときに値を確認することで、 ある程度の確証を得ることができます。 MAKELPARAMマクロは第1引数をlParamの下位ワードに、第2引数を上位ワードに設定します。 ちなみに、上記の処理は以下のように書くこともできます。

if (wParam == VK_RETURN)
	PostMessage(hwnd, 0x313, 0, GetMessagePos());

GetMessagePosは、GetMessageでメッセージを取得したときのカーソルの位置を返す関数ですが、 このカーソルの位置をDWORD型で返すという風変わりな性質のおかげで、 戻り値をLPARAMへと直に指定できます。



戻る