EternalWindows
メニュー / ビットマップメニュー

Windows Vista以前のWindowsでは、メニュー項目にビットマップを表示する方法としてオーナードローが主流でした。 この最大の理由は、ビットマップを表示するための標準的なメカニズムが存在しなかったことに尽きますが、 Windows Vistaからはこの点が改善されています。 具体的には、MENUITEMINFO構造体のhbmpItemに32ビットのビットマップを指定することで、 メニュー項目の横にビットマップを表示することができるようになったのです。 正確にはWindows XPでもhbmpItemに指定したビットマップは表示されるのですが、 背景が透過されなかったり、選択時に色が反転したりと不具合が見られます。

今回のプログラムは、メニュー項目にビットマップを設定します。 SHGetStockIconInfoという関数を呼び出す関係上、Windows Vista以降で実行することになります。

#include <windows.h>

#define ID_FILE 10
#define ID_SAVE 20
#define ID_EXIT 30

void SetMenuItemFromIcon(HMENU hmenu, int nId, HICON hicon);
HBITMAP CreateBitmapARGB(int nWidth, int nHeight);
void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId, HMENU hmenuSub);
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)
{
	switch (uMsg) {

	case WM_CREATE: {
		HMENU           hmenu;
		HMENU           hmenuPopup;
		MENUINFO        menuInfo;
		SHSTOCKICONINFO sii;
		
		hmenu  = CreateMenu();
		hmenuPopup = CreatePopupMenu();

		InitializeMenuItem(hmenuPopup, TEXT("保存(&S)"), ID_SAVE, NULL);
		InitializeMenuItem(hmenuPopup, NULL, 0, NULL);
		InitializeMenuItem(hmenuPopup, TEXT("終了(&X)"), ID_EXIT, NULL);
		InitializeMenuItem(hmenu, TEXT("ファイル(&F)"), ID_FILE, hmenuPopup);
		SetMenu(hwnd, hmenu);

		sii.cbSize = sizeof(SHSTOCKICONINFO);
		SHGetStockIconInfo(SIID_SHIELD, SHGFI_ICON | SHGFI_SMALLICON, &sii);
		SetMenuItemFromIcon(hmenuPopup, ID_SAVE, sii.hIcon);
		
		menuInfo.cbSize = sizeof(MENUINFO);
		menuInfo.fMask = MIM_STYLE; 
		GetMenuInfo(hmenuPopup, &menuInfo);
		menuInfo.dwStyle = (menuInfo.dwStyle & ~MNS_NOCHECK) | MNS_CHECKORBMP;
		SetMenuInfo(hmenuPopup, &menuInfo);

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

		if (nId == ID_SAVE)
			MessageBox(hwnd, TEXT("保存が選択されました。"), TEXT("OK"), MB_OK);
		else if (nId == ID_EXIT)
			PostMessage(hwnd, WM_CLOSE, 0, 0);
		else
			;
		
		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

void SetMenuItemFromIcon(HMENU hmenu, int nId, HICON hicon)
{
	UINT         uWidth, uHeight;
	HDC          hdcMem;
	HBITMAP      hbmp, hbmpPrev;
	MENUITEMINFO mii;

	uWidth = GetSystemMetrics(SM_CXSMICON);
	uHeight = GetSystemMetrics(SM_CYSMICON);
	hbmp = CreateBitmapARGB(uWidth, uHeight);
	
	hdcMem = CreateCompatibleDC(NULL);
	hbmpPrev = (HBITMAP)SelectObject(hdcMem, hbmp);
	DrawIconEx(hdcMem, 0, 0, hicon, uWidth, uHeight, 0, NULL, DI_NORMAL);
	SelectObject(hdcMem, hbmpPrev);
	DeleteDC(hdcMem);

	mii.cbSize = sizeof(MENUITEMINFO);
	mii.fMask = MIIM_BITMAP;
	mii.hbmpItem = hbmp;
	SetMenuItemInfo(hmenu, nId, FALSE, &mii);	
}

HBITMAP CreateBitmapARGB(int nWidth, int nHeight)
{
	LPVOID           lpBits;
	BITMAPINFO       bmi;
	BITMAPINFOHEADER bmiHeader;

	ZeroMemory(&bmiHeader, sizeof(BITMAPINFOHEADER));
	bmiHeader.biSize      = sizeof(BITMAPINFOHEADER);
	bmiHeader.biWidth     = nWidth;
	bmiHeader.biHeight    = nHeight;
	bmiHeader.biPlanes    = 1;
	bmiHeader.biBitCount  = 32;

	bmi.bmiHeader = bmiHeader;
	
	return CreateDIBSection(NULL, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS, &lpBits, NULL, 0);
}

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId, HMENU hmenuSub)
{
	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;

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

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

WM_CREATEで呼び出しているSetMenuItemFromIconという自作関数は、アイコンのハンドルからビットマップを取得し、 それをメニュー項目に設定する関数です。 第1引数は項目を含むメニューハンドルであり、第2引数はビットマップを設定したい項目のIDです。 第3引数はアイコンのハンドルであり、これはSHGetStockIconInfoで取得しています。 この関数はシステムで用意されているアイコンを返す関数であり、 第1引数にSIID_SHIELDを指定した場合はシールドアイコンを取得できます。 今回はアイコンをハンドルとして取得したいので、第2引数にはSHGFI_ICONを指定し、 さらに小さいアイコンとして取得するためにSHGFI_SMALLICONを指定しています。

SetMenuItemFromIconの目的は、アイコンのピクセルを格納したビットマップハンドルをSetMenuItemInfoに指定することです。

void SetMenuItemFromIcon(HMENU hmenu, int nId, HICON hicon)
{
	UINT         uWidth, uHeight;
	HDC          hdcMem;
	HBITMAP      hbmp, hbmpPrev;
	MENUITEMINFO mii;

	uWidth = GetSystemMetrics(SM_CXSMICON);
	uHeight = GetSystemMetrics(SM_CYSMICON);
	hbmp = CreateBitmapARGB(uWidth, uHeight);
	
	hdcMem = CreateCompatibleDC(NULL);
	hbmpPrev = (HBITMAP)SelectObject(hdcMem, hbmp);
	DrawIconEx(hdcMem, 0, 0, hicon, uWidth, uHeight, 0, NULL, DI_NORMAL);
	SelectObject(hdcMem, hbmpPrev);
	DeleteDC(hdcMem);

	mii.cbSize = sizeof(MENUITEMINFO);
	mii.fMask = MIIM_BITMAP;
	mii.hbmpItem = hbmp;
	SetMenuItemInfo(hmenu, nId, FALSE, &mii);	
}

GetSystemMetricsで小さいアイコンのサイズを取得し、それを基にCreateBitmapARGBという自作関数を呼び出しています。 この関数は32ビットのビットマップをDIBセクションとして作成し、そのハンドル返します。 ただし、ビットマップのハンドルだけではそのビットマップにアイコンのピクセルを格納できないため、 SelectObjectでメモリデバイスコンテキストに選択するようにしています。 このメモリデバイスコンテキストに対してDrawIconExを呼び出せば、 選択されているビットマップにアイコンが描画されることになり、 ビットマップにはアイコンのピクセルが格納されたことになります。 後はこれをMENUITEMINFO構造体のhbmpItemに指定してSetMenuItemInfoを呼び出せば、 メニュー項目にビットマップが設定されることになります。

既定のメニューでは、項目の左横にビットマップとチェックマークの2つのスペースが用意されますが、 今回のようにビットマップだけを表示するつもりの場合は、1つのスペースだけで十分といえます。 このような場合は、メニュー項目ではなくメニュー自体の情報を設定するSetMenuInfoを呼び出し、 メニューのスタイルを変更することになります。

menuInfo.cbSize = sizeof(MENUINFO);
menuInfo.fMask = MIM_STYLE;
menuInfo.dwStyle = MNS_CHECKORBMP;
SetMenuInfo(hmenuPopup, &menuInfo);

fMaskにMIM_STYLEを指定すると、dwStyleを初期化できるようになります。 MNS_CHECKORBMPを指定した場合は項目の横のスペースが1つだけになり、 そこにビットマップかチェックマークを表示できるようになります。

WICの使用

今回はGDIによってアイコンのピクセルをビットマップに格納しましたが、 Windows Vistaから登場したWIC(Windows Imaging Component)を使用した方法も有名です。 次に例を示します。

#include <windows.h>
#include <wincodec.h>

#define ID_FILE 10
#define ID_SAVE 20
#define ID_EXIT 30

#pragma comment(lib, "Windowscodecs.lib")

void SetMenuItemFromIcon(HMENU hmenu, int nId, HICON hicon, IWICImagingFactory *pWICFactory);
HBITMAP CreateBitmapARGB(int nWidth, int nHeight, LPBYTE *lplpBits);
void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId, HMENU hmenuSub);
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 IWICImagingFactory *pWICFactory;

	switch (uMsg) {

	case WM_CREATE: {
		HRESULT         hr;
		HMENU           hmenu;
		HMENU           hmenuPopup;
		MENUINFO        menuInfo;
		SHSTOCKICONINFO sii;

		CoInitialize(NULL);

		hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pWICFactory));
		if (FAILED(hr))
			return -1;
		
		hmenu  = CreateMenu();
		hmenuPopup = CreatePopupMenu();

		InitializeMenuItem(hmenuPopup, TEXT("保存(&S)"), ID_SAVE, NULL);
		InitializeMenuItem(hmenuPopup, NULL, 0, NULL);
		InitializeMenuItem(hmenuPopup, TEXT("終了(&X)"), ID_EXIT, NULL);
		InitializeMenuItem(hmenu, TEXT("ファイル(&F)"), ID_FILE, hmenuPopup);
		SetMenu(hwnd, hmenu);

		sii.cbSize = sizeof(SHSTOCKICONINFO);
		SHGetStockIconInfo(SIID_SHIELD, SHGFI_ICON | SHGFI_SMALLICON, &sii);
		SetMenuItemFromIcon(hmenuPopup, ID_SAVE, sii.hIcon, pWICFactory);
		
		menuInfo.cbSize = sizeof(MENUINFO);
		menuInfo.fMask = MIM_STYLE; 
		GetMenuInfo(hmenuPopup, &menuInfo);
		menuInfo.dwStyle = (menuInfo.dwStyle & ~MNS_NOCHECK) | MNS_CHECKORBMP;
		SetMenuInfo(hmenuPopup, &menuInfo);

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

		if (nId == ID_SAVE)
			MessageBox(hwnd, TEXT("保存が選択されました。"), TEXT("OK"), MB_OK);
		else if (nId == ID_EXIT)
			PostMessage(hwnd, WM_CLOSE, 0, 0);
		else
			;
		
		return 0;
	}

	case WM_DESTROY:
		if (pWICFactory != NULL)
			pWICFactory->Release();
		CoUninitialize();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

void SetMenuItemFromIcon(HMENU hmenu, int nId, HICON hicon, IWICImagingFactory *pWICFactory)
{
	UINT         uWidth, uHeight;
	UINT         uLineSize, uBufferSize;
	LPBYTE       lpBits;
	HBITMAP      hbmp;
	IWICBitmap   *pWICBitmap;
	MENUITEMINFO mii;
		
	pWICFactory->CreateBitmapFromHICON(hicon, &pWICBitmap);
	pWICBitmap->GetSize(&uWidth, &uHeight);

	hbmp = CreateBitmapARGB(uWidth, uHeight, &lpBits);
	uLineSize = uWidth * 4;
	uBufferSize = uLineSize * uHeight;

	pWICBitmap->CopyPixels(NULL, uLineSize, uBufferSize, lpBits);
	pWICBitmap->Release();

	mii.cbSize = sizeof(MENUITEMINFO);
	mii.fMask = MIIM_BITMAP;
	mii.hbmpItem = hbmp;
	SetMenuItemInfo(hmenu, nId, FALSE, &mii);	
}

HBITMAP CreateBitmapARGB(int nWidth, int nHeight, LPBYTE *lplpBits)
{
	BITMAPINFO       bmi;
	BITMAPINFOHEADER bmiHeader;

	ZeroMemory(&bmiHeader, sizeof(BITMAPINFOHEADER));
	bmiHeader.biSize      = sizeof(BITMAPINFOHEADER);
	bmiHeader.biWidth     = nWidth;
	bmiHeader.biHeight    = -nHeight;
	bmiHeader.biPlanes    = 1;
	bmiHeader.biBitCount  = 32;

	bmi.bmiHeader = bmiHeader;
	
	return CreateDIBSection(NULL, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS, (void **)lplpBits, NULL, 0);
}

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId, HMENU hmenuSub)
{
	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;

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

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

WICを使用するには、CoCreateInstanceにCLSID_WICImagingFactoryを指定してIWICImagingFactoryを取得します。 SetMenuItemFromIconではまず、IWICImagingFactory::CreateBitmapFromHICONを呼び出して、 アイコンのハンドルからIWICBitmapを取得します。 IWICBitmap::GetSizeを呼び出せばアイコンのサイズを取得できるため、 その大きさを持った空のビットマップをCreateBitmapARGBで作成します。 CreateBitmapARGBがビットイメージを返している理由は、 アイコンのピクセルをビットマップにコピーするために使用するIWICBitmap::CopyPixelsが、 ビットマップのハンドルではなくビットイメージを表すバッファを要求するからです。 CopyPixelsを呼び出すには、バッファの一行に要するサイズとバッファ全体のサイズが必要になるため、 それぞれをuLineSizeとuBufferSizeに格納しておきます。 uLineSizeに4を掛けているのは、1つの色がARGBの4バイトで表されるからです。 CopyPixelsが成功すれば第4引数のバッファにピクセルが格納され、 これはDIBセクションのビットイメージでしたから、 ビットマップハンドルであるhbmpも更新されたことになります。



戻る