EternalWindows
メニュー / ショートカットメニュー

メニューの表示というのは、何もウインドウのメニューバーだけに限ったことではありません。 ウインドウ上で右クリックしたときにメニューを表示するような アプリケーションも大変多く存在します。 実は、このようなメニューの正体はこれまで説明してきたポップアップメニューであり、 これを任意のタイミングで表示することでショートカットメニューと呼ばれたりしています。 ポップアップメニューを明示的に表示するには、TrackPopupMenuを呼び出します。

BOOL TrackPopupMenu(
    HMENU hMenu,
    UINT uFlags,
    int x,
    int y,
    int nReserved,
    HWND hWnd,
    CONST RECT *prcRect
);

hMenuは、ポップアップメニューのハンドルを指定します。 つまり、CreatePopupMenuの戻り値を指定します。 uFlagsは、メニューの表示方法に関わるフラグですが、基本的に0で構いません。 xは、スクリーン座標で表されたメニューの水平位置を指定します。 yは、スクリーン座標で表されたメニューの垂直位置を指定します。 nReservedは、0でなければなりません。 hWndは、ウインドウのハンドルを指定します。 項目を選択したとき、このウインドウにWM_COMMANDが発行されます。 prcRectは無視されるため、NULLを指定しておけばよいと思われます。

TrackPopupMenuの第2引数にTPM_NONOTIFYを指定した場合は、WM_INITMENUPOPUPが送られなくなることに注意してください。 WM_INITMENUPOPUPは、メニューを表示する直前に項目の有効/無効を切り替える機会を与えますが、 自作メニューがこうした動作を行うつもりがないのであればTPM_NONOTIFYを指定しても問題ありません。 しかし、システムから返されるメニューではWM_INITMENUPOPUPを処理するようなこともあり、 そのような場合にTPM_NONOTIFYを指定していると、項目が正しく表示されないことがあります。 ちなみに、TPM_NONOTIFYを指定した場合でもWM_MENUSELECTは送られるようになっています。

今回のプログラムは、ウインドウ上で右クリックしたときにポップアップメニューを表示します。

#include <windows.h>

#define ID_COPY  10
#define ID_PASTE 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("コピー(&C)"), ID_COPY);
		InitializeMenuItem(hmenuPopup, TEXT("貼り付け(&P)"), ID_PASTE);	

		return 0;
	}
	
	case WM_CONTEXTMENU: {
		POINT pt;

		pt.x = LOWORD(lParam);
		pt.y = HIWORD(lParam);
		
		TrackPopupMenu(hmenuPopup, 0, pt.x, pt.y, 0, hwnd, NULL);

		return 0;
	}

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

		if (nId == ID_COPY)
			MessageBox(hwnd, TEXT("コピーが選択されました。"), TEXT("OK"), MB_OK);
		else if (nId == ID_PASTE)
			MessageBox(hwnd, TEXT("貼り付けが選択されました。"), TEXT("OK"), MB_OK);
		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.fType      = MFT_STRING;
	mii.wID        = nId;
	mii.dwTypeData = lpszItemName;

	InsertMenuItem(hmenu, nId, FALSE, &mii);
}

ウインドウ上で右クリックしたときには、WM_CONTEXTMENUが送られます。 WM_RBUTTONDOWNも送られますが、ショートカットメニューを表示する場合は、 可能な限りWM_CONTEXTMENUを処理した方がよいでしょう。 というのも、WM_CONTEXTMENUのlParamはカーソル位置を スクリーン座標で表現しているからです。 WM_RBUTTONDOWNの場合、lParamはクライアント座標であるため、 クライアント座標をスクリーン座標に変換する作業が生じることになります。 どうしても、WM_RBUTTONDOWNで処理したい場合は以下のようになるでしょう。

case WM_RBUTTONDOWN: {
	POINT pt;

	pt.x = LOWORD(lParam);
	pt.y = HIWORD(lParam);
	ClientToScreen(hwnd, &pt);
	
	TrackPopupMenu(hmenuPopup, 0, pt.x, pt.y, 0, hwnd, NULL);
	
	return 0;
}

今回は、WM_DESTROYでメニューハンドルを破棄しています。

DestroyMenu(hmenuPopup);

DestroyMenuを呼び出すのは、このメニューがウインドウに割り当てられていないからです。 SetMenu等の呼び出しで、メニューがウインドウに割り当てられている場合、 そのメニューはウインドウの破棄と共に削除されますが、 割り当てられていない場合は、メニューの破棄はプログラムが負います。 ちなみに、IsMenuという指定した引数がメニューハンドルかを調べる関数がありますが、 この関数に破棄されたメニューを指定した場合、0が返ります。

システムメニューを明示的に表示する

ウインドウのタイトル上で右クリックして表示されるシステムメニューは、 原理的にはTrackPopupMenuの呼び出しで生じていると考えて問題ないでしょう。 GetSystemMenuの戻り値をTrackPopupMenuに指定することで、 アプリケーションは任意のタイミングでシステムメニューを表示できます。

TrackPopupMenu(GetSystemMenu(hwnd, FALSE), 0, pt.x, pt.y, 0, hwnd, NULL);

このコードは、確かにシステムメニューを指定した位置に表示できますが、 実際にはこのメニューの項目を選択しても何も起きることはありません。 TrackPopupMenuは、選択した項目の識別子をwParamの下位ワードとしてWM_COMMANDを送る設計になっていますが、 システムメニューの項目はWM_SYSCOMMANDとして送らなければならないため、 単純に上記のようにTrackPopupMenuを呼ぶだけでは効果がないのです。 そこで、TrackPopupMenuの第2引数のフラグを利用することになります。

nId = TrackPopupMenu(GetSystemMenu(hwnd, FALSE), TPM_RETURNCMD, pt.x, pt.y, 0, hwnd, NULL);
		
PostMessage(hwnd, WM_SYSCOMMAND, (WPARAM)nId, 0);

TPM_RETURNCMDという定数を指定した場合は関数の戻り値が選択した項目の識別子となり、WM_COMMANDは送られません。 後は、これを基に明示的にWM_SYSCOMMADを送るようにすれば、 実際に項目を選択したのと同じ効果が生じます。



戻る