SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

デザイン性に優れた拡張メニューコントロール

LuxMenuの内部構造の詳細を理解する

デザイン性に優れた拡張メニューコントロール 第3回


CLuxMenuクラス

 CLuxMenuクラスでは、既存メニューのインポートから描画まで、メニューの編集機能を提供するクラスモジュールがコーディングされています。すべてを解説すると量が多くなってしまうので、今回は以下のポイントを重視して説明します(その他の情報に関しては、ソースコードを参照)。

メンバとコンストラクタ、デストラクタ

 まずは、このクラスで必要になるメンバとコンストラクタ、デストラクタを定義していきます。

CLuxMenuクラスのメンバ宣言
Class CLuxMenu
    Inherits CLuxCtrlBase

    'COM参照カウンタ
    m_ref As Long

Protected
    m_hMenu As HMENU
    m_hMenuFont As HFONT

    MenuColor_Text As COLORREF           'テキストの色
    MenuColor_Gray_Text As COLORREF      '淡色テキストの色
    MenuColor_Default_Line As COLORREF   '通常時の線の色
    MenuColor_Default_Back As COLORREF   '通常時の背景色
    MenuColor_Hot_Line As COLORREF       'ホット状態の線の色
    MenuColor_Hot_Back As COLORREF       'ホット状態の背景色
    MenuColor_Select_Line As COLORREF    '選択状態の線の色
    MenuColor_Select_Back As COLORREF    '選択状態の背景色

Public

    iMenuItemNum As Long
    ppobj_MenuItemData As **CMenuItemData

    '------------------------------------------
    ' コンストラクタ
    '※フォント、各種カラーの初期化
    '------------------------------------------
    Sub CLuxMenu()
        'COM参照カウンタの初期化
        m_ref=0

        'アイテム データ用の領域を初期化
        ppobj_MenuItemData=malloc(1)
        iMenuItemNum=0

        'メニューフォント
        Dim NCMetrics As NONCLIENTMETRICS
        NCMetrics.cbSize = SizeOf( NONCLIENTMETRICS )
        SystemParametersInfo( SPI_GETNONCLIENTMETRICS,
            sizeof( NONCLIENTMETRICS ), VarPtr(NCMetrics), 0 )
        m_hMenuFont=CreateFontIndirect(NCMetrics.lfMenuFont)

        '各種カラー
        MenuColor_Text=GetSysColor(COLOR_WINDOWTEXT)     'テキストの色
        MenuColor_Gray_Text=GetSysColor(COLOR_GRAYTEXT)  '淡色テキストの色
        MenuColor_Default_Line=GetSysColor(COLOR_3DDKSHADOW) 
                                                         '通常時の線の色
        MenuColor_Default_Back=GetSysColor(COLOR_3DFACE) '通常時の背景色
        MenuColor_Hot_Line=GetSysColor(COLOR_ACTIVECAPTION)
                                                         'ホット状態の線の色
        MenuColor_Hot_Back=LightRGB(MenuColor_Hot_Line,0.7)
                                                         'ホット状態の背景色
        MenuColor_Select_Line=MenuColor_Default_Line     '選択状態の線の色
        MenuColor_Select_Back=GetSysColor(COLOR_MENU)    '選択状態の背景色
    End Sub

    '------------------------------------------
    ' デストラクタ
    '------------------------------------------
    Sub ~CLuxMenu()
        Dim i As Long
        For i=0 To ELM(iMenuItemNum)
            Delete ppobj_MenuItemData[i]
        Next
        free(ppobj_MenuItemData)

        DeleteObject(m_hMenuFont)

        If m_hMenu Then DestroyMenu(m_hMenu)
    End Sub

    ' Note - この部分が各種メソッドの記述領域になります
    
End Class

 先ほど説明したCLuxCtrlBaseクラスを継承し、グラデーションビットマップ生成や淡色アイコン生成のメソッドを使えるようにします。

  • m_refIUnknown用の参照カウンタです。
     
  • m_hMenuはメニューハンドル、m_hMenuFontはメニューテキストのフォントハンドルです。
     
  • MenuColor_???はメニューアイテムの描画時に利用する各種カラーです(詳細はコード内のコメント参照)。
     
  • ppobj_MenuItemDataiMenuItemNumはメニューアイテムの詳細情報を管理するためのオブジェクトポインタとその個数値です。CMenuItemDataクラスについては後述します。

 コンストラクタ、デストラクタではメンバの初期化と解放を行っています。

