メニューを動的に追加する(後編)

 前編では、動的にメニューを追加する簡単な方法を見ていきました。まだ見てない方はまずそちらを見て欲しいです。

 さて、その「前編」の方法では、いくつか問題点が存在します。ネットスケープのツリー表示と比べてみれば歴然ですね。
 まず、メニューがたくさん増えていく場合、「前編」のように別にメニューを作成しておくというのは現実的に不可能です。さらに、そのメンバ関数を関連づけておくことも不可能でしょう。第一、追加されるメニューが全く不定のものなら、それらを行うことも不可能です。

 ということで、「後編」では1万個近く自由にメニューを作成できるようなアプリケーションの作成方法を見ていきます。


 アプリケーションのベースは前回のままでいいでしょう。
 まず初めに、ただひたすらメニューを追加する方法を考えてみましょう。

 まず、メニューの元になる配列を作っておきましょう。単純なメッセージの羅列を用意します。今回はCStringArrayクラスを使用します。
 下準備として、ドキュメントクラスにCStringArray型のメンバ変数m_cMsgStrAryを作成します。
 次にビュークラスのOnInitialUpdateメンバ関数のハンドラを作成します。ビュークラスの.cppファイルを開いて、オブジェクトはそのまま、IDをOnInitialUpdateにして新しい関数を作成します。その中に、まず次のようなコーディングをしてください。


void CKABView::OnInitialUpdate()
{
	CView::OnInitialUpdate();
	
	////////////
	// メニューを動的に作成します。
	
	// 面倒くさいので、最初からデータを入れておきます。
	CKABDoc* pDoc = GetDocument();	// ドキュメントを取得しておきます。
	pDoc->m_cMsgStrAry.Add( _T( "いちばんめ" ) );
	pDoc->m_cMsgStrAry.Add( _T( "にばんめ" ) );
	pDoc->m_cMsgStrAry.Add( _T( "さんばんめ" ) );
	// つづく。
}
	

 ビュークラスに割り当てられているドキュメントへのポインタを取得してから、メンバ変数にアクセスして最初にメニューの項目を作ってしまいます。

(注:ドキュメントクラスにこのような設定をしたのは、いちばん一般的な方法かなと思ったからです。ネットスケープのブックマークはこれに近いと思います。
 ですが、メニューを新たに作成する場合には、ドキュメントごとに違うほかにもアプリケーションごとに違う場合や、それとも全く違う場合もあります。
 ドキュメントクラス以外の方法として、1つはレジストリやiniファイルを使う方法があります。CWinAppクラスWriteProfileStringメンバ関数を使用したり、CRecentFileクラス#include <afxadv.h>を忘れずに)を使用したりするのが簡単な方法です。
 もうひとつはインターネットエクスプローラー3.0のようにファイル形式にしてしまう方法がありますが、これはWin32APIを使わなければならないので面倒だと思います。
 いずれにせよ、いちばん適していると思う方法を選んでみてください。ドキュメントクラスを使うのは単なる一例です。)

 さて、次に、前回の「簡単にメニューを作る方法」を付け足します。


	// つづき。

	///////////////
	// メニューを加えます。
	// トップメニューを取得します。
	CMenu* pcTopMenu = AfxGetMainWnd()->GetMenu();
	// サブメニューを取得します。
	CMenu* pcThisMenu = pcTopMenu->GetSubMenu( 2 );
	// メニューを追加します。
	pcThisMenu->AppendMenu( MF_STRING, ID_NEWMENU, _T( "追加" ) );
	

 これは前回のものそのままなので、変更しなければいけません。もともとあるメニューは同じなので、サブメニューを取得するところまでは同じです。
 もし、最初のひとつだけメニューを加えるとしたら、「メニューを追加します」の次の行を以下のように変えるでしょう。


	pcThisMenu->AppendMenu( MF_STRING, ID_NEWMENU_0001, pDoc->m_cMsgStrAry[0] );
	

 こうすれば「いちばんめ」というメニューが作成されます。が、問題はメッセージID「ID_NEWMENU_0001」です。
 前回のように新しくメニューを作成して、そのメニューアイテムにID_NEWMENU_0001を割り当ててしまったらなんの意味もありません。
 が、そのままではID_NEWMENU_0001はなんの定義もされていないので、メッセージだけ作成しておきます。VC++のメニュー「表示」−「シンボルブラウザ」を選択してください。シンボル一覧のダイアログが現れます。
 そうしたら、ID_NEWMENU_0001というメッセージをシンボルを作成して、整数値は36001くらいにでもしておいてください。

 さて、ここでメッセージがなんなのか、シンボルがなんなのか考えてみると……ただの整数値です。その証拠にresource.hファイルを開いてみましょう。中に


