.NETプログラミング研究 第4号

注意事項

今回はコードが長いため、VB.NETのコードのみを紹介します。ご了承ください。C#のコードについては、私のサイトで紹介するかもしれません。

ピンポイントリンク

メニューにアイコンを表示する

注意

メニューにアイコンを表示する」も参考にしてください。

メニュー項目の右側にアイコンが表示されているようなアプリケーションも最近では普通になってきました。実際、このようなアプリを開発するのことはBorland社のDelphiやC++ Bulderなどでは非常に簡単にできるようです。ところが肝心のMicrosoftでは残念ながら.NETになってもそうはなりませんでした。しかしかろうじてAPIを使わなくてもある程度はできるようになったようなので、その方法をできるだけ分かりやすく説明します。

メニューにアイコンを表示するためには「オーナードロー(オーナー描画)」という方法を用います。メニューは通常システムが描画しますが、オーナードローでは描画を自分でコードを書いて行います。そのため自由度の高いメニューが作れますが、しっかりしたものを作るにはかなりの技術と手間がかかります。

早速ですが、オーナードローでメニューを表示させてみましょう。ここではその仕組みを説明するため、メニュー項目に画像のみを表示させてみます。

Form1というフォームに、MainMenu1というMainMenuオブジェクトがあり、トップレベルメニューとしてMenuItem1というMenuItemオブジェクトと、その下にサブメニューとしてMenuItem2というMenuItemオブジェクトがあるものとします。

ここではMenuItem2にオーナードローにより画像を表示してみることにします。そのためにまずMenuItem2のOwnerDrawプロパティをTrueにします。OwnerDrawプロパティをTrueにすることにより、MeasureItemとDrawItemというイベントが発生するようになります。MeasureItemイベントハンドラではメニュー項目を表示させるのに必要なサイズを計算し、DrawItemイベントハンドラで実際に描画します。

以下の例ではMenuItem2にアイコン"open.ico"を表示させています。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
'メニュー項目に表示させるアイコン
Private _icon As New Icon("open.ico")
 
'メニュー項目の大きさを計算する
Private Sub MenuItem2_MeasureItem(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.MeasureItemEventArgs) _
        Handles MenuItem2.MeasureItem
    '高さを"e.ItemHeight"に幅を"e.ItemWidth"に入れる
    'メニュー項目の高さを設定する
    e.ItemHeight = _icon.Height
    'メニュー項目の幅を設定する
    e.ItemWidth = _icon.Width
End Sub
 
'メニュー項目の描画
Private Sub MenuItem2_DrawItem(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.DrawItemEventArgs) _
        Handles MenuItem2.DrawItem
    'e.Graphicsに描画する
    'メニュー項目の境界はe.Boundsから取得できる
    e.Graphics.DrawIcon(_icon, e.Bounds.X, e.Bounds.Y)
End Sub

うまくメニューに画像が表示できたことと思います。メニューのオーナードローについては大体分かっていただけたでしょうか?

この調子だと目的を達成するのも簡単そうに思えるでしょう。次のサンプルではさらに文字列を描画し、さらにメニュー項目が選択されたときに背景色と前景色が変わるようにしてみます。これでかなりそれらしくなるはずなのですが・・・。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
'メニュー項目に表示させるアイコン
Private _icon As New Icon("open.ico")
 
'メニュー項目の大きさを計算する
Private Sub MenuItem2_MeasureItem(ByVal sender As System.Object, _
        ByVal e As System.Windows.Forms.MeasureItemEventArgs) _
        Handles MenuItem2.MeasureItem
 
    '幅を決定する
    '文字列の幅を計算する
    'メニューで使うフォントは
    'SystemInformation.MenuFontプロパティで取得できる
    Dim textWidth As Integer = CInt( _
        e.Graphics.MeasureString(sender.Text, _
        SystemInformation.MenuFont).Width)
    e.ItemWidth = textWidth + 46
 
    '高さを決定する
    e.ItemHeight = _
        Math.Max(SystemInformation.MenuFont.Height, _icon.Height) _
        + 2