IUnknownの実装コード

 さてCLuxMenuクラスは、後ほどILuxMenuインターフェイスとして提供しなければなりません。COMコンポーネントの義務であるIUnknownは、前回説明したとおりの流れで実装されています。インターフェイスを新たに生成するQueryInterfaceでは、IID_ILuxMenuを識別し、自身への参照が可能になっています。IID_ILuxMenuは後述のILuxMenuインターフェイスと一緒に定義していきます。

IUnknownの実装
'-------------------------------------------------------
' COMインターフェイス用IUnknownメソッド

Virtual Function QueryInterface(ByRef riid As IID,
    ppvObj As *VoidPtr) As HRESULT
    If IsEqualIID(riid,IID_ILuxMenu)<>0 or _
        IsEqualIID(riid,IID_IUnknown)<>0 Then
        Set_LONG_PTR(ppvObj,VarPtr(This) As LONG_PTR)
        AddRef()
        QueryInterface=S_OK
    Else
        Set_LONG_PTR(ppvObj,0)
        QueryInterface=E_NOINTERFACE
    End If
End Function

Virtual Function AddRef() As DWord
    m_ref++
    AddRef=m_ref
End Function
Virtual Function Release() As DWord
    m_ref--
    If m_ref=0 Then
        Delete VarPtr(This)
    End If
    Release=m_ref
End Function

' ここまで
'-------------------------------------------------------

 ここから定義していくメソッドは、COMインターフェイスとして機能させたいものばかりですので、Virtual修飾子を指定して仮想関数にしていきます。

ImportMenuメソッド(一般メニューのインポート)

 ImportMenuメソッドによって、Win32APIのCreateMenu関数や、LoadMenu関数で生成されたメニューハンドルを、CLuxMenuクラスがそのまま取り込めるようになっています。こうすることで、既存のメニューからLuxMenuへと、すぐに移行することが可能です。

 hMenuパラメータにLuxMenuへ移行したいメニューハンドルが指定されます。GetMenuItemCount関数でメニューアイテムの個数を取得し、その数だけループを行います。ループ内ではAddMenuExItemDataメソッドを利用してCMenuItemDataクラスのインスタンスを、アイテムの個数分だけ生成します。AddMenuExItemDataPrivateとしてCLuxMenuが持つメソッドです。

 サブメニューを持つポップアップアイテムにぶつかったときは、別途CLuxSubMenuクラスのインスタンスを生成しています。サブアイテムの生成先でもImportMenuメソッドを呼び出します(CLuxSubMenuCLuxMenuの派生クラスなので、ImportMenuを再帰的に呼び出すことができるのです)。

ポイント: LuxMenuではメニューアイテムをオーナー描画アイテムとして扱う
 一般的なメニューでは、アイテムタイプ(文字列、セパレータ、オーナー描画)をMENUITEMINFO構造体のfTypeメンバに持ちますが、LuxMenuではすべてのメニューアイテムをオーナー描画アイテムとして扱わなければなりません。このループ内では、SetMenuItemInfo関数を呼び出してオーナー描画の指定も行っています。拡張メニュー情報として、MENUITEMINFO構造体のdwItemDataメンバにpobj_MenuItemDataを指定しているところにも注目しましょう。dwItemDataに設定した値は、後ほど解説するWM_MASUREITEMWM_DRAWITEMメッセージのパラメータとして取得することが可能になります。
 

 ついでなので、GetMenuHandleメソッドの定義もしてしまいましょう。これは外部からCLuxMenuクラスのインスタンスが持っているメニューハンドルを取得するためのものです。

