MenuItemのオーナードロー

注意:
この文書は以前「.NETでいきまっしょい!」で公開していたものですが、公開以降メンテナンスされていません。 今や古い情報となった内容が記載されている場合があるのでご注意ください。

0.オーナードローについて


 オーナードローとは何かなど、オーナードローに関する基本的なことは、こちらで解説しているので適宜参照してください。

1.MenuItemのオーナードローの基本


 まず始めに、MenuItemクラスを用いてオーナードローを行う場合、OwnerDrawプロパティをTrueに設定し、MeasureItem及びDrawItemイベントに適切なイベントハンドラを指定します。 MeasureItemではオーナードローされる場合における項目毎の寸法を計算するもで、DrawItemでは実際にオーナードローを行います。 基本的にMeasureItemイベントハンドラでは、メニュー項目で指定されている文字列のサイズを計算し、それをもって項目のサイズとします。 また、オーナードローを使用するでは、Textプロパティに"-"を指定してもセパレータとしては扱われないので、この文字列が指定されているメニュー項目ではセパレータの描画をするコードを付け加えなければなりません。
 次のコードでは、オーナードローされたコンテキストメニューを作成し、テキストボックスに右クリックされたときに表示させるコンテキストメニューを指定しています。 これを実行する場合は、テキストボックスを右クリックすればコンテキストメニューが表示されるはずです。
MenuItemのオーナードローの基本
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
Option Strict On

Public Class Form1
    Inherits System.Windows.Forms.Form

#Region " Windows フォーム デザイナで生成されたコード "
#End Region

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        ' コンテキストメニューのインスタンスを作成
        Dim contextMenu As New ContextMenu()

        ' メニュー項目のインスタンスへの参照を一時的に保持するための変数
        Dim menuItem As MenuItem

        ' メニュー項目のインスタンスを作成し、コンテキストメニューに追加
        menuItem = New MenuItem("切り取り")
        menuItem.OwnerDraw = True
        AddHandler menuItem.MeasureItem, AddressOf MenuItem_MeasureItem
        AddHandler menuItem.DrawItem, AddressOf MenuItem_DrawItem
        contextMenu.MenuItems.Add(menuItem)


        menuItem = New MenuItem("コピー")
        menuItem.OwnerDraw = True
        AddHandler menuItem.MeasureItem, AddressOf MenuItem_MeasureItem
        AddHandler menuItem.DrawItem, AddressOf MenuItem_DrawItem
        contextMenu.MenuItems.Add(menuItem)


        menuItem = New MenuItem("張り付け")
        menuItem.OwnerDraw = True
        AddHandler menuItem.MeasureItem, AddressOf MenuItem_MeasureItem
        AddHandler menuItem.DrawItem, AddressOf MenuItem_DrawItem
        contextMenu.MenuItems.Add(menuItem)


        menuItem = New MenuItem("削除")
        menuItem.OwnerDraw = True
        AddHandler menuItem.MeasureItem, AddressOf MenuItem_MeasureItem
        AddHandler menuItem.DrawItem, AddressOf MenuItem_DrawItem
        contextMenu.MenuItems.Add(menuItem)


        menuItem = New MenuItem("-")
        menuItem.OwnerDraw = True
        AddHandler menuItem.MeasureItem, AddressOf MenuItem_MeasureItem
        AddHandler menuItem.DrawItem, AddressOf MenuItem_DrawItem
        contextMenu.MenuItems.Add(menuItem)


        menuItem = New MenuItem("すべて選択")
        menuItem.OwnerDraw = True
        AddHandler menuItem.MeasureItem, AddressOf MenuItem_MeasureItem
        AddHandler menuItem.DrawItem, AddressOf MenuItem_DrawItem
        contextMenu.MenuItems.Add(menuItem)


        ' テキストボックスを作成し、コンテキストメニューを指定。
        Dim textBox As New TextBox()

        textBox.Location = New Point(50, 20)
        textBox.Size = New Size(200, 25)
        textBox.ContextMenu = contextMenu

        Me.Controls.Add(textBox)

    End Sub


    ' メニュー項目のサイズを計算するためのイベントハンドラ
    Private Sub MenuItem_MeasureItem(ByVal sender As Object, ByVal e As MeasureItemEventArgs)

        ' メニュー項目を取得
        Dim menuItem As MenuItem = DirectCast(sender, MenuItem)

        ' メニュー項目に指定されている文字列のサイズを計算する
        ' フォントはシステムで指定されているメニューのフォントを使用
        Dim size As SizeF = e.Graphics.MeasureString(menuItem.Text, SystemInformation.MenuFont)

        ' セパレータの場合は、幅と高さを直接指定
        If menuItem.Text = "-" Then

            size.Width = 20
            size.Height = 5

        End If

        ' 取得できたサイズをメニュー項目のサイズとする
        e.ItemWidth = CInt(size.Width)
        e.ItemHeight = CInt(size.Height)

    End Sub


    ' メニュー項目のオーナードローを行うためのイベントハンドラ
    Private Sub MenuItem_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs)

        ' メニュー項目を取得
        Dim menuItem As MenuItem = DirectCast(sender, MenuItem)

        ' 背景をグラデーションで描画
        Dim gradPoint1 As New Point(e.Bounds.Left, e.Bounds.Y) 'グラデーションの始点
        Dim gradPoint2 As New Point(e.Bounds.Right, e.Bounds.Y) 'グラデーションの終点
        Dim gradBrush As New Drawing2D.LinearGradientBrush(gradPoint1, gradPoint2, Color.LightSkyBlue, Color.White)

        e.Graphics.FillRectangle(gradBrush, e.Bounds)

        gradBrush.Dispose()

        ' セパレータ以外の場合
        If menuItem.Text <> "-" Then

            ' 文字を描画
            e.Graphics.DrawString(menuItem.Text, SystemInformation.MenuFont, Brushes.DarkBlue, e.Bounds.X, e.Bounds.Y)

        Else

            ' セパレータを描画
            Dim point1 As New PointF(e.Bounds.Left + 5, e.Bounds.Y + e.Bounds.Height / 2.0F)
            Dim point2 As New PointF(e.Bounds.Right - 5, point1.Y)

            e.Graphics.DrawLine(Pens.DarkBlue, point1, point2)

        End If

        ' 選択されている項目の場合、半透明のボックスを描画して強調
        If (e.State And DrawItemState.Selected) = DrawItemState.Selected Then

            Dim b As New SolidBrush(Color.FromArgb(&H60, &H0, &HA0, &HFF))

            e.Graphics.FillRectangle(b, e.Bounds)

            b.Dispose()

        End If

    End Sub