End Sub
 
'メニュー項目の描画
Private Sub MenuItem2_DrawItem(ByVal sender As System.Object, _
        ByVal e As System.Windows.Forms.DrawItemEventArgs) _
        Handles MenuItem2.DrawItem
 
    '背景の塗りつぶし
    'DrawItemEventArgs.DrawBackgroundメソッドを使ってみる
    e.DrawBackground()
 
    'アイコンの描画
    If Not (_icon Is Nothing) Then
        e.Graphics.DrawIcon(_icon, _
            e.Bounds.Left + 2, _
            e.Bounds.Top + (e.Bounds.Height - _icon.Height) \ 2)
    End If
 
    '文字の描画
    Dim textBrush As Brush
    'DrawItemEventArgs.ForeColorプロパティを使ってみる
    textBrush = New SolidBrush(e.ForeColor)
    e.Graphics.DrawString(sender.Text, _
        SystemInformation.MenuFont, _
        textBrush, _
        e.Bounds.Left + 22, _
        e.Bounds.Top + _
            (e.Bounds.Height - _
            SystemInformation.MenuFont.Height) \ 2)
 
    'リソースを開放
    textBrush.Dispose()
End Sub

ところがこのサンプルには多くの問題があります。なんといっても、メニュー項目の背景の色が違います(これはDrawItemEventArgs.DrawBackgroundメソッドでは選択状態でないときSystemColors.Windowの色で描画するため)。さらに"&+文字"をアンダーラインにして表示する必要がありますし、Shortcutも表示させなければいけません。実は文字の色も実際のメニューのものとは異なっています(DrawItemEventArgs.ForeColorプロパティは選択状態でないときSystemColors.WindowTextの色になります)。

さらに、このようなコードを直接一つ一つのMenuItemオブジェクトのイベントハンドラに記述するのは現実的ではありませんし、プロシージャにしたところでスマートな方法とはいえません。実際に使えるものにするためには、MenuItemクラスから継承した新たなクラスを作成し、OnMeasureItemとOnDrawItemメソッドをオーバーライドすべきでしょう。

何を隠そう、これらの問題をクリアしたサンプルがMicrosoftの「Visual Studio .NET Family 製品対応 .NETなんだろ? CD-ROM」内にあります(「Windows フォーム: オーナー描画メニュー」というサンプル)。これを参考にして私なりにクラスを作成してみました。ImageListを使うことを考えて、このクラスではIconオブジェクトではなく、Imageオブジェクトで画像を設定するようにしました。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
Imports System
Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Text
Imports System.Windows.Forms
 