ImportMenuメソッドのコード
'-------------------------------------------------------
'既存メニューのインポート
'-------------------------------------------------------
Virtual Sub ImportMenu(hMenu As HMENU)
    '------------------------------------
    ' 従来のメニューハンドルの取り込み
    '------------------------------------
    m_hMenu=hMenu

    Dim temporary[MAX_PATH] As Byte
    Dim mii As MENUITEMINFO
    mii.cbSize=SizeOf(MENUITEMINFO)
    mii.fMask=MIIM_TYPE or MIIM_ID or MIIM_SUBMENU or MIIM_DATA

    Dim nCount As Long
    nCount=GetMenuItemCount(m_hMenu)

    Dim i As Long
    For i=0 To ELM(nCount)
        mii.dwTypeData=temporary
        mii.cch=MAX_PATH
        GetMenuItemInfo(m_hMenu,i,1,mii)

        Dim temp2=0 As LPSTR
        If mii.fType=MFT_STRING Then temp2=temporary

        'サブメニューもインポートする
        Dim pobj_SubMenu As *CLuxSubMenu
        If mii.hSubMenu Then
            pobj_SubMenu=New CLuxSubMenu()
            pobj_SubMenu->ImportMenu(mii.hSubMenu)
        Else
            pobj_SubMenu=0
        End If

        '拡張データをメモリに退避
        Dim pobj_MenuItemData As *CMenuItemData
        pobj_MenuItemData=AddMenuExItemData(i,mii.wID,
            temp2,VarPtr(This),pobj_SubMenu)

        'メニューアイテムをすべてオーナー描画可能にする
        mii.fType=MFT_OWNERDRAW
        mii.dwItemData=pobj_MenuItemData As ULONG_PTR
        SetMenuItemInfo(m_hMenu,i,1,mii)
    Next
End Sub

'-------------------------------------------------------
' メニューハンドルの取得
'-------------------------------------------------------
Virtual Function GetMenuHandle() As HMENU
    GetMenuHandle=m_hMenu
End Function

MeasureItemメソッド(WM_MASUREITEM)、DrawItemメソッド(WM_DRAWITEM)

 繰り返しになりますが、LuxMenuは既存メニューコントロールのオーナー描画によって実現されるコントロールです。したがって、ウィンドウからWM_MASUREITEMWM_DRAWITEMなどのオーナー描画用のメッセージが来たら、しかるべき処理をしなければなりません。そのような機能をウィンドウプロシージャ内に書くのは美しくないので、CLuxMenuが提供する一機能として作成します。

 双方のメソッド共に、lParamをパラメータとして持ちます。これでウィンドウプロシージャに渡された値がそのまま送り届けられることになります。パラメータ内の32ビット値にはCMenuItemDataクラスのインスタンスポインタが格納されているので、pobj_MenuItemDataにコピーを行い、CMenuItemDataへのアクセスを可能にしています。

 また、WM_MASUREITEMWM_DRAWITEMメッセージはターゲットとなるメニューアイテムに関係なく送られてくるので、HitTest_MenuObjメソッド(CLuxMenuクラスでPrivate宣言されています)を使って、自身へのメッセージなのかどうかをチェックするための機構を埋め込んでいます。

 最後に、MeasureItemメソッドはlpMI->itemWidthlpMI->itemHeightにメニューアイテムを表示するために必要な幅と高さを指定して終了します。DrawItemメソッドは、CMenuItemData.OwnerDrawメソッドを呼び出して終了します。

MeasureItemメソッド、DrawItemメソッド
'-------------------------------------------------------
' WM_MASUREITEMメッセージの処理
'-------------------------------------------------------
Virtual Function MeasureItem(lParam As LPARAM) As BOOL
    Dim lpMI As *MEASUREITEMSTRUCT
    lpMI=lParam As *MEASUREITEMSTRUCT
    If lpMI->CtlType=ODT_MENU Then
        Dim pobj_MenuItemData As *CMenuItemData
        pobj_MenuItemData=lpMI->itemData As *CMenuItemData

        If HitTest_MenuObj(VarPtr(This),
            pobj_MenuItemData->pobj_ThisMenu As *CLuxMenu) Then
            Dim size As SIZE
            pobj_MenuItemData->GetItemSize(size)

            lpMI->itemWidth=size.cx
            lpMI->itemHeight=size.cy
        End If
    End If

    MeasureItem=1
End Function

