更新日: 2010 年 7 月 2 日

執筆者: エディフィストラーニング株式会社 矢嶋 聡

この記事は、「MSDN プログラミング シリーズ」として発行している技術書籍「ステップアップ Visual Basic 2010 ~開発者がもう一歩上達するための必読アドバイス」(日経 BP 社刊) を基に先進的なテクニックを紹介しています。


目次

  1. はじめに
  2. イベントハンドラーの動的な設定 ~コントロール配列でハンドラーを共有する~
  3. イベント、デリゲート インスタンス、およびイベントハンドラーの関係
  4. イベントハンドラーの動的な解除と注意点
  5. 型が違うデリゲート同士の互換性
  6. デリゲートとイベントハンドラーとの間の互換性 ~複数イベントでハンドラーを共有する~

1. はじめに

今回は、アプリケーションの実行中に、プログラム コードを用いて、イベントハンドラーを動的に設定したり、解除したりする方法と、それらに関するさまざまな注意点を取り上げます。

たしかに、Visual Basic のフォーム デザイナーを使用すれば、マウス操作でフォームの画面構成やイベントハンドラーの作成を簡単に行えます。このため、(プログラム コードによる) 動的な設定は必要ないように思われます。しかし、プログラムを実行するまで画面上のコントロールの数が決まらない場合や、画面構成が複雑でコードで書いたほうが簡単な場合など、実行中に画面を構築するほうが良い場合もあります。そのようなケースでは、イベントハンドラーも動的な設定が必要になってきます。

ページのトップへ


2. イベントハンドラーの動的な設定 ~コントロール配列でハンドラーを共有する~

前回 (第 28 回) にも触れたように、イベントでは、1 つのイベントの発生に対して、複数のメソッドを呼び出す「マルチ キャスト」を実現できます。それでは、逆に複数のコントロールのイベントを 1 つのイベントハンドラーで受けることができるでしょうか? もちろん、これも可能です。

まずは、Visual Basic 6.0 を利用されている方にはお馴染みの「コントロール配列」(複数のコントロールから構成される配列) を .NET 版の Visual Basic で構築する下記のサンプルを用いて、「コントロール配列」の各コントロールが発生させるイベントを 1 つのイベントハンドラーで処理する方法を確認してみましょう。

なお、以下の例 1 を実行すると、画面構成はこの後の図 1 のようになります。(30 個のボタンの集合は、実行時のコードで生成するので、デザイナー上は用意する必要はありません。以下のコードを実行するのであれば、30 個のボタン以外の部分を、あらかじめフォーム デザイナー上で構築してください。)

例 1. コントロール配列の動的な生成とイベントハンドラーの設定

Public Class Form1

     Public Sub New()
          InitializeComponent() 'この呼び出しはデザイナーで必要です。
          MakeButtons()
     End Sub

     Private buttons() As Button 'ボタンの配列変数 ←[1]

     Private Sub MakeButtons() 'ボタン配列の作成 ←[2]
          Me.SuspendLayout()
          Dim offsetTop As Integer = 150
          buttons = New Button(29) {}
          For ndx As Integer = 0 To 29
               buttons(ndx) = New Button()
               With buttons(ndx) '1個分のボタンの設定
                    .Text = CStr(ndx)
                    .Tag = ndx'ボタンごとの固有情報(任意)
                    .Size = New Size(30, 24)
                    .Location = New Point(10 + (30 * (ndx Mod 10)),
                                            offsetTop)
                    .TextAlign = ContentAlignment.MiddleCenter
               End With
               If (ndx Mod 10) = 9 Then offsetTop += 25 '10個表示したら次段に表示
          Next
          Me.Controls.AddRange(buttons)
          Me.ResumeLayout()
     End Sub

     Private Sub Numbers_Click(ByVal source As Object, ByVal args As EventArgs)←[3]
          Dim button = TryCast(source, Button) ←[4]
          Dim data As String = ""
          If button IsNot Nothing Then
               data = CStr(button.Tag) ←[5]
          End If
          ListBox1.Items.Add("Numbers_Click : " & data)
          ListBox1.TopIndex = ListBox1.Items.Count - 1
     End Sub

     Private Sub Button1_Click(...) Handles Button1.Click

          For Each btn As Button In buttons
               AddHandler btn.Click, New EventHandler(AddressOf Me.Numbers_Click)←[6]
          Next

     End Sub

