オーナードローメニューの作製(後編)

 前編ではオーナードローメニューの流れを見てきました。後編では「いかにして普通っぽいオーナードローメニューを作製するか」という点を見ていきましょう。

 オーナードローメニューは基本的に「なんでもあり」ということになります。が、例え何でもアリだとしても、スタートメニューのような普通のメニューを作りたいときにもオーナードローメニューを使わざるを得ません。そこで、なんとか「普通」っぽく見せる必要があります。
 ここで言う「普通」とは、ふたつあります。ひとつは、「普通のメニュー」、もうひとつは「普通のアイコン付きメニュー」です。
 色々な点で、このふたつは微妙な違いがあります。スタートメニューひとつ取っても、「普通のメニュー」とは違います。アンダーラインの付いた文字がありませんし、サイズも若干違います。時には、文字がはみ出している時もあります。
 こういった部分は、さらにアプリケーションによって少しずつ違ってきます。つまり、「普通のアイコン付きメニュー」の中にもいくつかのバリエーションがあるということです。

 ここで紹介する方法は、筆者ができる限り「普通のメニュー」っぽく見せてみようと努力したものです。そのため、これまた「普通のアイコン付きメニュー」のバリエーションのひとつとなってしまいました。皆さんは、これから紹介する方法を参考にし、自分なりの方法を見つけてもらえればと思います。

OnMeasureItem()
 OnMeasureItem()では、メニューアイテムのサイズを計算することが目的です。単純に考えれば、横幅は「アイコンの幅+文字の幅+遊びのスペース」、高さは「アイコンの高さ」か「文字の高さ」の高い方ということになります。

 アイコンの幅と高さは、なぜか変わりません。スタートメニュー(の二段目以降)のアイコンは、今のところアイコンのサイズを変える方法を知りません。一応16x16のようです。ビットマップデータのアイコンは、サイズを変更するとそれだけ汚くなってしまいます。16x16ならそれで固定して描画する方がいいでしょう。

 さて、問題は文字のサイズです。CDCのメンバにGetTextExtent()という関数があります。これは、そのデバイスコンテキストでの文字のサイズを取得してくれます。この関数があれば文字のサイズは判ります。
 では、このデバイスコンテキストはどうやって取得すればいいのでしょうか? OnMeasureItem()では、この問題がいちばんやっかいです。

 解像度や色数などは、画面と同じです。ですから、::GetDC( ::GetDesktopWindow() )からコンパチブルなデバイスコンテキストを取得すればいいことになります(この部分のコードは最後の方で紹介します)。
 問題はフォントです。調べた結果、システムで使用しているフォントを直接取得するようなAPIは見つかりませんでした(が、見つかりました!! 詳しくは最後のUpdate Roomにて)。結局、レジストリから取得することにしました。HKEY_CURRENT_USER\control panel\desktop\windowmetricsにウィンドウズが使用しているフォントが設定されています。
 初めてメニューを開く場合には、こういったことが必要になると思われます。二度目以降は、OnDrawItem()で、メニューで使われている実際のデバイスコンテキストが取得できるので、そこからコンパチブルなデバイスコンテキストを取得すればいいでしょう。

 具体的なコードを下に書いておきます。