'-------------------------------------------------------
' WM_DRAWITEMメッセージの処理
'-------------------------------------------------------
Virtual Function DrawItem(lParam As LPARAM) As BOOL
    Dim pdis As *DRAWITEMSTRUCT
    pdis=lParam As *DRAWITEMSTRUCT
    If pdis->CtlType=ODT_MENU Then
        Dim pobj_MenuItemData As *CMenuItemData
        pobj_MenuItemData=pdis->itemData As *CMenuItemData

        If HitTest_MenuObj(VarPtr(This),
            pobj_MenuItemData->pobj_ThisMenu As *CLuxMenu) Then
            Dim bSelect As BOOL
            If pdis->itemState and ODS_SELECTED Then
                bSelect=1
            Else
                bSelect=0
            End If

            pobj_MenuItemData->OwnerDraw(pdis->hDC,
                pdis->rcItem,
                bSelect)
        End If

    End If

    DrawItem=1
End Function

メニュー操作のための各種メソッド

 CLuxMenuクラスは以下に示すメソッドを提供します。これらは、Win32APIの各関数の呼び出しを行うことで実現しています。

CLuxMenuクラスのメソッド一覧
メソッド名 説明
CreateNewMenuメソッド メニューコントロールの新規作成
EnableItemメソッド メニューアイテムを有効化/無効化
CheckMenuメソッド メニューアイテムのチェックのON/OFF
IsCheckedメソッド メニューアイテムのチェック状況を取得
RemoveItemメソッド メニューアイテムの消去
InsertItemメソッド メニューアイテムの挿入
InsertPopupItemメソッド ポップアップ メニューアイテムの挿入

SetIconメソッド

 メニューアイテムにアイコンを指定するためのメソッドです。dwItemIDパラメータにメニューアイテムIDを指定し、hIconにアイコンハンドルを指定します。成功すると1が、失敗すると0が返ります。CLuxCtrlBase.CreateGrayIconメソッドを呼び出して自動的に淡色アイコンの生成を行っているのも見逃せない点です。

 メニューの深い階層のアイテムも察知できるよう、必要に応じてサブメニューに対してのSetIconメソッドも呼び出しています。

SetIconメソッドのコード
'-------------------------------------------------------
' メニューアイテムにアイコンをセット
'-------------------------------------------------------
Virtual Function SetIcon(dwItemID As DWord, hIcon As HICON) As BOOL
    Dim i As Long
    For i=0 To ELM(iMenuItemNum)
        If ppobj_MenuItemData[i]->dwItemID=dwItemID Then
            ppobj_MenuItemData[i]->hIcon=hIcon

            '淡色アイコンを作成
            ppobj_MenuItemData[i]->hGrayIcon=CreateGrayIcon(hIcon)

            SetIcon=1
            Exit Function
        End If

        If ppobj_MenuItemData[i]->pobj_SubMenu Then
            Dim lResult As LRESULT
            lResult=ppobj_MenuItemData[i]->pobj_SubMenu->SetIcon( _
                dwItemID,hIcon)
            If lResult Then
                SetIcon=1
                Exit Function
            End If
        End If
    Next

    SetIcon=0
End Function

GetItemSizeメソッド

 このメソッドはWM_MEASUREITEMメッセージ→MeasureItemメソッド→GetItemSizeメソッドという流れで呼び出されます。item_indexパラメータで指定されたメニューアイテムのサイズをsizeパラメータに格納します。

GetItemSizeメソッドのコード
'-------------------------------------------------------
' メニューアイテムの表示領域サイズを取得
'-------------------------------------------------------
Virtual Sub GetItemSize(item_index As Long, ByRef size As SIZE)
    If ppobj_MenuItemData[item_index]->str Then
        Dim hdc As HDC
        hdc=GetDC(GetDesktopWindow())
        Dim hOldFont As HFONT
        hOldFont=SelectObject(hdc,m_hMenuFont)

        GetTextExtentPoint32(hdc,
            ppobj_MenuItemData[item_index]->str,
            lstrlen(ppobj_MenuItemData[item_index]->str),
            size)

        SelectObject(hdc,hOldFont)
        DeleteDC(hdc)
    Else
        'セパレータのとき
        size.cx=50
        size.cy=10
    End If
End Sub

