EternalWindows
メニュー / メニューの選択

メニューの項目を選択したとき、その項目の簡単な説明をステータスバーなどに 表示するアプリケーションはよく見かけます。 このような説明文は、項目の1つの情報として存在していませんが、MENUITEMINFO構造体には dwItemDataというアプリケーションが自由に使ってよいとされるメンバがあります。 よって、このメンバに項目の説明文を維持するバッファのアドレスを関連付ければ、 必要に応じてGetMenuItemInfoでそのアドレスを取得できます。

BOOL GetMenuItemInfo(
  HMENU hMenu,
  UINT uItem,
  BOOL fByPosition,
  LPMENUITEMINFO lpmii
);

引数の意味は、InsertMenuItemやGetMenuItemInfoと同じになります。 MENUITEMINFO構造体は関数によって初期化されることになりますが、 その初期化されるメンバはfMaskで決定されることになるため、 このメンバはプログラムが適切に初期化しなければなりません。

今回のプログラムは、ステータスバーの代わりにウインドウタイトルへ項目の説明文を表示します。

#include <windows.h>

#define ID_FILE  10
#define ID_OPEN  20
#define ID_CLOSE 30

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId, HMENU hmenuSub, LPTSTR lpszData);
LPTSTR GetItemData(HMENU hmenu, int nParam, BOOL bByPosition);
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 LPTSTR lpszData[] = {TEXT("ファイルを開きます。"), TEXT("ファイルを閉じます。"), TEXT("ファイルに関するコマンドです。")};

	switch (uMsg) {

	case WM_CREATE: {
		HMENU hmenu;
		HMENU hmenuPopup;

		hmenu = CreateMenu();
		hmenuPopup = CreatePopupMenu();
		
		InitializeMenuItem(hmenuPopup, TEXT("開く(&O)"), ID_OPEN, NULL, lpszData[0]);
		InitializeMenuItem(hmenuPopup, TEXT("閉じる(&C)"), ID_CLOSE, NULL, lpszData[1]);
		
		InitializeMenuItem(hmenu, TEXT("ファイル(&F)"), ID_FILE, hmenuPopup, lpszData[2]);

		SetMenu(hwnd, hmenu);
		SetWindowText(hwnd, TEXT(""));

		return 0;
	}

	case WM_MENUSELECT: {
		HMENU hmenu;
		
		hmenu = (HMENU)lParam;

		if (hmenu != NULL && HIWORD(wParam) != 0xFFFF) {
			LPTSTR lpszData;

			lpszData = GetItemData(hmenu, LOWORD(wParam), HIWORD(wParam) & MF_POPUP);
			SetWindowText(hwnd, lpszData);
		}

		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId, HMENU hmenuSub, LPTSTR lpszData)
{
	MENUITEMINFO mii;
	
	mii.cbSize     = sizeof(MENUITEMINFO);
	mii.fMask      = MIIM_ID | MIIM_TYPE | MIIM_DATA;
	mii.fType      = MFT_STRING;
	mii.wID        = nId;
	mii.dwItemData = (DWORD)lpszData;
	mii.dwTypeData = lpszItemName;

	if (hmenuSub != NULL) {
		mii.fMask   |= MIIM_SUBMENU;
		mii.hSubMenu = hmenuSub;
	}

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

LPTSTR GetItemData(HMENU hmenu, int nParam, BOOL bByPosition)
{
	MENUITEMINFO mii;

	mii.cbSize = sizeof(MENUITEMINFO);
	mii.fMask  = MIIM_DATA;

	GetMenuItemInfo(hmenu, nParam, bByPosition, &mii);

	return (LPTSTR)mii.dwItemData;
}

プログラムを実行すると、メニューを開いたときや選択したときに、 ウインドウタイトルが変更されるのが確認できるはずです。 プログラムではまず、項目と表示する文字列を関連付けています。

InitializeMenuItem(hmenuPopup, TEXT("開く(&O)"), ID_OPEN, NULL, lpszData[0]);
InitializeMenuItem(hmenuPopup, TEXT("閉じる(&C)"), ID_CLOSE, NULL, lpszData[1]);

InitializeMenuItem(hmenu, TEXT("ファイル(&F)"), ID_FILE, hmenuPopup, lpszData[2]);

今回のInitializeMenuItemは、第5引数が関連付ける文字列のアドレスになっています。 項目と文字列のアドレスを関連付ければ、項目の識別子または位置、 どちらからでも文字列のアドレスを取得できます。 次のコードは、InitializeMenuItemの一部です。

mii.cbSize     = sizeof(MENUITEMINFO);
mii.fMask      = MIIM_ID | MIIM_TYPE | MIIM_DATA;
mii.fType      = MFT_STRING;
mii.wID        = nId;
mii.dwItemData = (DWORD)lpszData;
mii.dwTypeData = lpszItemName;

dwItemDataメンバはプログラムが自由に使ってよいとされており、 fMaskにMIIM_DATAを加えることにより有効になります。 dwItemDataには文字列のアドレスを格納するだけで、 実際に文字列をコピーしているわけではありません。 したがって、文字列のアドレスはプログラムが終了するまで有効でなければなりません。 WindowProcのlpszDataを静的変数で宣言しているのはそのためです。

メニューの項目を選択したときには、WM_MENUSELECTが送られます。 ここで述べている選択とは、基本的にはカーソルを項目の上に乗せたときのことです。 WM_MENUSELECTのパラメータは、以下のようになります。

パラメータ 意味
wParam(下位ワード) 項目がポップアップメニューを持つなら項目の位置、そうでないなら項目の識別子
wParam(上位ワード) 項目の属性
lParam 選択したメニューのハンドル

厄介なのはwParamの下位ワードであり、ポップアップメニューを持つかどうかで 意味が異なります。 また、lParamがNULLでwParamの上位ワードが0xFFFFであるときは メニューが閉じられたことを意味するので、このときは処理が不要です。

case WM_MENUSELECT: {
	HMENU hmenu;
	
	hmenu = (HMENU)lParam;

	if (hmenu != NULL && HIWORD(wParam) != 0xFFFF) {
		LPTSTR lpszData;

		lpszData = GetItemData(hmenu, LOWORD(wParam), HIWORD(wParam) & MF_POPUP);
		SetWindowText(hwnd, lpszData);
	}

	return 0;
}

まず、WM_MENUSELECTがメニューを閉じられた意味合いで送られていないかを調べます。 GetItemDataは内部でGetMenuItemInfoを呼び出す自作関数で、 第2引数と第3引数は、GetMenuItemInfoのそれと対応しています。 LOWORD(wParam)は、メニューがポップアップメニューを持つかどうかでその値の意味が異なりますが、 第3引数をHIWORD(wParam) & MF_POPUPとすれば、 ポップアップメニューを持つ場合は値がTUREとなり、そうでない場合はFALSEとなるため、 第2引数との値の意味に矛盾はなくなります。

LPTSTR GetItemData(HMENU hmenu, int nParam, BOOL bByPosition)
{
	MENUITEMINFO mii;

	mii.cbSize = sizeof(MENUITEMINFO);
	mii.fMask  = MIIM_DATA;

	GetMenuItemInfo(hmenu, nParam, bByPosition, &mii);

	return (LPTSTR)mii.dwItemData;
}

dwItemDataメンバを取得したいので、fMaskにMIIM_DATAを指定します。 関数の呼び出し側は、戻り値を通じて項目の説明文を取得することになります。

メインメニューの選択

WM_MENUSELECTは、メインメニューの項目の選択時には送られてくることはありません。 クリックした場合は送られるのですが、簡単な概要を先に示しておくためにも、 項目の選択を擬似的に再現する価値はあるでしょう。 マウスカーソルがメインメニューやウインドウタイトルなどの非クライアント領域を 通過するときにはWM_NCMOUSEMOVEというメッセージが送られるため、 これを利用した項目の選択方法を考えてみます。

case WM_NCMOUSEMOVE: {
	int   nPos;
	POINT pt;
	HMENU hmenu;

	hmenu = GetMenu(hwnd);

	pt.x = LOWORD(lParam);
	pt.y = HIWORD(lParam);

	nPos = MenuItemFromPoint(hwnd, hmenu, pt);
	if (nPos != -1) {
		LPTSTR lpszData;

		lpszData = GetItemData(hmenu, nPos, TRUE);

		SetWindowText(hwnd, lpszData);
	}

	return 0;
}

MenuItemFromPointは、第3引数で表される位置に項目があるかどうかを調べ、 項目がなければ-1を返し、あれば項目の位置を返します。 この第3引数にlParamで表されるカーソルの位置を指定すれば、 カーソルが項目を選択しているかどうかを確認できるため、 後の処理はWM_MENUSELECTと同じように書くことができます。 MenuItemFromPointが返すのは項目の識別子ではなく位置ですから、 GetItemDataの第3引数はTRUEにします。



戻る