////////////////////////////////////////////////////////////////////
// メニューで使用されている標準のフォントを取得します。
BOOL KABMenuTree::GetMenuFontFromReg( LOGFONT &p_rstFont )
{
	LONG	lRes;	//エラー情報。
	HKEY	hKey;	//レジストリのキー。

	// HKEY_CURRENT_USER\control panel\desktop\windowmetricsへのアクセス。
	lRes = ::RegOpenKeyEx( HKEY_CURRENT_USER, _T( "control panel\\desktop\\windowmetrics" ),
							0, KEY_READ, &hKey );
	if( lRes != ERROR_SUCCESS )
	{
		TRACE0( "KABMenuTree::GetMenuFontFromReg() -- レジストリのキーを開けませんでした。\n" );
		return FALSE;
	}

	DWORD	dwBytes = 257;	//文字列のサイズ。
	DWORD	dwType;	//データのタイプ。
	char	buffer[257];	//レジストリの情報を格納するための文字列。

	// フォント情報を取得します。
	lRes = ::RegQueryValueEx( hKey, _T( "MenuFont" ), NULL, &dwType, 
							(unsigned char *)buffer, &dwBytes );
	if( lRes != ERROR_SUCCESS )
	{	
		TRACE0( "KABMenuTree::GetMenuFontFromReg() -- レジストリの値を取得できませんでした。\n" );
		return FALSE;
	}

	// フォントに標準のキャラセットを用います。
	p_rstFont.lfCharSet = DEFAULT_CHARSET;

	// 取得したフォント情報は、先頭1バイトにそのサイズが、10バイト目が2ならボールド、
	// 11バイト目が1ならイタリック、18文字以降にフォント名が格納されています。
	p_rstFont.lfHeight = (LONG)buffer[0] * 10;	//フォントサイズを取得します。

	if( (UINT)buffer[9] == 2 )	//ボールドかどうか。
		p_rstFont.lfWeight = 700;
	else
		p_rstFont.lfWeight = 400;

	if( (UINT)buffer[10] == 1 )	//イタリックかどうか。
		p_rstFont.lfItalic = TRUE;

	// フォント名を取得します。
	for( int fi1 = 0; buffer[fi1 + 18] != '\0'; fi1++ )
		p_rstFont.lfFaceName[fi1] = buffer[fi1 + 18];
	p_rstFont.lfFaceName[fi1] = '\0';

	::RegCloseKey( hKey );	//レジストリハンドルを閉じます。
	
	return TRUE;
}
	

 かなりべたべたな方法でフォントの情報を取得しています。本当はもう少しスマートな方法があると思うんですが……。この関数で得られたLOGFONT構造体CFont::CreatePointFontIndirect()を使って実際のフォントに変換し、先ほど作製した、ディスプレイにコンパチブルなデバイスコンテキストに割り当てればいいでしょう。

 以上のようにしてフォントをデバイスコンテキストに割り当てたら、GetTextExtent()を使って文字のサイズを計算します。これを元にメニューのサイズを作製してください。もちろん、見やすくするための「遊び」を入れておくことも重要です。これは適当でいいみたいです(同じマイクロソフト社製でもばらばら……)。

