┏弟4号━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃         .NETプログラミング研究         ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜メニュー ■注意事項 ■.NET Tips ・メニューにアイコンを表示する 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 ─────────────────────────────── ■注意事項 ─────────────────────────────── 今回はコードが長いため、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"を表示させています。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'メニュー項目に表示させるアイコン 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 '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ うまくメニューに画像が表示できたことと思います。メニューのオー ナードローについては大体分かっていただけたでしょうか? この調子だと目的を達成するのも簡単そうに思えるでしょう。次のサ ンプルではさらに文字列を描画し、しかもメニュー項目が選択された ときに背景色と前景色が変わるようにしてみます。これでかなりそれ らしくなるはずなのですが・・・。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'メニュー項目に表示させるアイコン 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オブジェクトで画像を設定するようにしました。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 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オブジェクトを動的 に作成していますが、フォームのデザインであらかじめ追加されてい ても問題ありません。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 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風のメニューも作成できるでしょう。実際、そのようなクラスは 多く公開されていますので、自作が面倒であれば利用させていただき ましょう。私が調べたところでは、次のようなものがありました。 The Code Project Visual Studio .NET Menu Style By Carlos H. Perez http://www.codeproject.com/cs/menu/vsnetmenu.asp C# Help .NET Menu Style Revisited By Francesco Natali http://www.csharphelp.com/archives2/archive416.html Planet Source Code Create a Menu like as VS.NET Menu By KrsMurali http://www.rentacoder.com/vb/scripts/ShowCode.asp?txtCodeId=1074&lngWId=10 Planet Source Code Another XPStyle menu By Mick Doherty http://www.rentacoder.com/vb/scripts/ShowCode.asp?txtCodeId=961&lngWId=10 最後に補足ですが、現在NotifyIconコントロールのContextMenuではオー ナードローできないというバグがあるようですので、ご注意ください。 =============================== ■このマガジンの購読、購読中止、バックナンバー、説明に関しては  次のページをご覧ください。  http://www.mag2.com/m/0000104516.htm ■発行人・編集人:どぼん!  http://dobon.net  dobon@bigfoot.com ■ご質問等はメールではなく、掲示板へお願いいたします。  http://dobon.net/bbs ■上記メールアドレスへのメールは確実に読まれる保障はありません  (スパム、ウィルス対策です)。メールは下記URLのフォームメール  から送信してください。  http://dobon.net/mail.html Copyright (c) 2003 DOBON! All rights reserved. ===============================