Public Class ImageMenuItem : Inherits MenuItem
    '定数-----------------------------------------
    '表示する画像のサイズ
    Private Const imageHeight As Integer = 16
    Private Const imageWidth As Integer = 16
    '上の余白(下の余白も同じになる)
    Private Const topMargin As Integer = 1
    '左側の余白
    Private Const leftMargin As Integer = 2
    '画像とテキストの間隔
    Private Const intervalImageAndText As Integer = 4
    '右側の余白
    Private Const rightMargin As Integer = 20
    'メニューのテキストの表示位置
    Private Const textLeft As Integer = _
        leftMargin + imageWidth + intervalImageAndText
    'テキスト以外の部分の幅
    Private Const widthExceptText As Integer = _
        imageWidth + leftMargin + intervalImageAndText + rightMargin
 
    'プロパティ-----------------------------------
    '表示する画像
    Private _image As Image
    Public Property Image() As Image
        Get
            Return _image
        End Get
        Set(ByVal Value As Image)
            _image = Value
        End Set
    End Property
 
    'コンストラクタ-------------------------------
    Public Sub New(ByVal text As String)
        MyBase.New(text)
        Me.OwnerDraw = True
    End Sub
 
    'メソッド-------------------------------------
    'OnMeasureItemのオーバーライド
    'メニューアイテムの大きさを計算する
    '高さを"e.ItemHeight"に幅を"e.ItemWidth"に入れる
    Protected Overrides Sub OnMeasureItem( _
            ByVal e As MeasureItemEventArgs)
        '基本クラスのOnMeasureItemを処理する
        MyBase.OnMeasureItem(e)
 
        'メニューに表示する文字列を取得
        Dim menuText As String
        menuText = Text
        'シュートカットを表示するときは、その文字列を追加する
        If ShowShortcut = True And _
                Shortcut <> Shortcut.None Then
            menuText += _
                TypeDescriptor.GetConverter(GetType(Keys)). _
                ConvertToString(CType(Shortcut, Keys))
        End If
 
        Dim sf As New StringFormat()
        'ホットキープリフィックスの表示(&を_にする)
        sf.HotkeyPrefix = HotkeyPrefix.Show
 
        '幅を決定する
        e.ItemWidth = CInt( _
            e.Graphics.MeasureString(menuText, _
                SystemInformation.MenuFont, _
                Integer.MaxValue, _
                sf).Width) + widthExceptText
        'リソースを開放
        sf.Dispose()
 
        '高さを決定する
        e.ItemHeight = _
            Math.Max(SystemInformation.MenuFont.Height, topMargin) _
            + topMargin * 2
    End Sub
 
    'OnDrawItemのオーバーライド
    'メニュー項目を描画する
    Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs)
        Dim textBrush As Brush
 
        '基本クラスのOnDrawItemを処理する
        MyBase.OnDrawItem(e)
 
        If CBool(e.State And DrawItemState.Selected) Then
            '選択されている時
            '背景を塗りつぶす
            e.Graphics.FillRectangle(SystemBrushes.Highlight, _
                e.Bounds)
            '文字列の描画に使用するブラシの作成
            textBrush = New SolidBrush(SystemColors.HighlightText)
        Else
            '選択されていない時
            '背景を塗りつぶす
            e.Graphics.FillRectangle(SystemBrushes.Menu, e.Bounds)
            '文字列の描画に使用するブラシの作成
            textBrush = New SolidBrush(SystemColors.MenuText)
        End If
 
        '画像の描画
        If Not (_image Is Nothing) Then
            e.Graphics.DrawImage(_image, _
                e.Bounds.Left + leftMargin, _
                e.Bounds.Top + (e.Bounds.Height - imageHeight) \ 2, _
                imageWidth, _
                imageHeight)
        End If
 
        '文字列の描画の準備
        Dim sf As New StringFormat()
        'ホットキープリフィックスの表示(&をアンダーラインにする)
        sf.HotkeyPrefix = HotkeyPrefix.Show
 
        'Text部分の表示位置を決定する
        Dim textPosition As New PointF()
        textPosition.X = e.Bounds.Left + textLeft
        textPosition.Y = e.Bounds.Top + _
            (e.Bounds.Height - SystemInformation.MenuFont.Height) \ 2
 
        'Text部分の文字列を描画
        e.Graphics.DrawString(Text, _
            SystemInformation.MenuFont, _
            textBrush, _
            textPosition, _
            sf)
 
        'Shortcut部分を描画
        If ShowShortcut = True And _
                Shortcut <> Shortcut.None Then
            'Shortcut部分の文字列を取得
            Dim shortcutText As String = _
                TypeDescriptor.GetConverter(GetType(Keys)). _
                ConvertToString(CType(Shortcut, Keys))
            'Shortcut部分の幅を取得
            Dim shortcutWidth As Integer = _
                CInt( _
                e.Graphics.MeasureString(shortcutText, _
                    SystemInformation.MenuFont, _
                    Integer.MaxValue, _
                    sf).Width _
                )
            'Shortcut部分の表示位置を決定する
            Dim shortcutPosition As New PointF()
            shortcutPosition.X = _
                e.Bounds.Right - shortcutWidth - rightMargin
            shortcutPosition.Y = textPosition.Y
            'Shortcut部分の文字列を描画
            e.Graphics.DrawString(shortcutText, _
                SystemInformation.MenuFont, _
                textBrush, _
                shortcutPosition, _
                sf)
        End If
 
        'リソースを開放
        textBrush.Dispose()
        sf.Dispose()
    End Sub
