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

 ウィンドウズを初めて使ったとき、おそらく一番最初に「スタートボタン」を押したことでしょう。「スタートボタン」からツリー表示されていくメニューにはアイコンが表示されています。普通のメニューよりも、見た目が綺麗で、かつ分かりやすくなっています。
 では、このメニュー、どうやって作製するのでしょうか。

 今回は、この機能を実現する「オーナードロー」というシステムを見ていきます。メニューを追加する部分はメニューを動的に追加すると同じ方法で行います。必ず「メニューを動的に……」を読んでから、このページを読んでください。

 では下準備をしましょう。まず、メニュー関係をコーディングするクラス(ここではフレームウィンドウクラス(ここ修正してください))の直前に、次のような構造体のコードを書き込んでください。


//////////////////////////////////
// ODMENUITEM構造体:AppendMenu等で渡す、メニューアイテムについての情報。
typedef struct _ODMENUITEM
{
	HICON hMenuIcon;	//メニューに貼り付けるアイコンのハンドル。
	CString cTextStr;	//メニューに書き込む文字。
} ODMENUITEM;
	

 この構造体の配列をメンバ変数として追加します。ここではm_stMenuItem[32]とします。
 次に、メニューを追加します。メニューを動的に追加するとほとんど同じコーディングをしてください(ってゆーか、このとき作ったプログラムを流用した方が早いかも)。
 ただし、メニューアイテムを追加するAppendMenu()の部分だけは、少し違います。実際には


	if( pcThisMenu && pcThisMenu->GetMenuItemID( 0 ) == ID_ADDMENU )
	{
		// メニューアイテムの情報を格納します。
		m_stMenuItem[0].cTextStr = "あうあう";
		m_stMenuItem[0].hMenuIcon = ;AfxGetApp()->LoadIcon( IDR_MAINFRAME );
		
		// メニューを追加します。
		pcThisMenu->AppendMenu( MF_OWNERDRAW, ID_NEWMENU, (LPCTSTR)0 );
	}
	

 オーナードローを使うため、1番目の引数にはMF_OWNERDRAWを渡します。2番目は同じです。3番目は、通常は書き込む文字ですが、オーナードローでは32ビットの整数を渡します。ここで渡している「0」は、配列のインデックスです。
 この32ビット整数というのは、言ってみればオーナードローメニューの整理番号です。メニュー描画を行うときに、この整数が渡されます。その整数を元に、今描画しようとしているアイテムがどのメニューアイテムなのかを調べるわけです。
 ここではひとつだけしかアイテムを作りませんが、通常は複数作るでしょう。そういうときにはもちろん配列のインデックスを変えて追加していきます。そういうことを考えれば、単なる配列ではなくCArrayCMapなどを使う方がいいでしょう。

 さて、ここからが、実際にオーナードローメニューを描画する部分です。
 オーナードローメニューを作製した直後には、描画は行いません。行うのは、実際にメニューが表示される直前からメニューが閉じるまでの間です。

 どんなアイテムも、オーナードローと指定した場合にはWM_MEASUREITEMWM_DRAWITEMが送られます。このメッセージを処理するため、フレームウィンドウクラスにこれらふたつのメッセージに関連づけされた関数をクラスウィザードを使って作製してください。CWndクラスのメンバ関数OnMeasureItem()OnDrawItem()が作製されます。このふたつの関数の中に、オーナードローのための機能をインプリメントします。

<バグリポート>
 この部分、以前は「ビュークラスで」と書いていましたが、これは誤りでした!! 「フレームウィンドウクラスで」に修正します。 苦悩していた皆様、申し訳ありませんでした!!

 上記のふたつのメッセージは、メニューのメッセージです。そのため、普通のメッセージとは少し違い、メニューを管理するウィンドウに送られてきます。
 ここで、「メニューを動的に変更する」を見てください。AfxGetMainWnd() を使ってトップウィンドウ、つまりメインフレームウィンドウを取得して、そのウィンドウクラスの持つメニューを操作しています。つまり、フレームウィンドウがメニューを管理しているというわけです。
 となれば、当然上記の2メッセージはメインフレームウィンドウへと送られるわけで、ハンドラ関数もフレームウィンドウクラスに作らなければならないというわけです。

 これは、プログラムテストの時にはフレームウィンドウで行っていたものを、このページを書いたときに「こっちの方が解りやすいかな」とテストもせずに書き換えたため起きたミスでした。本当に申し訳ありませんでした!!

 最後に、このレポートをしていただいた甲斐様、ありがとうございました!!