#define ID_NEWMENU_0001                 36001
	

 というふうに書かれているはずです。つまり、メッセージはただの整数値だということです。

 そこで、先ほどのメニュー追加の部分を次のように書いてしまいます。


	// メニューを追加します。
	for( int i = 0; i <= 2; i++ )
		pcThisMenu->AppendMenu( MF_STRING, ID_NEWMENU_0001 + i, pDoc->m_cMsgStrAry[i] );
	

 これで、「にばんめ」のメニューアイテムに ID_NEWMENU_0001 + 1、つまり36002が、「さんばんめ」には36003が割り当てられました。これで、無数にメッセージを割り当てることができるようになります。

(注:もちろん「無数に」は比喩ですが、それより前に、シンボルが重ならないようにする必要があります。
 どういうことかというと、もし非常に多くのメッセージが割り当てられるとすると、40000とかまで増えることがあるかもしれません。もし3600040000の間に他の定義済みのメッセージがあったらどうなるでしょうか。あとで説明しますが、最初に定義したメッセージが優先されます。ブックマークからひとつ選択したらそれが「終了」のメッセージでアプリケーションが終了してしまうかもしれないのです!
 そうならないよう、シンボルは慎重に定義してください。無数に作成するメッセージはできるだけ大きい値を割り当てておき、他のメニューのメッセージは少ない値を割り当てておきましょう。)

 さて、ここまで作成してからビルドをしてみてください。成功すればメニューが自動的に作成されているはずです……淡色表示で
 さらに、まだメッセージに対するハンドラは作成してないのですから、淡色表示でなくてもなんの意味もありません。
 というわけで、最後にメッセージに対応する方法を見ていきます。

(ここから下を省略できます。詳しくは最後の
Update Roomを見てね<なんかプレゼントの応募みたい(汗))

 メッセージに対してプログラム上で反応させる場合、OnCmdMsgというメンバ関数を使います。これはCCmdTargetクラスのメンバ関数ですが、CViewCDocumentCFrameWndのようなメッセージを処理できるクラスはたいがい基底クラスにCCmdTargetクラスを持っています。ですから、そのままアプリケーションのビュークラスのメンバ関数としてオーバーライドできます。

 実際にしてみましょう。ビュークラスを開いて、オブジェクトIDはそのまま、メッセージにOnCmdMsgを選んで新しくハンドラを作成します。そうすれば、OnCmdMsgメンバ関数が新たに作成されるはずです。
 では、実際にコーディングをしてみましょう。


BOOL CKABView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) 
{
	if( pHandlerInfo == NULL )	// すでに割り当てられているメッセージがない場合……
	{
		// この部分にあとで追加します。
	
	}

	//  コマンドを処理しなかった場合には基本クラスの同名の関数を呼び出します。
	return CView::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
}
	

 pHandlerInfoという引数は、そのメッセージがすでに関連づけられているか、つまり、同ファイルの上の方のメッセージマップ上に


