はじめに、オーナードローとはメニューアイテムなどの描画処理をWindows側ではなくプログラム側(オーナー)で行うことを言います。 プログラムで描画処理を記述することができるので、Windowsの画一的なデザインのメニューではなく、全くオリジナルの個性的なメニューを作ることができます。 また、メニューだけでなく、リストボックスなどでもオーナードローを行うことができます。
下図のスクリーンショトは自作のランチャーソフトSylpheenのメニュー項目なのですが、その左側はWindowsがデフォルトで描画したもの、右側がオーナードローで描画したものです。 背景や文字色、選択されている項目のハイライトカラーなどWindowsのものとは異なります。 また、アイコンが表示されていることからもわかるとおり、絵なども表示することができます。 オーナードローでは描画対象のGraphicsオブジェクトが渡されるので、このGraphicsに対して描画処理を施せばこのようなメニューを作ることができます。
Visual Basicでこれを行うにはAPIなどを駆使してやらなければならなかったのですが、VB.NETではその必要がなく、イベントハンドラで行うことができるのでかなり楽になったといえます。
オーナードローを行うには
オーナードローを行うためには、まずOwnerDrawプロパティをTrueに設定します。 Falseにしておくと、オーナードローしない、つまり Windowsに描画を任せてしまうことになります。 次に、MeasureItem、DrawItemイベントに適切なイベントハンドラを割り当てます。 MeasureItemは描画する際に必要な項目の寸法を知るためのものです。 これは項目が表示される直前に呼び出されます。 DrawItem は実際に描画を行うためのものです。 当然のごとく描画が必要になった際に呼び出されます。 ひとまず簡単なサンプルを作ったのでそのソースを見てみましょう。
このサンプルでは何もコントロールを配置していないフォーム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)」
実際にオーナードローがなされているか確認するにはフォーム上の任意の点で右クリックしてコンテキストメニューを表示させるだけです。
ListBoxのオーナードロー
メニューアイテムでオーナードローをする方法はわかりました。 ただ、いまいちオーナードローっぽくないサンプルだったので、リストボックスの場合でもう一度やってみます。
リストボックスの場合はOwnerDrawプロパティではなくDrawModeプロパティをOwnerDrawFixedまたは OwnerDrawVariableに指定することでオーナードローを有効にします。 Fixedの方は、項目のサイズは変更できず、デフォルトの項目の大きさでしか描画できません。 対してVariableは項目の大きさを変更して描画することができます。
ここではOwnerDrawVariableを指定してリストボックスでオーナードローを行い、色指定用のリストメニューを作成してみることにします。 コーディングする前にフォーム上にlistBoxColorという名前でリストボックスを追加しておいて下さい。
このプログラムを実行するとこのような感じになります。 右は同様のことをドロップダウンリストのコンボボックスで行ってみた例です。 多少のコード変更だけで作れます。
MenuItemのオーナードロー
まず始めに、MenuItemクラスを用いてオーナードローを行う場合、OwnerDrawプロパティをTrueに設定し、MeasureItem及び DrawItemイベントに適切なイベントハンドラを指定します。 MeasureItemではオーナードローされる場合における項目毎の寸法を計算するもで、DrawItemでは実際にオーナードローを行います。 基本的にMeasureItemイベントハンドラでは、メニュー項目で指定されている文字列のサイズを計算し、それをもって項目のサイズとします。 また、オーナードローを使用するでは、Textプロパティに"-"を指定してもセパレータとしては扱われないので、この文字列が指定されているメニュー項目ではセパレータの描画をするコードを付け加えなければなりません。
次のコードでは、オーナードローされたコンテキストメニューを作成し、テキストボックスが右クリックされたときに表示させるコンテキストメニューを指定しています。 これを実行する場合は、テキストボックスを右クリックすればコンテキストメニューが表示されるはずです。
デフォルト指定の描画
オーナードローをする場合でも、描画処理をデフォルトのシステムカラーなどで行うこともできます。 DrawItemEventArgsクラスには、背景やフォーカスを描画するメソッドや、前景・背景色、フォントなどのプロパティがあるため、これらを参照することができます。 次のコードは実際にそれを用いて描画した場合の例です。 MenuItem_DrawItem()メソッド以外はすべて前のコードと同じです。
ただ、この場合でもセパレータだけは自分で描画する必要があります。
メニュー項目の状態の取得と描画
チェック状態や選択状態を取得するには、DrawItemEventArgs.Stateプロパティを参照します。 このメンバは列挙型の値で、メニュー項目に関する様々な状態が格納されます。 この状態にあわせてオーナードローする場合は、次のようにして状態ごとに描画処理を記述します。
この方法によってチェック状態の判別などはができますが、メニュー項目の横にチェックマークなどは入らないので、これも独自に描画しなければなりません。 独自に描画するのがめんどくさい場合は、ControlPaintクラスのDrawMenuGlyph()メソッドを使用することができます。 ただ、このメソッドではチェックマークは描画されますが、白い背景も同時に描画されてしまうので、デザインを重視する場合はやはり独自の描画をするべきかと思います。
このコードでは「コピー」の項目のCheckedプロパティをTrue、「貼り付け」EnabledプロパティをFalseに指定してオーナードローしたものです。 淡色表示やチェックマークの表示はMenuItem_DrawItem()メソッドで行いますが、チェックマークを挿入するために MenuItem_MeasureItem()でのサイズ計算の式を多少変更しています。 具体的には、文字を横にずらすために幅を少し広めに取っています。
タスクトレイ上のコンテキストメニューに関するバグ
結論から先に言うと、オーナードローを利用したコンテキストメニューをNotifyIconクラスのインスタンスに対して指定した場合、正しく描画されません。 まず、次のようにコードを変えてタスクトレイ上にアイコンを表示させます。
そして、これをコンパイル・実行したあと、タスクトレイに現れたアイコンを右クリックすると次のようになってしまいます。
これは意図的にこのようにしたのではなく、本当にこのようになってしまいます。 つまり、タスクトレイ上のアイコンからはオーナードローしたメニューを正しく表示することができないのです。 これはどうも.NET Framework自体のバグのようなので、これを回避する唯一の手段は、オーナードローを使用しないことしかありません。