EternalWindows
メニュー / オーナードロー

オーナードローとは、項目の描画をプログラム自ら行うことであると 考えたら分かりやすいでしょう。 メニューの項目には名前があるわけですが、 その名前の色や項目を選択したときの色は、全てシステムによって決められています。 オーナードローを採用すれば項目の領域に自由に描画できるので、 項目の見た目をカスタマイズできます。 描画はデバイスコンテキストを通じて行うため、GDIに関する知識が必要です。

オーナードローは、メニュー固有の機能ではありません。 リストボックスやコンボボックスもオーナードローを採用できます。 しかし、オーナードローで使われるメッセージは同じなので、 基本的を押さえれば描画対象を問わず応用が効きます。 オーナードローの鍵となるメッセージは、WM_DRAWITEMです。 このメッセージは、項目を表示するときや選択されたときなどに送られます。 lParamはDRAWITEMSTRUCT構造体のアドレスで、描画に必要な情報が含まれています。

typedef struct tagDRAWITEMSTRUCT {
    UINT CtlType;
    UINT CtlID;
    UINT itemID;
    UINT itemAction;
    UINT itemState;
    HWND hwndItem;
    HDC hDC;
    RECT rcItem;
    ULONG_PTR itemData;
} DRAWITEMSTRUCT;

CtlTypeメンバで描画対象のタイプを特定できます。 メニューならばODT_MENU、リストボックスならODT_LISTBOXです。 CtlIDメンバはリストボックスやコンボボックスが参照するメンバで、 メニューの場合は参照することはありません。 メニューではitemIDメンバを参照し、描画対象の項目の識別子を取得します。 hDCはデバイスコンテキストのハンドル、rcItemは項目の大きさです。

次のコードは、WM_DRAWITEMをどのように処理することになるのかを示したものです。

case WM_DRAWITEM: {
	TCHAR            szItem[256];
	COLORREF         cr;
	LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam;

	if (lpdis->itemID == ID_COPY) {
		cr = RGB(0, 0, 255);
		lstrcpy(szItem, TEXT("コピー"));
	}
	else if (lpdis->itemID == ID_PASTE) {
		cr = RGB(0, 255, 255);
		lstrcpy(szItem, TEXT("貼り付け"));
	}
		
	SetTextColor(lpdis->hDC, cr);
		
	DrawText(lpdis->hDC, szItem, -1, &lpdis->rcItem, DT_CENTER | DT_VCENTER);
}

見て分かるように、itemIDメンバを参照して項目ごとに処理を行っています。 しかしながら、このコードは美しいとはいえないでしょう。 このメッセージで項目ごとの文字列や色を定義してしまっては、 他のメッセージからこれらを参照することができなくってしまうからです。 また、項目の内容は項目をメニューに追加すると同時に初期化したほうが、 効率の面でも優れているでしょう。

今回のプログラムは、マウスの左ボタンが押された場合にショートカットメニューを表示します。 メニュー項目の色は、オーナードローによってカラフルになっています。

#include <windows.h>

#define ID_COPY  10
#define ID_PASTE 20

struct DATA {
	int      nId;
	COLORREF crText;
	COLORREF crSelect;
	TCHAR    szItem[256];
};
typedef struct DATA DATA;
typedef DATA *LPDATA;