End Class

この例 1 のサンプルを実行すると、ボタン 30 個が配置されたフォーム (図 1 参照) が開きます。このフォームでは、下図左上の [Button1] を 1 回クリックすると、数字が刻印された 30 個のボタンに対して 1 つのイベントハンドラーが動的に (上記のプログラム コードにより) 設定されます。その後で、30 個のボタンのいずれかをクリックすると、そのボタンに対応した数字がリストボックスに表示されます。

図 1. 30 個のボタンを持つフォーム

例 1 のコードでは、30 個のどのボタンが押されても、[3] のイベントハンドラーが実行されます。どのボタンが押されたかは、[3] の 1 番目の引数 source で判断できるので、[4] で Button 型に変換した後、[5] で各ボタン固有の Tag プロパティの値を調べ、リストボックスに表示します。(Tag プロパティの値は、[2] のメソッドの中であらかじめ設定しています。)

肝心のイベントハンドラーの設定は [6] です。ボタンの数だけ AddHandler ステートメントを繰り返し呼び出し、それぞれのボタンの Click イベントに対して、Numbers_Click メソッド ([3] のメソッド) を呼び出すデリゲート インスタンスを設定します。

この設定手順は単純ですが、注意すべき点がいくつかあります。そこで、次に、このデリゲート インスタンスの性質をさらに確認してみましょう。

ページのトップへ


3. イベント、デリゲート インスタンス、およびイベントハンドラーの関係

前述の例 1 の [6] では、第 28 回でも述べたように、デリゲート インスタンスの型を推論できるので、インスタンス作成の表記を省略して以下のように AddressOf 句だけでもかまいません。

例 2. インスタンス生成の表記を省略、暗黙的にインスタンスを作成

          For Each btn As Button In buttons
               AddHandler btn.Click, AddressOf Me.Numbers_Click
          Next

いずれの表記でも、ボタンの数だけデリゲート インスタンスが新規作成 (ここでは 30 個) されている点に注意してください。(それぞれが同一のイベントハンドラーを指しています。) このように、デリゲートの基本的な性質の 1 つとして、複数のデリゲート インスタンスを作成して、それらが単一のメソッドを参照できます。

また一方で、単一のデリゲート インスタンスを複数のボタンのイベントで共有することもできます。つまり、次の例 3 のように記述しても、例 2 と同様に単一のイベントハンドラーを共有できます。

例 3. AddHandler ステートメントとは別にインスタンスを作成

          Dim handler As New EventHandler(AddressOf Me.Numbers_Click)
          For Each btn As Button In buttons
               AddHandler btn.Click, handler
          Next

前者の例 2 のほうが、AddHandler ステートメント 1 つで行いたいことが書かれており、より表現が簡潔ですが、後者の例 3 は、ボタンの数に関わらず、作成されるデリゲート インスタンスは 1 つだけで、リソースの節約になります。一般にユーザー インターフェイスであれば、このリソース消費の差が、アプリケーションのパフォーマンス要件に大きく影響するとは考えづらいですが、サーバーのマルチユーザー環境下で、これに類似したロジックを持つアプリケーションを大量に並行実行させるような場合には、こうしたリソース消費が無視できない場合もあるでしょう。

Note: 参考までに、著者の環境で、例 2 と例 3 のループをそれぞれ 1000 回行うよう修正して、 Release 版としてビルドし、Visual Studio 2010 の「パフォーマンス分析」ツールで消費メモリを測定したところ、例 2 では 32000 バイトのメモリを消費したのに対して、例 3 のほうの消費メモリはわずか 32 バイトでした。

ページのトップへ


4. イベントハンドラーの動的な解除と注意点

次に、動的なイベントハンドラーの解除方法について確認します。次の例 4 は、例 1 で設定した 30 個のボタンのイベントハンドラーを解除する例です。(この処理は、図 1 の [Button2] ボタンの Click イベントハンドラーに記述することにします。)