OnDrawItem()
 OnDrawItem()では、実際に描画するコードをインプリメントします。
 ここの重要な点は3つ。選択状態と非選択状態の切り替え、「&」付きテキストの書き込み、そしてアイコンの描画です。

 選択状態と非選択状態は、lpDIS->itemStatelpDIS->itemActionのふたつのフラグから判別します。また、各色は::GetSysColor()を用いて取得します。具体的なコードは次のようになります。


	CDC*	pMenuDC = CDC::FromHandle( lpDIS->hDC );	//書き込むデバイスコンテキスト。

	m_stHighLightColor = (COLORREF)::GetSysColor( COLOR_HIGHLIGHT );	//選択色。
	m_stHighLightTextColor = (COLORREF)::GetSysColor( COLOR_HIGHLIGHTTEXT );//その時の文字の色。
	m_stMenuColor = (COLORREF)::GetSysColor( COLOR_MENU );	//メニューの背景色。
	m_stMenuTextColor = (COLORREF)::GetSysColor( COLOR_MENUTEXT );	//メニューの文字の色。

	if( ( lpDIS->itemState & ODS_SELECTED ) &&
		( lpDIS->itemAction & ( ODA_SELECT | ODA_DRAWENTIRE ) ) )
	{
		cBGBrush.CreateSolidBrush( m_stHighLightColor );	//選択状態色をブラシに割り当てます。
		stOldColor = pMenuDC->SetTextColor( m_stHighLightTextColor );	//選択状態の文字描画色を設定します。
	}
	else if( !( lpDIS->itemState & ODS_SELECTED )&&
		( lpDIS->itemAction & ( ODA_SELECT | ODA_DRAWENTIRE ) ) )	//メニューが選択されていないとき。
	{
		cBGBrush.CreateSolidBrush( m_stMenuColor );	//メニューの背景色をブラシに割り当てます。
		stOldColor = pMenuDC->SetTextColor( m_stMenuTextColor );	//メニューの文字色を設定します。
	}
	

 このふたつのフラグはかなり細かく設定されていますが、別に「選択時」と「非選択時」に分ければそれで十分でしょう。

 次に文字の書き込みです。まずアイテムのサイズ(ポップアップメニュー全体からの論理矩形です。これが、選択時に反転する部分になります)はlpDIS->rcItemに入っているので、これをCRectに入れておきます(ここではcItemRectとします)。
 次にこのサイズを元に文字の描画部分を算出します。この部分は次のようにしました。


	// 中央に文字を描画します。表示はちょうど中央になるように(縦的に)。
	CSize	cTextSize;
	CPoint cTextPt;
	cTextSize = pMenuDC->GetTextExtent( cTextStr, cTextStr.GetLength() );
	cTextPt.x = cItemRect.left + m_uiLSpace + m_cMenuIconSize.cx + m_uiCSpace;
	cTextPt.y = cItemRect.top + ( cItemRect.Height() - cTextSize.cy ) / 2;
	

 cTextStrは書き込む文字です。また、m_uiLSpaceは左はしからアイコンまでのスペース、m_uiCSpaceはアイコンと文字との間のスペースです。上のようにして文字が書き込まれるスペースの左上角の座標を取得しています。

 次に実際の文字描画です。まず選択時の色で塗りつぶし、その上に文字を描画します。塗りつぶす関数はCDC::FillRect()を使用します。文字の方はCDC::DrawState()を使用します。このDrawState()は、CDC::TextOut()と違い、非選択状態や点線、そしてアンダーラインの付いた文字列を描画できます。つまり、アクセラレータ文字を描画できるというわけです。実際のコードは次のようになります。


	pMenuDC->FillRect( &cItemRect, &cBGBrush );	//背景を塗りつぶします。
	pMenuDC->DrawState( cTextPt, cTextSize, (LPCTSTR)cTextStr, DST_PREFIXTEXT | DSS_NORMAL,
							TRUE, 0, (CBrush *)NULL );
	

 ただし、オーナードローメニューですからアクセラレータに対する機能は自分でインプリメントしなければなりません。現に、オーナードローメニューでアクセラレータを使っているものは少ないようなので、別に使わなくてもいいでしょう。
 あと、この関数を使えば選択不可状態も描画できます。が、いまいち選択不可状態のカラーが判らない(ウィンドウズが自動的に作製してしまうため)ので、ここでは紹介しません(汗)。おそらく3Dオブジェクトの明るい部分だと思うんですが、ときどき全然違う色だったり、点線だったりします。謎です。

 最後に、アイコンの描画です。アイコンをデバイスコンテキストに描くにはCDC::DrawIcon()を使用します。が、この関数、なぜか必ず32x32で描画してしまいます。そのため、まずメモりデバイスコンテキストを取得して、次にそのメモリDCにアイコンを描画して、最後にCDC::StretchBlt()を用いて拡大縮小コピーを行います。

 まずメモリDCの作製です。与えられているメニューのデバイスコンテキストにコンパチブルなメモリDCを作製します。


