オーナードロー機能を活用しよう

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

1.オーナードローとは


オーナードロー
 はじめに、オーナードローとはメニューアイテムなどの描画処理をWindows側ではなくプログラム側(オーナー)で行うことを言います。 プログラムで描画処理を記述することができるので、Windowsの画一的なデザインのメニューではなく、全くオリジナルの個性的なメニューを作ることができます。 また、メニューだけでなく、リストボックスなどでもオーナードローを行うことができます。
 左のスクリーンショトは自作のランチャーソフトのメニュー項目なのですが、その左側はWindowsがデフォルトで描画したもの、右側がオーナードローで描画したものです。 背景や文字色、選択されている項目のハイライトカラーなどWindowsのものとは異なります。 また、アイコンが表示されていることからもわかるとおり、絵なども表示することができます。 オーナードローでは描画対象のGraphicsオブジェクトが渡されるので、このGraphicsに対して描画処理を施せばこのようなメニューを作ることができます。
 Visual Basicでこれを行うにはAPIなどを駆使してやらなければならなかったのですが、VB.NETではその必要がなく、イベントハンドラで行うことができるのでかなり楽になったといえます。

2.オーナードローするには


 オーナードローを行うためには、まずOwnerDrawプロパティをTrueに設定します。 Falseにしておくと、オーナードローしない、つまりWindowsに描画を任せてしまうことになります。 次に、MeasureItem、DrawItemイベントに適切なイベントハンドラを割り当てます。 MeasureItemは描画する際に必要な項目の寸法を知るためのものです。 これは項目が表示される直前に呼び出されます。 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
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
Public Class formMain
    Inherits System.Windows.Forms.Form

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

    Dim menuMain As ContextMenu
    Dim menuItem1 As MenuItem
    Dim menuItem2 As MenuItem
    Dim menuItem3 As MenuItem
    Dim menuItem4 As MenuItem

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

        menuMain = New ContextMenu()

        menuItem1 = New MenuItem("Item1")
        SetOwnerDrawProperties(menuItem1)

        menuItem2 = New MenuItem("Item2")
        SetOwnerDrawProperties(menuItem2)

        menuItem3 = New MenuItem("Item3")
        SetOwnerDrawProperties(menuItem3)

        menuItem4 = New MenuItem("Item4")
        SetOwnerDrawProperties(menuItem4)

        menuMain.MenuItems.Add(menuItem1)
        menuMain.MenuItems.Add(menuItem2)
        menuMain.MenuItems.Add(menuItem3)
        menuMain.MenuItems.Add(menuItem4)

        Me.ContextMenu = menuMain

    End Sub

    Private Sub SetOwnerDrawProperties(ByVal item As MenuItem)

        item.OwnerDraw = True
        AddHandler item.MeasureItem, AddressOf MenuItem_MeasureItem
        AddHandler item.DrawItem, AddressOf MenuItem_DrawItem

    End Sub

    Private Sub MenuItem_MeasureItem(ByVal sender As System.Object, ByVal e As Windows.Forms.MeasureItemEventArgs)

        e.ItemWidth = 200
        e.ItemHeight = 15

    End Sub

    Private Sub MenuItem_DrawItem(ByVal sender As System.Object, ByVal e As Windows.Forms.DrawItemEventArgs)

        Dim item As MenuItem = CType(sender, MenuItem)

        e.DrawBackground()

        e.Graphics.DrawString(item.Text, Control.DefaultFont, SystemBrushes.ControlText, e.Bounds.X + e.Index * 5, e.Bounds.Y)

        e.DrawFocusRectangle()

    End Sub


End Class
実行結果
実行結果

 このサンプルでは何もコントロールを配置していないフォームformMainにソースコードでコンテキストメニューを作成、さらにメニューアイテムを追加しています。 さらにオーナードローに必要なソースコードも記述されています。
 ソースコードを部分毎に説明していきます。 まず13〜36行目までの間では、コンテキストメニューmenuMain及び四つのメニューアイテムmenuItem1〜4をを作成し、各々のメニューアイテムをコンテキストメニューのMenuItemsコレクションに追加します。 最後にmainMenu自体もフォームのコンテキストメニューとして登録します。 コンテキストメニューはコントロール上で右クリックした際に表示されるメニューのことです。 そのため、自動的に表示されるので特別表示のためのイベントハンドラは必要ありません。 SetOwnerDrawProperties()ではオーナードローに必要なプロパティの設定を行う処理を記述しています。
 38〜44行目のSetOwnerDrawProperties()では、引数にとったMenuItemオブジェクトのOwnerDrawプロパティをTrueにし、MeasureItem・DrawItemイベントに適切なイベントハンドラMenuItem_MeasureItem・MenuItem_DrawItemを追加しています。
 46〜51行目のMenuItem_MeasureItem()では特定の項目の寸法を計算するためのイベントハンドラで、本来ならば表示すべきテキストの幅などから算出するべきなのですが、ここでは簡単のため幅・高さに一律200×15ピクセルを指定しています。
 53〜63行目のMenuItem_DrawItem()では実際に特定の項目の描画を行うイベントハンドラです。 引数のDrawItemEventArgsにはGraphicsプロパティがあり、これに対して描画処理を施してやります。 まず、55行目でイベント送信元のメニューアイテムを取得します。 つぎに57行目で項目の背景を描画しています。 このメソッドはデフォルトの描画処理を行うものでオーナードローを行わない場合の背景と同じものが描画されます。 61行目も同様に前景を描画するメソッドです。 59行目でメニュー項目の文字を描画しています。 変化を持たせるために項目のインデックスが増えるにつれ横に5ピクセルずつずれるように描画しています。
 59行目について、回りくどい方法で記述してしまいましたが、DrawItemEventArgsにはFontおよびForeColorプロパティがあるので(後で気づいた・・・)この行は次のように簡略化することができます。 「e.Graphics.DrawString(item.Text, e.Font, New SolidBrush(e.ForeColor), e.Bounds.X + e.Index * 5, e.Bounds.Y)
 実際にオーナードローがなされているか確認するにはフォーム上の任意の点で右クリックしてコンテキストメニューを表示させるだけです。