例 4. イベントハンドラーの解除

     Private Sub Button2_Click(...) Handles Button2.Click

          For Each btn As Button In buttons
               RemoveHandler btn.Click, New EventHandler(AddressOf Numbers_Click)←[1]
          Next

          'For Each btn As Button In buttons ←[2]
          '     RemoveHandler btn.Click, AddressOf Numbers_Click
          'Next

          'Dim handler1 As New EventHandler(AddressOf Me.Numbers_Click) ←[3]
          'For Each btn As Button In buttons
          '     RemoveHandler btn.Click, handler1
          'Next

     End Sub

解除の場合は、AddHandler キーワードの代わりに、[1] の太線部分のように RemoveHandler キーワードを使います。キーワードの後の引数は同様です。2 つ目の引数には、解除したいデリゲート インスタンスを指定します。もちろん、[2] のループや、その後の [3] のコード ブロックのように記述することもできます。

ここで、1 つ疑問に思われるかも知れません。「RemoveHandler ステートメントで解除する際には、AddHandler ステートメントで設定したデリゲート インスタンスと同じインスタンス (の参照) を指定すべきではないか?」と。

実は、その必要はありません。デリゲート インスタンスでは、型が同じで、呼び出し先のメソッドが同じならば、実際はデリゲート インスタンス自体が別物でも等価とみなされます。(そのため、呼び出し先が同じなら、[1] のように解除の際に新しいインスタンスを指定して構いません。)ただし、[1] や [2] のループでは、削除したい数だけ、新たにインスタンスを作成することになるので、大量に削除する場合には、やはり、リソース消費に留意する必要があるでしょう。

以上、イベントハンドラーの設定と解除を確認しました。ただし、ここまでは同じ型のイベント (ボタンの Click イベント) に関して、1 つのイベントハンドラーを共有しました。それでは、型が違うイベントはイベントハンドラーを共有できるのでしょうか? 例えば、Click イベントと MouseMove イベントでは、引数が異なるのですが、同一のイベントハンドラーを流用できるのでしょうか? 次に、この点を確認してみます。

ページのトップへ


5. 型が違うデリゲート同士の互換性

原則として、イベントの型 (引数の形式) と、設定するデリゲート インスタンスの型、そして、イベントハンドラーの型は、一致させる必要があります。しかし、いくつか例外もあるのです。これを考えるにあたり、まず、デリゲートの型変換の性質から改めて確認してみます。

以下の例 5 では、デリゲートの型が異なるもの同士で、いつくかの操作をしてみました。(この操作は、図 1 の [Button3] ボタンの Click イベントハンドラーに記述することにします。)

例 5. 型のと異なるデリゲート間での変換 (コンパイル エラー)

'Public Delegate Sub EventHandler (sender As Object, e As EventArgs) ←[1]
'Public Delegate Sub MouseEventHandler (sender As Object, e As MouseEventArgs )

Private Sub Button3_Click(...) Handles Button3.Click

     'NG! 以下の 4つはコンパイルエラー! ←[2]
     AddHandler Button1.Click, New MouseEventHandler(AddressOf MouseMove_Event)
     AddHandler Button1.MouseMove, New EventHandler(AddressOf Any_Event)
     Dim handler1 As EventHandler =
          TryCast(New MouseEventHandler(AddressOf MouseMove_Event), EventHandler)
     Dim handler2 As MouseEventHandler =
          TryCast(New EventHandler(AddressOf Any_Event), MouseEventHandler)

End Sub

Private Sub Any_Event(ByVal source As Object, ByVal e As EventArgs) ←[3]

     ListBox1.Items.Add("Any Log:" & Date.Now.TimeOfDay.ToString())
     ListBox1.TopIndex = ListBox1.Items.Count - 1

End Sub

Private Sub MouseMove_Event(ByVal source As Object, ByVal e As MouseEventArgs) ←[4]

     ListBox1.Items.Add("MouseMove Log:" & Date.Now.TimeOfDay.ToString())
     ListBox1.TopIndex = ListBox1.Items.Count - 1

End Sub

上記 [1] の 2 行のコメントは、参考として Click イベントと MouseMove イベントのデリゲートの型を示しました。それぞれの型に合わせたイベントハンドラーとして、[3] の Any_Event メソッドと [4]の MouseMove_Event メソッド を用意しています。

