.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 = _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.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
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
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
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
Protected Overrides Sub OnMeasureItem( _
ByVal e As MeasureItemEventArgs)
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
Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs)
Dim textBrush As Brush
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
Dim textPosition As New PointF()
textPosition.X = e.Bounds.Left + textLeft
textPosition.Y = e.Bounds.Top + _
(e.Bounds.Height - SystemInformation.MenuFont.Height) \ 2
e.Graphics.DrawString(Text, _
SystemInformation.MenuFont, _
textBrush, _
textPosition, _
sf)
If ShowShortcut = True And _
Shortcut <> Shortcut.None Then
Dim shortcutText As String = _
TypeDescriptor.GetConverter(GetType(Keys)). _
ConvertToString(CType(Shortcut, Keys))
Dim shortcutWidth As Integer = _
CInt( _
e.Graphics.MeasureString(shortcutText, _
SystemInformation.MenuFont, _
Integer.MaxValue, _
sf).Width _
)
Dim shortcutPosition As New PointF()
shortcutPosition.X = _
e.Bounds.Right - shortcutWidth - rightMargin
shortcutPosition.Y = textPosition.Y
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
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
menuNew.Image = ImageList1.Images(0)
menuOpen = New ImageMenuItem("開く(&O)")
menuOpen.Shortcut = Shortcut.CtrlO
menuOpen.Image = ImageList1.Images(1)
menuExit = New ImageMenuItem("終了(&X)")
menuFile.MenuItems.Add(menuNew)
menuFile.MenuItems.Add(menuOpen)
menuFile.MenuItems.Add(New MenuItem("-"))
menuFile.MenuItems.Add(menuExit)
mainMenu1.MenuItems.Add(menuFile)
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ではオーナードローできないというバグがあるようですので、ご注意ください。
コメント †