3.リストボックスでオーナードロー


 メニューアイテムでオーナードローをする方法はわかりました。 ただ、いまいちオーナードローっぽくないサンプルだったので、リストボックスの場合でもう一度やってみます。
 リストボックスの場合はOwnerDrawプロパティではなくDrawModeプロパティをOwnerDrawFixedまたはOwnerDrawVariableに指定することでオーナードローを有効にします。 Fixedの方は、項目のサイズは変更できず、デフォルトの項目の大きさでしか描画できません。 対してVariableは項目の大きさを変更して描画することができます。
 ここではOwnerDrawVariableを指定してリストボックスでオーナードローを行い、色指定用のリストメニューを作成してみることにします。 コーディングする前にフォーム上にlistBoxColorという名前でリストボックスを追加しておいて下さい。
リストボックスでのオーナードロー
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
Public Class formMain
    Inherits System.Windows.Forms.Form

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

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

        listBoxColor.DrawMode = DrawMode.OwnerDrawVariable

        AddHandler listBoxColor.MeasureItem, AddressOf ListBox_MeasureItem
        AddHandler listBoxColor.DrawItem, AddressOf ListBox_DrawItem

        listBoxColor.Items.Clear()

        listBoxColor.BeginUpdate()

        listBoxColor.Items.Add(Color.Red)
        listBoxColor.Items.Add(Color.Orange)
        listBoxColor.Items.Add(Color.Yellow)
        listBoxColor.Items.Add(Color.GreenYellow)
        listBoxColor.Items.Add(Color.Green)
        listBoxColor.Items.Add(Color.Blue)
        listBoxColor.Items.Add(Color.Purple)
        listBoxColor.Items.Add(Color.ForestGreen)
        listBoxColor.Items.Add(Color.MediumSlateBlue)
        listBoxColor.Items.Add(Color.SkyBlue)
        listBoxColor.Items.Add(Color.Cornsilk)

        listBoxColor.EndUpdate()

    End Sub

    Private Sub ListBox_MeasureItem(ByVal sender As System.Object, ByVal e As Windows.Forms.MeasureItemEventArgs)

        e.ItemWidth = 200
        e.ItemHeight = 20

    End Sub

    Private Sub ListBox_DrawItem(ByVal sender As System.Object, ByVal e As Windows.Forms.DrawItemEventArgs)

        ' 背景を白でクリア
        e.Graphics.FillRectangle(New SolidBrush(Color.White), e.Bounds)

        Dim listBox As ListBox = CType(sender, ListBox)
        Dim col As Color = CType(listBox.Items(e.Index), Color)
        Dim b As New SolidBrush(col)

        ' リストアイテムの色で■を描画
        e.Graphics.FillRectangle(b, New Rectangle(e.Bounds.X + 5, e.Bounds.Y + 5, 10, 10))

        Dim sizeText As SizeF = e.Graphics.MeasureString(col.ToString, e.Font)

        ' リストアイテムのテキストを描画
        e.Graphics.DrawString(col.ToString, e.Font, b, e.Bounds.X + 25, e.Bounds.Y + (20 - sizeText.Height) / 2)

        ' 項目が選択されている場合
        If e.State And DrawItemState.Selected Then

            Dim boundFocus As New Rectangle(e.Bounds.X + 1, e.Bounds.Y + 1, listBox.GetItemRectangle(e.Index).Width - 2, listBox.GetItemRectangle(e.Index).Height - 2)
            Dim colFocus As Color = Color.FromArgb(&H20, col.R, col.G, col.B) ' αチャンネルを使用
            Dim bFocus As New SolidBrush(colFocus)
            Dim p As New Pen(col)

            ' ハイライト表示
            e.Graphics.FillRectangle(bFocus, boundFocus)
            e.Graphics.DrawRectangle(p, boundFocus)

            bFocus.Dispose()
            p.Dispose()

        End If

        b.Dispose()

    End Sub


End Class
実行結果
実行結果

 このプログラムを実行するとこのような感じになります。 右のはドロップダウンリストのコンボボックスに変えてみた例です。 多少のコード変更だけで作れます。
 このように、オーナードローを用いれば以外と簡単におしゃれなデザインのコントロールが作れてしまいます。 WindowsXPになって多少はデザインが洗練されてきたWindowsのGUIですが、まだまだ不満があるという人はこのオーナードローを駆使してかっこいいデザインのアプリケーションを作ってみるのもいいでしょう。