End Class
実行結果
実行結果

2.デフォルト指定の描画


 オーナードローをする場合でも、描画処理をデフォルトのシステムカラーなどで行うこともできます。 DrawItemEventArgsクラスには、背景やフォーカスを描画するメソッドや、前景・背景色、フォントなどのプロパティがあるため、これらを参照することができます。 次のコードは実際にそれを用いて描画した場合の例です。 MenuItem_DrawItem()メソッド以外はすべて前のコードと同じです。
デフォルト指定の描画
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
' メニュー項目のオーナードローを行うためのイベントハンドラ
Private Sub MenuItem_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs)

    ' メニュー項目を取得
    Dim menuItem As MenuItem = DirectCast(sender, MenuItem)

    ' デフォルトの背景を描画
    e.DrawBackground()

    ' セパレータ以外の場合
    If menuItem.Text <> "-" Then

        ' デフォルトの前景色で文字を描画
        Dim b As New SolidBrush(e.ForeColor)

        e.Graphics.DrawString(menuItem.Text, e.Font, b, e.Bounds.X, e.Bounds.Y)

        b.Dispose()

    Else

        Dim point1 As New PointF(e.Bounds.Left + 5, e.Bounds.Y + e.Bounds.Height / 2.0F)
        Dim point2 As New PointF(e.Bounds.Right - 5, point1.Y)

        ' デフォルトの前景色でセパレータを描画
        Dim p As New Pen(e.ForeColor)

        e.Graphics.DrawLine(p, point1, point2)

        p.Dispose()

    End If

    ' デフォルトでのフォーカス描画
    e.DrawFocusRectangle()

End Sub
実行結果
実行結果

 ただ、この場合でもセパレータだけは自分で描画する必要があります。

3.メニュー項目の状態の取得と描画


 チェック状態や選択状態を取得するには、DrawItemEventArgs.Stateプロパティを参照します。 このメンバは列挙型の値で、メニュー項目に関する様々な状態が格納されます。 この状態にあわせてオーナードローする場合は、次のようにして状態ごとに描画処理を記述します。
メニュー項目の状態の取得
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
If (e.State And DrawItemState.Checked) = DrawItemState.Checked Then

    ' チェックされている時の描画

ElseIf (e.State And DrawItemState.Grayed) = DrawItemState.Selected Then

    ' 選択されている時の描画

ElseIf (e.State And DrawItemState.Disabled) = DrawItemState.Disabled Then

    ' 無効な状態(EnabledにFalseが指定)されている時の描画

ElseIf (e.State And DrawItemState.None) = DrawItemState.None Then

    ' 特に状態が無い(平常時の)ときの描画