////////////////////////////////////////////////////////////////////
// メモリDCを初期化します。
BOOL KABMenuTree::MemDCInit( HDC p_hMenuDC, CFont *p_pcMenuFont, CDC &p_rcMemDC, CBitmap &p_rcMemBmp )
{
	CBitmap *pOldBmp;
	CDC cMenuDC;
	cMenuDC.Attach( p_hMenuDC );

	p_rcMemDC.CreateCompatibleDC( &cMenuDC );	//メニューDCにコンパチブルなDCを作製します。
	p_rcMemBmp.CreateCompatibleBitmap( &cMenuDC, 32, 32 );	//メニューDCにコンパチブルなBMPを作製します。
	pOldBmp = p_rcMemDC.SelectObject( &p_rcMemBmp );	//メモリDCにBMPを割り当てます。

	pOldBmp->DeleteObject();
	cMenuDC.Detach();

	p_rcMemDC.SelectObject( p_pcMenuFont );	//メモリDCにフォントをセットします。

	return TRUE;
}
	

 第1引数は、元となるデバイスコンテキストのハンドルです。第2引数は、OnMeasureItem()で作製したフォントです。第3引数は作製されたメモリDCを返すための参照です。メモリDCを作製するときにはビットマップも作製しなければならないため、そのビットマップも返します。それが第4引数です。
 この関数を使えば、最初のOnMeasureItem()で文字サイズを得るためのデバイスコンテキストも作製できます。ってゆーか、その時に作ったメモリDCをそのままアイコン描画用にすればいいでしょう。

 次に、このデバイスコンテキストの背景を塗りつぶしてから、アイコンを描画します。


	// メモリDCにアイコンを描画してから、メニューのDCに大きさを変えてコピーします。
	CRect	cMemRect( 0, 0, 32, 32 );	//アイコンのサイズです

	m_cMemDC.FillRect( &cMemRect, &cBGBrush );	//背景を塗りつぶします。
	cBGBrush.DeleteObject();	//背景用ブラシを解放します。
	m_cMemDC.SetMapMode( MM_TEXT );	//画面描画モードにセットします(1単位=1ピクセル)。
	m_cMemDC.DrawIcon( 0, 0, hMenuIcon );	//アイコンを描画します(絶対に32×32)。
	

 メモリDCにアイコンを描画したら、大きさを変えてメニューに貼り付けます。張り付けはCDC::StretchBlt()を使用します。


	pMenuDC->StretchBlt( cItemRect.left + m_uiLSpace, 
		cItemRect.top + ( cItemRect.Height() - m_cMenuIconSize.cy ) / 2,
		15, 15, &m_cMenuDC, 0, 0, 31, 31, SRCCOPY );
	

 以上で、駆け足でしたが「よく使われてるアイコン付きメニュー」に似せたメニューを作製する方法を見てきました。ただ、今では多くのアプリケーションが使用していること、にも関わらずフォーマットが整っていないことを考えると、ウィンドウズ98では標準APIで装備できるようになるかもしれません。
 あと、もちろん今回は「標準っぽい」のを狙っていましたが、別に標準でなくてもいいでしょう。ビットマップだけのメニューなど、色々夢のある分野です。かといっても、やたら変なのは歓迎されないでしょう。もし、無茶苦茶変なメニューを作るのだとしたら、それはメニューとしてではなく普通のポップアップウィンドウとして作った方がいいのではないかと……。

Update Room
 メニューのフォントを取得する方法が見つかったのでお知らせします。


	NONCLIENTMETRICS	stNCMetrics;
	stNCMetrics.cbSize = sizeof( NONCLIENTMETRICS );

	::SystemParametersInfo( SPI_GETNONCLIENTMETRICS, sizeof( NONCLIENTMETRICS )
		, &stNCMetrics, 0 );
	

 このようにSystemParametersInfo()というAPIを使用します。SPI_GETNONCLIENTMETRICSフラグを立てて呼び出すと、NONCLIENTMETRICSlfMenuFontメンバにメニューのフォントが入っています。ちゃんとこーゆーふーに取れるもんなんですね……。
(C)KAB-studio 1997 ALL RIGHTS RESERVED.