BEGIN_MESSAGE_MAP(CT_Menu1View, CView)
	//{{AFX_MSG_MAP(CT_Menu1View)
	ON_COMMAND(ID_ADDMENU, OnAddmenu)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()
	

 てなふうに書かれているか(ヘッダーファイルにも書き込む必要がある)という情報が入っています。
 これがNULLということは、関連づけられていないということです。逆に関連づけられている場合にはこのifに引っかからないので、最後の通常のメッセージループへと入ってきます(これが「注」で書いた、あらかじめ関連づけられているメッセージの方が優先される理由です)。 
 そこで、関連づけられていないメッセージに対する処理を「この部分にあとで追加します。」の部分に追加します。


		CKABDoc* pDoc = GetDocument();	// ドキュメントを取得しておきます。
		// 各メッセージをチェックします。
		for( int i = 0; i <= pDoc->m_cMsgStrAry.GetSize() - 1; i++ )
		{
			if( nID == (UINT)( ID_NEWMENU_0001 + i ) )// もしそのメッセージなら……
			{
				// この部分にさらに追加します。
			}
		}
	

 まずドキュメントへのポインタを取得して、メニューの項目を持っているm_cMsgStrAryメンバ変数へとアクセスできるようにします。次に、CStringArrayクラスGetSizeメンバ関数で、配列の中の項目数を取得します。項目数は当然3つですが、それだと0から数えたとき1つ多いので、−1しておきます。
 そして、さらにIDをチェックします。nID引数には、送られてきたメッセージのIDが入っています。それが、先ほど新しく追加したメニューに割り当てたメッセージかどうかチェックします。
 そして、そのメッセージなら「この部分にさらに追加します。」と書かれた部分が実行されるわけです。その部分は……


			if( nCode == CN_COMMAND )
			{
				// WM_COMMAND メッセージを処理します。
				MessageBox( pDoc->m_cMsgStrAry[i], pDoc->m_cMsgStrAry[i] );
			}
			else if( nCode == CN_UPDATE_COMMAND_UI )
			{
				// シンボルをオンにします(これをしないとメニュー不可のままです)。
				CCmdUI* pCmdUI = (CCmdUI*)pExtra;
				pCmdUI->Enable( TRUE );
			}
			return TRUE;
	

 nCode引数には、そのメッセージのコマンドが入っています。試しにオブジェクトIDを「ID_ADDMENU」にして、メッセージを見てみてください。COMMANDUPDATE_COMMAND_UIがあるでしょう。この2つに対処しておきます。
 COMMANDは、そのメッセージが実行されたことを意味します。そこで、そのメッセージに対する反応をこのifの中に書き込みます。今回は単に選択されたメニューアイテムの名前が書かれたダイアログを表示するだけですが、実際にアプリケーションを作成するときにはこの中にそれぞれのメッセージに対する反応を書き込みます。
 UPDATE_COMMAND_UIはシンボルの状態が確認されるときに送られます。一番最初に表示されるときにも呼び出されます。ここで、pExtra引数CCmdUIクラスにキャストして、このメニューハンドラへのポインタを得ます。そして、同クラスのEnableメンバ関数を使ってメニューを使用可能にします。これでやっと、淡色表示ともおさらばです。
 最後に、普通のメッセージループで処理されないようにTRUEを返してOnCmdMsgメンバ関数を終了しておきます。


 以上で、基本的なコーディングは終了しました。ビルドすればメニューが動的に作成されているでしょう。淡色表示も取れているでしょう。各メニューを選択すればメッセージボックスが表示されるでしょう。

 ここからの拡張は、実際には難しくなります。アイコン付きのメニューは「オーナードロー」というものを使います。また、ツリー形式のメニューを作製する場合には、CMenuの配列を作るよりはHMENUの配列を作って、Win32APIベースで作る方が楽でしょう。これらについてはこちらをご覧ください。

Update Room
 メニューの淡色表示を簡単に解決する方法が見つかったんで紹介します。CFrameWnd::m_bAutoMenuEnableというメンバ変数にFALSEを入れておけばOK。コンストラクタででもいいので入れておいてください。こうすれば「デフォルトでメニューアイテムの使用を許可」になります。UPDATE_COMMAND_UIは、任意に不可にしたいときとかに使ってください。

(C)KAB-studio 1997 ALL RIGHTS RESERVED.