End If

 この方法によってチェック状態の判別などはができますが、メニュー項目の横にチェックマークなどは入らないので、これも独自に描画しなければなりません。 独自に描画するのがめんどくさい場合は、ControlPaintクラスのDrawMenuGlyph()メソッドを使用することができます。 ただ、このメソッドではチェックマークは描画されますが、白い背景も同時に描画されてしまうので、デザインを重視する場合はやはり独自の描画をするべきかと思います。
淡色表示・チェック項目の表示
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
' メニュー項目のオーナードローを行うためのイベントハンドラ
Private Sub MenuItem_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs)

    ' メニュー項目を取得
    Dim menuItem As MenuItem = DirectCast(sender, MenuItem)

    ' 背景をグラデーションで描画
    Dim gradPoint1 As New Point(e.Bounds.Left, e.Bounds.Y) 'グラデーションの始点
    Dim gradPoint2 As New Point(e.Bounds.Right, e.Bounds.Y) 'グラデーションの終点
    Dim gradBrush As New Drawing2D.LinearGradientBrush(gradPoint1, gradPoint2, Color.LightSkyBlue, Color.White)

    e.Graphics.FillRectangle(gradBrush, e.Bounds)

    gradBrush.Dispose()

    ' セパレータ以外の場合
    If menuItem.Text <> "-" Then

        ' 文字を描画
        If (e.State And DrawItemState.Disabled) = DrawItemState.Disabled Then

            ' 無効状態の時
            e.Graphics.DrawString(menuItem.Text, SystemInformation.MenuFont, Brushes.Gray, e.Bounds.X + 16, e.Bounds.Y)

        Else

            ' 通常状態の時
            e.Graphics.DrawString(menuItem.Text, SystemInformation.MenuFont, Brushes.DarkBlue, e.Bounds.X + 16, e.Bounds.Y)

        End If

        ' チェックされているとき
        If (e.State And DrawItemState.Checked) = DrawItemState.Checked Then

            ' チェックマークの描画範囲
            Dim r As New Rectangle(e.Bounds.Left + 2, e.Bounds.Top + 2, 12, 12)

            ' チェックマークを描画
            ControlPaint.DrawMenuGlyph(e.Graphics, r, MenuGlyph.Checkmark)

        End If

    Else

        ' セパレータを描画
        Dim point1 As New PointF(e.Bounds.Left + 5, e.Bounds.Y + e.Bounds.Height / 2.0F)
        Dim point2 As New PointF(e.Bounds.Right - 5, point1.Y)

        e.Graphics.DrawLine(Pens.DarkBlue, point1, point2)

    End If

    ' 選択されている項目の場合、半透明のボックスを描画して強調
    If (e.State And DrawItemState.Selected) = DrawItemState.Selected Then

        Dim b As New SolidBrush(Color.FromArgb(&H60, &H0, &HA0, &HFF))

        e.Graphics.FillRectangle(b, e.Bounds)

        b.Dispose()

    End If

End Sub
実行結果
実行結果
今気づいたんですが、正しくは「張り付け」ではなく「貼り付け」ですね (笑

 このコードでは「コピー」の項目のCheckedプロパティをTrue、「貼り付け」EnabledプロパティをFalseに指定してオーナードローしたものです。 淡色表示やチェックマークの表示はMenuItem_DrawItem()メソッドで行いますが、チェックマークを挿入するためにMenuItem_MeasureItem()でのサイズ計算の式を多少変更しています。 具体的には、文字を横にずらすために幅を少し広めに取っています。

4.タスクトレイ上のコンテキストメニューに関するバグ

 (注)これは.NET Framework version 1.0上での結果です。 .NET Framework version 1.1では改善されているかもしれませんが、まだ実験していません。
 結論から先に言うと、オーナードローを利用したコンテキストメニューをNotifyIconクラスのインスタンスに対して指定した場合、正しく描画されません。 まず、次のようにコードを変えてタスクトレイ上にアイコンを表示させます。
タスクトレイ上のコンテキストメニュー
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    '
    ' メニュー項目を作成するコードは前述のものと同じ。
    '


    ' 通知アイコンを作成し、コンテキストメニューを指定。
    Dim notifyIcon As New NotifyIcon()

    notifyIcon.Visible = True
    notifyIcon.Icon = Me.Icon
    notifyIcon.ContextMenu = contextMenu
    notifyIcon.Text = "MenuItemOwnerDraw"

End Sub

 そして、これをコンパイル・実行したあと、タスクトレイに現れたアイコンを右クリックすると次のようになってしまいます。
実行結果
実行結果

 これは意図的にこのようにしたのではなく、本当にこのようになってしまいます。 つまり、タスクトレイ上のアイコンからはオーナードローしたメニューを正しく表示することができないのです。 これはどうも.NET Framework自体のバグのようなので、これを回避する唯一の手段は、オーナードローを使用しないことしかありません。