void InitializeData(LPDATA lpData, int nId, COLORREF crText, COLORREF crSelect, LPTSTR lpszItem);
void InitializeMenuItem(HMENU hmenu, LPDATA lpData);
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 DATA  data[2] = {0};
	static HFONT hfontNC = NULL;
	static HMENU hmenuPopup = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		NONCLIENTMETRICS ncMetrics;

		ncMetrics.cbSize = sizeof(NONCLIENTMETRICS);
		SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &ncMetrics, 0);	
		
		hfontNC = CreateFontIndirect(&ncMetrics.lfMenuFont);

		hmenuPopup = CreatePopupMenu();

		InitializeData(&data[0], ID_COPY, RGB(0, 0, 255), RGB(255, 255, 0), TEXT("コピー"));
		InitializeData(&data[1], ID_PASTE, RGB(0, 255, 255), RGB(128, 0, 128), TEXT("貼り付け"));

		InitializeMenuItem(hmenuPopup, &data[0]);
		InitializeMenuItem(hmenuPopup, &data[1]);

		return 0;
	}

	case WM_MEASUREITEM: {
		HDC                 hdc;
		SIZE                size;
		HFONT               hfontOld;
		LPDATA              lpData;
		LPMEASUREITEMSTRUCT lpmis = (LPMEASUREITEMSTRUCT)lParam;
		
		lpData = (LPDATA)lpmis->itemData;
		
		hdc = GetDC(hwnd);
		
		hfontOld = (HFONT)SelectObject(hdc, hfontNC);
		GetTextExtentPoint32(hdc, lpData->szItem, lstrlen(lpData->szItem), &size);

		lpmis->itemWidth  = size.cx + 10;
		lpmis->itemHeight = size.cy + 4;
    
		SelectObject(hdc, hfontOld);

		ReleaseDC(hwnd, hdc);

		return 0;
	}
			 
	case WM_DRAWITEM: {
		LPDATA           lpData;
		LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam;

		lpData = (LPDATA)lpdis->itemData;
		
		if (lpdis->itemState & ODS_SELECTED) {
			HBRUSH hbr;
			
			hbr = CreateSolidBrush(lpData->crSelect);

			FillRect(lpdis->hDC, &lpdis->rcItem, hbr);

			DeleteObject(hbr);
			
			SetBkMode(lpdis->hDC, TRANSPARENT);
		}
		else
			FillRect(lpdis->hDC, &lpdis->rcItem, (HBRUSH)GetStockObject(WHITE_BRUSH));
		
		SetTextColor(lpdis->hDC, lpData->crText);

		DrawText(lpdis->hDC, lpData->szItem, -1, &lpdis->rcItem, DT_CENTER | DT_VCENTER);

		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:
		DeleteObject(hfontNC);
		DestroyMenu(hmenuPopup);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

void InitializeData(LPDATA lpData, int nId, COLORREF crText, COLORREF crSelect, LPTSTR lpszItem)
{
	lpData->nId      = nId;
	lpData->crText   = crText;
	lpData->crSelect = crSelect;

	lstrcpy(lpData->szItem, lpszItem);
}

void InitializeMenuItem(HMENU hmenu, LPDATA lpData)
{
	MENUITEMINFO mii;
	
	mii.cbSize     = sizeof(MENUITEMINFO);
	mii.fMask      = MIIM_ID | MIIM_TYPE | MIIM_DATA;
	mii.fType      = MFT_OWNERDRAW;
	mii.wID        = lpData->nId;
	mii.dwItemData = (DWORD)lpData;
	
	InsertMenuItem(hmenu, lpData->nId, FALSE, &mii);
}

プログラムでは、項目一個にその内容を定義した構造体を関連付けています。 それによって、WM_DRAWITEMで項目ごとの処理をする必要がなくなっています。 項目の内容を定義している構造体は、プログラムの冒頭で宣言されています。

struct DATA {
	int      nId;
	COLORREF crText;
	COLORREF crSelect;
	TCHAR    szItem[256];
};

nIdは項目の識別子、szItemは項目の文字列、crTextは文字列の色、 crSelectは項目を選択したときの色です。 この構造体は、InitializeDataで初期化されています。 次のコードは、WM_CREATEの一部です。

InitializeData(&data[0], ID_COPY, RGB(0, 0, 255), RGB(255, 255, 0), TEXT("コピー"));
InitializeData(&data[1], ID_PASTE, RGB(0, 255, 255), RGB(128, 0, 128), TEXT("貼り付け"));

InitializeMenuItem(hmenuPopup, &data[0]);
InitializeMenuItem(hmenuPopup, &data[1]);

第1引数のdataの型はDATA構造体であり、静的変数として宣言されています。 以降の引数は、文字列、識別子、文字色、選択したときの色、となっています。 構造体を初期化したら、それを項目に関連付けることになります。

void InitializeMenuItem(HMENU hmenu, LPDATA lpData)
{
	MENUITEMINFO mii;
	
	mii.cbSize     = sizeof(MENUITEMINFO);
	mii.fMask      = MIIM_ID | MIIM_TYPE | MIIM_DATA;
	mii.fType      = MFT_OWNERDRAW;
	mii.wID        = lpData->nId;
	mii.dwItemData = (DWORD)lpData;
	
	InsertMenuItem(hmenu, lpData->nId, FALSE, &mii);
}

dwItemDataにlpDataのアドレスを指定します。 また、fTypeメンバにはオーナードローを示すMFT_OWNERDRAWを必ず指定します。 MFT_OWNERDRAWを指定したとき、関連するデータはdwTypeDataに設定すると リファレンスにはありますが、これは恐らく間違いでしょう。

case WM_DRAWITEM: {
	LPDATA           lpData;
	LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam;

	lpData = (LPDATA)lpdis->itemData;
		
	if (lpdis->itemState & ODS_SELECTED) {
		HBRUSH hbr;
			
		hbr = CreateSolidBrush(lpData->crSelect);

		FillRect(lpdis->hDC, &lpdis->rcItem, hbr);

		DeleteObject(hbr);
		
		SetBkMode(lpdis->hDC, TRANSPARENT);
	}
	else
		FillRect(lpdis->hDC, &lpdis->rcItem, (HBRUSH)GetStockObject(WHITE_BRUSH));
	
	SetTextColor(lpdis->hDC, lpData->crText);
		
	DrawText(lpdis->hDC, lpData->szItem, -1, &lpdis->rcItem, DT_CENTER | DT_VCENTER);

	return 0;
}

項目ごとの処理を行っていないのが分かります。 項目に関連付けられている構造体さえ取得すれば、 どの項目かを意識する必要などは全く不要になります。

lpData = (LPDATA)lpdis->itemData;

この一文が項目に関連付けられている構造体を取得するコードです。 itemDataメンバに有効なアドレスが設定されているのは、 InitMenuItemでMENUITEMINFO構造体のdwItemDataメンバを初期化したためです。

if (lpdis->itemState & ODS_SELECTED) {
	HBRUSH hbr;
			
	hbr = CreateSolidBrush(lpData->crSelect);

	FillRect(lpdis->hDC, &lpdis->rcItem, hbr);

	DeleteObject(hbr);
			
	SetBkMode(lpdis->hDC, TRANSPARENT);
}

itemStateメンバは項目の状態を表すメンバで、項目が選択されているときには、 ODS_SELECTEDフラグが含まれています。 FillRectでrcItemの範囲を塗りつぶせば項目の背景色は完成です。 SetBkModeで文字列を透過描画するようにしているのは、 DrawTextで実際の文字列を書くときに背景を上書きしないためです。

WM_MEASUREITEMの説明に移ります。 このメッセージで行うことは、いわばサイズ調整です。 メニューは項目を囲えるだけの幅と高さを持っていなければならず、 一般にはこれはシステムが適切に調整しているものと思われます。 しかし、オーナードローの場合、文字列をプログラムで描画することになるため、 文字列そのものや、長さ、太さ、というものをシステムが把握することができません。 よって、文字列の幅と高さを専用の構造体に指定して、 メニューのサイズを特定するヒントをシステムに与えるのです。 次のコードは、WM_MEASUREITEMの一部です。

hfontOld = (HFONT)SelectObject(hdc, hfontNC);
GetTextExtentPoint32(hdc, lpData->szItem, lstrlen(lpData->szItem), &size);

SelectObjectで項目の描画に使うフォントをデバイスとコンテキストに割り当て、 そのフォントにおける文字列のサイズをGetTextExtentPoint32で取得します。 フォントは、WM_CREATEにて作成しています。

NONCLIENTMETRICS ncMetrics;

ncMetrics.cbSize = sizeof(NONCLIENTMETRICS);
SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &ncMetrics, 0);	
hfontNC = CreateFontIndirect(&ncMetrics.lfMenuFont);