End Class

次にこのImageMenuItemクラスの使い方を説明します。ImageMenuItemクラスを追加したいメニューには、MainMenuオブジェクトまたはContextMenuオブジェクトをそのまま使います。また、MainMenuの一番初めに登録するトップレベルメニュー項目にはMenuItemオブジェクトを使います。さらに、メニューのセパレータにもMenuItemオブジェクトを使います。

具体例を以下に示します。Form1にImageList1があり、画像が2つ以上登録されているものとします。ここではMainMenuオブジェクトを動的に作成していますが、フォームのデザインであらかじめ追加されていても問題ありません。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
Friend mainMenu1 As MainMenu
'トップレベルメニュー項目はMenuItemクラスを使用する
Friend WithEvents menuFile As MenuItem
'画像つきメニュー項目
Friend WithEvents menuNew As ImageMenuItem
Friend WithEvents menuOpen As ImageMenuItem
Friend WithEvents menuExit As ImageMenuItem
 
Private Sub Form1_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) _
        Handles MyBase.Load
    'メインメニューの作成
    mainMenu1 = New MainMenu()
 
    'トップレベルメニュー項目の作成
    menuFile = New MenuItem("ファイル(&F)")
 
    '画像つきメニュー項目の作成
    menuNew = New ImageMenuItem("新規作成(&N)")
    menuNew.Shortcut = Shortcut.CtrlN
    '画像を指定する
    'ImageList1に画像が登録されているものとする
    menuNew.Image = ImageList1.Images(0)
 
    menuOpen = New ImageMenuItem("開く(&O)")
    menuOpen.Shortcut = Shortcut.CtrlO
    menuOpen.Image = ImageList1.Images(1)
 
    menuExit = New ImageMenuItem("終了(&X)")
 
    'menuFileのサブメニューにする
    menuFile.MenuItems.Add(menuNew)
    menuFile.MenuItems.Add(menuOpen)
    'セパレータの追加
    menuFile.MenuItems.Add(New MenuItem("-"))
    menuFile.MenuItems.Add(menuExit)
 
    'MainMenuに追加する
    mainMenu1.MenuItems.Add(menuFile)
 
    'Form1のメインメニューとする
    Me.Menu = mainMenu1
End Sub
 
Private Sub menuNew_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles menuNew.Click
    MessageBox.Show("「新規作成」がクリックされました。")
End Sub
 
Private Sub menuOpen_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles menuOpen.Click
    MessageBox.Show("「開く」がクリックされました。")
End Sub
 
Private Sub menuExit_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles menuExit.Click
    Me.Close()
End Sub

だいぶよくなったように見えますが、これだけやっても実用には程遠い状態です。メニュー項目が無効になっているとき(EnabledがFalseになっているとき)の処理や、チェックが付いているとき(CheckedがTrueのとき)の処理ができていません。これらの問題をクリアしたクラスについてはまた別の機会にさせていただきます。

このメニューのオーナードローを使えば、Visual Studio .NETのようなXP風のメニューも作成できるでしょう。実際、そのようなクラスは多く公開されていますので、自作が面倒であれば利用させていただきましょう。私が調べたところでは、次のようなものがありました。

最後に補足ですが、現在NotifyIconコントロールのContextMenuではオーナードローできないというバグがあるようですので、ご注意ください。

コメント



ページ情報
  • カテゴリ : .NET
  • 作成日 : 2003-04-03 (木) 06:00:00
  • 作成者 : DOBON!
  • 最終編集日 : 2010-03-20 (土) 02:24:35
  • 最終編集者 : DOBON!
[ トップ ]   [ 編集 | 凍結 | 差分 | バックアップ | 添付 | 複製 | 名前変更 | リロード ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]