(1998/6/23)

 まず、この関数の中に、親クラスの同じメンバ関数を呼び出す部分があると思いますが、この部分を削除してください。この関数は、単に「そのアイテムのクラス」を探し出してそこにメッセージを配送するだけです。例えば、この場合ではCMenuMeasureItem()メンバ関数が呼び出されます。今回は、これをされても困ってしまうので削除しましょう。
(注:たとえ、独自にCMenuの派生クラスを作っていたとしても、これらの関数を使わず、この場で直接呼んでしまうのがいいでしょう。というか、そうしなければいけません。CWnd::OnMeasureItem()は、メニューIDからクラスを取得するという方法を採っています。当然、ポップアップメニューの場合にはこれは失敗し、クラスの関数は呼び出されません。これを回避するためにというわけです。
 ちなみにOnDrawItem()の場合には、メニューハンドルから直接取得しているので大丈夫です。
 これは、各メッセージで渡される構造体の中身が違うことが原因です。そういう点では、Visual C++は悪くないがWindows95の仕様そのものが間違っているとでも言えましょうか。まぁ、結局は……)

 ここで、このふたつの関数の役割について説明します。
 OnMeasureItem()では、メニューアイテムの横幅と高さを設定します。これまでは自動的にウィンドウズ95が設定していたものを、オーナードローでは自分で設定しなければならないということです。
 この関数はアイテム数だけまず呼び出されます。つまり、オーナードローのアイテムが3つあれば、メニューを開いたときにまず3回この関数が呼び出されるということです。
 基本的に高さは自由です。各アイテム好きなサイズを選択できます。横幅は、最も大きいものに自動的に合わせられます。もちろん、オーナードローアイテム以外のメニューアイテムが最も大きい横幅を持っていれば、その幅にメニュー全体が合わせられるということです。

 OnDrawItem()では、実際に描画する段階で呼び出されます。この「描画する段階」というのは何度となく訪れます。まずメニューを開いたときに(OnMeasureItem()のあとで)オーナードローメニューアイテムの数だけ呼び出されます。
 次に、アイテムを選択状態にしたときに呼び出され、選択状態を解除したときにも呼び出されます。つまり、あるアイテムを選択している状態で、一つ下へと選択を移したときには2回関数が呼ばれるということです。

 では、実際にコーディングしてみましょう。


////////////////////////////////////////////////////////////////////
// CMainFrameのオーナードロー用メンバ関数。
void CMainFrame::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) 
{
	lpMeasureItemStruct->itemWidth = 300;	//横幅です。
	lpMeasureItemStruct->itemHeight = 100;	//高さです。
}


void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) 
{
	// 書き込むデバイスコンテキストを準備します。
	CDC*	pcMenuDC = CDC::FromHandle( lpDrawItemStruct->hDC );

	CRect	cItemRect( lpDrawItemStruct->rcItem );	//項目の領域です。
	CString	cStr( m_stMenuItem[lpDrawItemStruct->itemData].cTextStr );	//書き込む文字です。

	pcMenuDC->TextOut( cItemRect.left, cItemRect.top, cStr );	//文字を描画します。
}
	

 OnMeasureItem()で渡される構造体のポインタ、そのitemWidthにはメニューアイテムの横幅を、itemHeightには高さを格納して、関数を終了させます。こうすることで、ウィンドウズがこのサイズでメニューを作ってくれます。
 OnDrawItem()で渡される構造体のポインタ、そのhDCには、そのメニューの表面に貼り付けられているデバイスコンテキストのハンドルが入っています。これを使ってメニューを描画します。さらに、構造体からアイテムにあたる四角形と、AppendMenu()で渡した配列のインデックスを取得して、これらを元に文字を描画します。四角形の右上角は、ほとんどの場合0ではありません。これはつまり、渡されるデバイスコンテキストがメニュー全体だということです。スタートメニューの上へと伸びる帯は、このことを利用して描画しているのでしょう。

 ここまでが大まかな流れです。基本的にはこれだけあれば自分の好きなようにメニューを変えられます。
 ですが、実際には「あまり変なメニューにしたくない」のではないでしょうか。「後編」では、オーナードローメニューをできるだけ「普通」っぽく、単にアイコンが着いただけのように見せる方法を紹介します。

(C)KAB-studio 1997, 1998 ALL RIGHTS RESERVED.