[2] 以降の 4 つのステートメントが、型を合わせずに操作を行った例です。例えば、1 つ目の AddHandler では、EventHandler 型の Click イベントに対して、MouseEventHandler のデリゲート インスタンスを設定しました。これはコンパイルできません。また、3 つ目の TryCast キーワードを使用した型変換でも、MouseEventHandler 型のインスタンスを EventHandler 型に変換してみました。これもできません。このように、デリゲート インスタンス自体は型を変えることはできないのです。

ただし、ジェネリック デリゲートを使えば、一定の条件下で型変換できます。ジェネリック デリゲートでは、2 つの同じ名前のデリゲートにおいて型パラメーターが異なるとき、2 つの型パラメーター同士が特定の条件を満たすと、その 2 つのデリゲート間で型変換が可能です。例えば、次の例 6 のような変換が可能です。(この例は、同じく、前述の Button3_Click イベントハンドラーに記述したものとします。)

例 6. ジェネリック デリゲートにおける互換性

     ' Public Delegate Sub Action(Of In T1, In T2) ( arg1 As T1, arg2 As T2) ←[1]

     Dim handler1 As Action(Of Object, EventArgs) ←[2]
     Dim handler2 As Action(Of Object, MouseEventArgs) ←[3]
     handler1 = New Action(Of Object, EventArgs)(AddressOf Any_Event)
     handler2 = TryCast(handler1, Action(Of Object, MouseEventArgs)) ←[4]

この例では、[1] のコメントで示した既存ライブラリのクラス (.NET Framework が既定で持っているクラス) の Action(Of In T1, In T2) デリゲート (これは、ジェネリック デリゲートです) を使用しました。[2] と [3] では太線部分のように、このデリゲートを使用して、型パラメーターだけ違うデリゲートをそれぞれ定義しました。 [2] で使用されている型パラメーターの EventArgs クラスは、[3] で使用されている型パラメーターの MouseEventArgs クラスの基本クラスであり、継承関係があります。このとき、[1] の型パラメーター T2 の定義には In 修飾子が付いているので、Action(Of Object, 派生クラス) から Action(Of Object, 基本クラス) への型変換が可能であり、結局 [4] のように、ジェネリック デリゲートの型変換ができるのです。

しかし、今回の Click イベントと MouseMove イベントの型は、残念ながら、ジェネリック デリゲートではないので、この方法は利用できません。

Note: 今回は型パラメーターの In 修飾子 (反変の指定) は使用しないので、詳細は割愛します。詳しくは、以下のアドレスを参照してください。

ページのトップへ


6. デリゲートとイベントハンドラーとの間の互換性 ~複数イベントでハンドラーを共有する~

実は、デリゲート インスタンスとイベントハンドラーとの間も、ある一定の条件下では、必ずしも型が一致 (引数の形式が一致) する必要はないのです。デリゲート インスタンスの引数の型よりも、イベントハンドラーの引数の型のほうが汎用性がある (継承元である) 場合は、互換性があるとみなされます。この性質を利用して、次の例 7 のように、Click イベントと MouseMove イベントを 1 つのハンドラーで共有できます。(以下は、同様に、例 5 の Button3_Click イベントハンド内に記述します。また、例 5 の [3] の Any_Event メソッドを参照しています。)

例 7. 異なるイベントでのハンドラーの共有

     AddHandler Button1.Click, New EventHandler(AddressOf Any_Event)
     AddHandler Button1.MouseMove, New MouseEventHandler(AddressOf Any_Event)

上記の 2 つの太線部分のイベントに対しては、それぞれに合致した型のデリゲートを指定している点に注意してください。しかし、AddressOf 句に指定したハンドラーは同一であり、例 5 の [3] にある Any_Event メソッドを共有しています。
この 2 つの AddHandler ステートメントのうち、後者の MouseEventHandler 型インスタンスは、Any_Event メソッドと型が一致しません。しかし、MouseMove イベントが発生した際に渡される引数 MouseEventArgs は、基本クラスである EventArgs に型変換できるので問題ないのです。Option Strict On に設定しても、これはコンパイルできます。

以上、動的な設定に関する様々なバリエーションや注意点について確認しました。今後、きめ細かい画面構成の制御をする際に、これらの知識を有効活用してみてください。


Code Recipe .NET Framework デベロッパー センター

ページのトップへ