OwnerDrawメソッド

 CLuxMenu.OwnerDrawメソッドは親メニュー(ウィンドウのメニューバー)の描画を行います。サブメニューの描画については、後ほどCLuxSubMenuOwnerDrawメソッドをオーバーライドして実装します。

 hdcパラメータにはデバイスコンテキストを、ref_rcには描画するためのメニューアイテムの領域を、bSelectにはセレクト状態であるか否かをそれぞれ指定します。

 bSelectを使って選択状態を識別し、選択状態であれば選択中の背景を描画します。選択状態でなければ、通常はデフォルトの背景色で塗りつぶしたいところですが、マウスカーソルがアイテム上にあるかどうかをCLuxCtrlBase.HitTestメソッドで判別し、必要に応じてホット状態の描画を行います。

 最後に、DrawText関数でメニューアイテムの文字列を描画して完了です。

OwnerDrawメソッドのコード
'-------------------------------------------------------
' 親メニューのオーナー描画
'-------------------------------------------------------
Virtual Sub OwnerDraw(hdc As HDC, ByRef ref_rc As RECT,
    bSelect As BOOL, item_index As Long)
    Dim BackColor As COLORREF, TextColor As COLORREF
    Dim rc As RECT
    Dim pos As POINTAPI

    Dim hPen As HPEN,hOldPen As HPEN
    Dim hBrush As HBRUSH, hOldBrush As HBRUSH

    If bSelect Then
        '選択状態にある場合
        hPen=CreatePen(PS_SOLID,0,MenuColor_Select_Line)
        hOldPen=SelectObject(hdc,hPen)

        hBrush=CreateSolidBrush(MenuColor_Select_Back)
        hOldBrush=SelectObject(hdc,hBrush)

        ref_rc.left++
        ref_rc.top++
        Rectangle(hdc,ref_rc.left,ref_rc.top,ref_rc.right,ref_rc.bottom)
        ref_rc.left--

        SelectObject(hdc,hOldPen)
        SelectObject(hdc,hOldBrush)

        DeleteObject(hPen)
        DeleteObject(hBrush)

        TextColor=MenuColor_Text
    Else
        'マウスカーソルの座標を取得
        GetCursorPos(pos)

        '親ウィンドウのウィンドウ左上からの相対座標に変換
        '(クライアント座標ではないところに注意)
        GetWindowRect(WindowFromDC(hdc),rc)
        pos.x-=rc.left
        pos.y-=rc.top

        If HitTest(ref_rc,pos) Then
            'ホットイメージで表示
            hPen=CreatePen(PS_SOLID,0,MenuColor_Hot_Line)
            hOldPen=SelectObject(hdc,hPen)

            hBrush=CreateSolidBrush(MenuColor_Hot_Back)
            hOldBrush=SelectObject(hdc,hBrush)

            ref_rc.left++
            ref_rc.top++
            Rectangle(hdc,ref_rc.left,ref_rc.top,ref_rc.right,ref_rc.bottom)
            ref_rc.left--
            ref_rc.top--

            SelectObject(hdc,hOldPen)
            SelectObject(hdc,hOldBrush)

            DeleteObject(hPen)
            DeleteObject(hBrush)

            TextColor=MenuColor_Text
        Else
            BackColor=MenuColor_Default_Back
            TextColor=MenuColor_Text

            '背景を塗りつぶす
            hBrush=CreateSolidBrush(BackColor)
            FillRect(hdc,ref_rc,hBrush)
            DeleteObject(hBrush)
        End If
    End If

    If ppobj_MenuItemData[item_index]->str Then
        rc=ref_rc
        rc.top++

        SetBkMode(hdc, TRANSPARENT)
        SetTextColor(hdc, TextColor)

        Dim hOldFont As HFONT
        hOldFont=SelectObject(hdc,m_hMenuFont)

        DrawText(hdc,ppobj_MenuItemData[item_index]->str,-1,
            rc,DT_CENTER or DT_VCENTER or DT_SINGLELINE)

        SelectObject(hdc,hOldFont)
    Else
        'セパレータのとき
    End If
End Sub

次のページ
CLuxSubMenuクラス

この記事は参考になりましたか?

デザイン性に優れた拡張メニューコントロール連載記事一覧

もっと読む

この記事の著者

山本 大祐(ヤマモト ダイスケ)

普段はActiveBasicと周辺ツールの開発を行っています。最近は諸先輩方を見習いながら勉強中の身。AB開発日記

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/514 2008/08/26 13:40

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー