ビットマップメニュー(MenuEx)


■ 概略説明
既成のソフトウェアには、Visual Studioのメニューのように、メニューの中にビットマップがあるものを良く見かけますね。
このサンプルはこの機能を実現した例です。

上の画面はメインメニューの中のビットマップメニューの例です。

ボップアップメニューにも対応しています。
ビットマップメニューは、米サイトのCodeGuruの中の記事の幾つかを元に改良(のつもりですが)したものです。
■ プログラムの説明
CMenuExは、オーナードローのメニューのためのCMenuの独自の派生クラスです。
オーナードローの処理の流れは、他のコントロールと同じくMeasureItem( )とDrawItem( )の関数によりなされます。メニューの画面の更新が必要なときに、システムはこの2つの関数を順に呼び出します。2つの関数の内容を簡単に説明しましょう。

MeasureItem( LPMEASUREITEMSTRUCT lpMIS)は、描画が必要なメニューのアイテム(1つのメニューの項目)毎に呼び出されます。
引数のLPMEASUREITEMSTRUCTは、メニューのための情報を持つ構造体のポインタですが、この関数ではこの中の、itemWidth(メニューアイテムの幅)とitemHeigh(メニューアイテムの高さ)を計算することが目的です。
計算の仕方は意外と簡単で、実際に仮想のメモリに書いてそのサイズを取得することによりなされています。
テキストの描画は、CDCクラスのDrawText( )関数を使いますが、この関数には便利な機能があります。それは、指定された文字列を描画するのに必要な矩形のサイズを計算するという機能です。まさしくMeasureItemのために作られたような機能ですね。この機能を使うためには、nFormatにDT_SINGLELINEとDT_CALCRECTを指定します。これにより、実際に描画することなくサイズだけを取得することが出来ます。
メニューのアイテムの文字列には、一般的にコントロールキーを使ったショートカット呼び出しを持つアイテムのように、タブによるスペースを持つものがあります。この場合は、タブのスペースとして一定量の幅を足します。
例として、
        新規作成(N)         Ctrl+N
の場合、[新規作成(N)]のサイズ + タブスペースのサイズ + [Ctrl+N]のサイズとなります。 ビットマップメニューの場合は、このサイズにビットマップの幅と他の隙間のサイズが加算されます。 DrawItem( LPDRAWITEMSTRUCT lpDIS)は、メニュー項目の描画処理で、これも1つのメニュー項目毎に呼び出されます。 引数のLPDRAWITEMSTRUCTは、メニュー描画のための情報を持つ構造体のポインタで、描くべきDCのハンドル、描画エリアの位置とサイズ及び、メニューの状態やタイプが送られてきます。
特殊なケースは、メニューのセパレータですが、この場合は文字列を持たない単純な線描画を行います。
サイズに関しては、MeasureItemにより各アイテム毎の計算を行ったあと、システムが最大のサイズ(幅)を決めて常にこの値を送ってくるようです。従ってここでは、単にその位置に文字列を描画するだけです。タブ以降の文字に関しては、右側から文字列を描くことによりタブ間隔が開いたように見せかけています。

最後に、ポップアップメニューの対応のためのSynchronize( )関数についてですが、これは、ポップアップメニューをオーナードローにするための処理で、ポップアップメニューが開かれる前に呼び出されるようにします。
■ ビットマップメニューの追加方法
@既存のプロジェクトへのCMenuExの追加
まず、プロジェクトへのファイルの追加で、MenuEx.cppとMenuEx.hを追加してください。

ACMenuExメンバの追加
CMainFrameへ、CMenuのメンバを追加してください。
        CMenuEx         m_menu;
BMainFrameへビットマップメニューのためのウィンドウメッセージ処理の追加
クラスウィザードでCMainFrameへ以下の3つの処理を追加してください。
WM_MEASUREITEM、WM_DRAWITEM、WM_INITMENUPOPUP
これらの処理ルーチンに以下の様に、ビットマップメニューのための処理を追加します。
        void CMainFrame::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) 
        {
                // ビットマップメニュー
                if ( lpMeasureItemStruct->CtlType == ODT_MENU)
                {
                        m_menu.MeasureItem( lpMeasureItemStruct);
                        return;
                }
                
                CFrameWnd::OnMeasureItem(nIDCtl, lpMeasureItemStruct);
        }

        void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) 
        {
                // ビットマップメニュー
                if ( lpDrawItemStruct->CtlType == ODT_MENU)
                {
                        m_menu.DrawItem( lpDrawItemStruct);
                        return;
                }

                CFrameWnd::OnDrawItem(nIDCtl, lpDrawItemStruct);
        }

        void CMainFrame::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu) 
        {
                CFrameWnd::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);
        
                // ビットマップメニュー
                if ( !bSysMenu)
                        CMenuEx::Synchronize( pPopupMenu);
        }
Cビットマップの作成
メニューに表示される各ビットマップを作成します。ビットマップのサイズは16(幅)X15(高さ)です。
また、このビットマップは背景色を指定し、その部分は実際表示される際、システム背景色に変更されます。そして、メニューの処理が禁止されているとき背景色以外は、ダークグレーで塗りつぶされます。背景色は、デフォルトで紫になっています。これを変更したい場合は、CMenuExのm_colMaskをDEFAULT_MASK_COLORから他の値に変更してください。

それぞれのビットマップにIDを任意の付けますが、サンプルでは分かりやすいように処理のIDとおなじIDB_で始まる名前にしてあります。
例えば、処理ID_FILE_NEWのビットマップは、IDB_FILE_NEWとなります。
チェック付のアイテムの場合でビットマップを持たない場合、デフォルトのチェック用のビットマップIDB_CHECKが必要です。
このサンプルのビットマップをコピーするか、自分で作成するかして下さい。

Dビットマップ処理の初期化とビットマップデータの登録
CMainFrameのOnCreate( )関数の最後にCMenuExの初期化処理と、Cで作成したビットマップデータの登録を追加します。
        int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
        {
                :
                :
                // ビットマップメニュー
                m_menu.Initialize( IDR_MAINFRAME, this);
                m_menu.AddBitmap( ID_FILE_NEW, IDB_FILE_NEW);
                m_menu.AddBitmap( ID_FILE_OPEN, IDB_FILE_OPEN);
                :

                return 0;
        }
ポップアップメニューのためのデータも全てここで追加します。

Eポップアップメニュー
ポップアップメニューの追加方法はは、ビットマップデータの追加(AddBitmap)以外は通常のメニューの時と同じです。
■ デバッグ時の注意
作成したプログラムをデバッグするとき、OnMeasureItem、OnDrawItem、及びOnInitMenuPopupでプログラムを止めて中断しないでください。これらは、メニューの場合システム(Windows)が直接かかわっている処理らしいので、中断するとその後メニュー関係の表示処理がおかしくなる場合がありました。
続行させる分には、大丈夫だとは思いますが、中断させないほうが良いと思います。
おかしくなった時は、私の場合はウィンドウズを起動しなおすことにより戻りました。
■ ファイルのダウンロード
(MFC6.0プロジェクトファィル28KB)