NONCLIENTMETRICS構造体は、メニューやタイトルバーに使われている情報を管理しています。 cbSizeメンバを初期化し、SystemParametersInfoを上記のように呼ぶことによって、 構造体が初期化されます。 NONCLIENTMETRICS構造体はフォント自体を含んでいるわけではないので、 フォントを作成するのはプログラムの役割です。

WM_MEASUREITEMの話に戻ります。 サイズの調整は、この部分です。

lpmis->itemWidth  = size.cx + 10;
lpmis->itemHeight = size.cy + 4;

lpmisは、MEASUREITEMSTRUCT構造体のアドレスです。 itemWidthはメニューの幅で、itemHeightは高さです。 +10や+4をしているのは、項目を限界に囲えるメニューにしたくないからです。 ある程度、余白を与えることにより、メニューがきれいに見えます。

WM_MEASUREITEMはメニューが初めて表示されようとするとき、 項目の数だけ送られます。 今回のプログラムの場合、項目が2つあるので、 初めて右クリックをしたときに2回、WM_MEASUREITEMが送られます。 それぞれの回のitemWidthとitemHeightの値は異なる可能性が有り得ますが、 表示されるメニューの大きさは、一番サイズの大きい項目を基準としているようです。

WM_MENUCHARについて

メニュー項目に(&F)のような文字を追加している場合は、 そのキーを押下することで項目を選択できることは既に説明してきました。 具体的にはこの動作はWM_MENUCHARがシステムによって処理されることで、 最終的にWM_COMMANDが発行されることになるのですが、項目の描画にオーナードローを 採用している場合は、プログラム自らがWM_MENUCHARを処理しなければなりません。 これは、GDIによって項目に描画された文字列をシステムが認識できないからであり、 仮に今回のメニューのコピーという項目に(&C)を付けていたとしても、 WM_MENUCHARを処理しなければCキーの押下は無意味なものになってしまいます。

case WM_MENUCHAR: {
	LRESULT lResult;

	if (LOWORD(wParam) == 'c') {
		lResult = MAKELONG(0, MNC_EXECUTE);
		return lResult;
	}
	else if (LOWORD(wParam) == 'p') {
		lResult = MAKELONG(1, MNC_EXECUTE);
		return lResult;
	}
	else
		break;
}

WM_MENUCHARのWPARAMの下位ワードには押下された文字が、 上位ワードにはメニューの属性が、LPARAMにはメニューのハンドルが返ります。 戻り値は上位ワード、下位ワードともに意味を持ちます。 MAKELONGというマクロは32ビット変数を作るマクロで、 第1引数を下位ワードに、第2引数を上位ワードにします。 下位ワードに項目の位置を指定し、上位ワードにMNC_EXECUTEを指定した場合、 WM_COMMANDが発行されることになります。 上位ワードをMNC_SELECTに指定した場合は、項目が選択されます。



戻る