Visual Basic 初級講座
VB.NET 2002 対応 VB.NET 2003 対応 VB2005 対応

 

Visual Basic 中学校 > 初級講座 >

第49回 イベントの作成

クラスでイベントを定義・発生させる方法を説明し、イベントの利用方法を具体例を挙げて説明します。

概要

・イベントを定義するにはEventを使う。

例:Public Event Pop(ByVal sender As Object, ByVal e As EventArgs)

・イベントを発生させるにはRaiseEventを使う。

例:RaiseEvent Pop(Me, New EventArgs)

・変数のイベントを使うには変数を宣言するときにWithEventsを使う。

例:Dim WithEvents Tester1 As New Tester

・イベントを使って双方向で情報をやりとりする方法

 

1.クラスのイベント

 

イベントはクラス間の連携手段の1つです。通常のプログラムでもButtonClickイベントやTextBoxKeyPressイベントなどさまざまなイベントを 使ってFormButtonクラスやTextBoxクラスとの連携を行っています。

イベントというとこのようなコントロールのイベントが真っ先に頭に浮かんでしまいますが、通常のクラスにもイベントを実装することができます。コントロールではないクラスにイベントを実装して何に使うかは後で説明することにしてまずはイベントを自由自在に扱えるようにしましょう。

なお、コントロールもクラスの一種ですから将来コントロールを作成するときにはここで説明しているイベントに関する事柄を活用することになります。コントロールの作成については中級講座で扱います。

 

2.イベントの宣言と発生

 

イベントもメソッドやプロパティと同様に宣言しなければなりません。また宣言とは別にイベントを発生させるコードを書く必要もあります。

宣言はEventキーワード(読み方:Event = イベント)を使って次のようにします。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class Tester

    Public Event Pop(ByVal sender As Object, ByVal e As EventArgs)

End Class

■リスト1:イベントの定義

この例ではTesterというクラスにPopというイベントを宣言しています。プロパティやメソッドと違って内容の記述はありません。イベントが発生したときにどのような動作を行うかはイベントの受け側(たとえばForm)に記述するからです。

また、 上記のコードはイベントが宣言されているだけなので自動的にイベントが発生するようなことはありません。イベントを発生させるにはRaiseEventステートメント(読み方:RaiseEvent = レイズイベント)を使用します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class Tester

    Public
Event Pop(ByVal sender As Object, ByVal e As EventArgs)

    Public
Sub DoAnything()
        RaiseEvent Pop(Me, New EventArgs)
    End
Sub

End
Class

■リスト2:イベントを定義・発生させる最もシンプルな例

これでイベントを発生させる側は完成です。TesterクラスのDoAnythingメソッドを呼び出すとPopイベントが発生します。この構造ではイベントが何の役にも立たないのですが、イベントを発生させるシンプルな例として記憶に留めておくとよいでしょう。

このTesterクラスのPopイベントを受け取るコードも見てみましょう。どのようなクラスでもイベントを受け取ることができますが、見慣れているフォームの例で説明します。フォームにButtonを貼り付けて次の通りに記述して下さい。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim WithEvents Tester1 As New Tester

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Tester1.DoAnything()

End
Sub

Private Sub Tester1_Pop(ByVal sender As Object, ByVal e As System.EventArgs) Handles Tester1.Pop

    MsgBox(
"Pop!")

End
Sub

■リスト3:フォーム側のプログラム。Testerクラスのテスト。

Button1をクリックするとTester1DoAnythingメソッドが呼ばれます。そして、上述のようにDoAnythingメソッドは特に何もしないでPopイベントを発生させるので画面には「Pop!」というメッセージボックスが表示されます。

ポイントはTester型の変数が単なるWithEvents(読み方:WithEvents = ウィズイベンツ)付きで宣言されていることです。イベントを受け取るためにはこのWithEventsを使って変数を宣言 するのが一般的です。イベントをトラップするのにHanldesではなく、AddHandlerを使う場合はWithEventsで宣言されていなくても問題ありません。

WithEventsを使っていればButtonTextBoxと同じようにイベントプロシージャを生成してイベントを受け取ることができます。

 

3.イベントの詳細

 

それではもう少し詳細にイベントの定義を眺めてみましょう。イベントプロシージャには必ず2つの引数がありますからイベントを定義するときも2つの引数を指定しなければなりません。

このことは第10回 イベントプロシージャで既に説明していますが簡単に復習してみます。

イベントプロシージャの第1引数はObject型で、イベントを発生させたクラスを表すことになっています。ですからイベントを発生させるクラスの側から見るとほとんどの場合第1引数にはMeをセットすることになります。

第2引数はイベントの追加情報でEventArgs型またはEventArgsクラスを継承したクラスを指定することになっています。追加情報が特に必要ない場合は単純にEventArgsクラスの新しいインスタンスを指定するだけで構いません。

イベントプロシージャの引数 セットすべき内容
sender As Object イベントを発生させるクラス側ではほとんどの場合Meを設定する。
e As EventArgs 追加情報が必要なければEventArgsクラスの新しいインスタンスを指定する。追加情報が必要な場合はEventArgsクラスを継承したクラスを指定する。

■表1:イベントプロシージャの引数

以上はほとんどのプログラマが従っているルールですが、絶対にこのようにしなければならないというルールではありません。たとえば第1引数の名前をsender以外のものにして良いですし、各引数の型を変更しても構いません。それどころか3つ以上の引数を指定することも可能です。

けれどわざわざルールから逸脱しないで、他のみなさんと同じ作りにしておいてください。1人だけ違う作りにしているとかなり使いにくいプログラムができあがってしまいます。

ルールを守って作ると先ほどのプログラムと同じような定義と呼び出しになります。

定義

VB.NET2002対応 VB.NET2003対応 VB2005対応


Public
Event Pop(ByVal sender As Object, ByVal e As EventArgs)
 

■リスト4:イベントの定義

呼び出し

VB.NET2002対応 VB.NET2003対応 VB2005対応


RaiseEvent
Pop(Me, New EventArgs)
 

■リスト5:イベントの発生

 

4.第2引数の拡張

 

次に追加情報が必要なイベントを説明しましょう。

追加情報の例としてはフォームなどのMouseMoveイベントでは第2引数を使ってマウスの座標を取得することができますし、KeyPressイベントでは押されたキーを取得することができます。このような仕組みを自作のクラスにも組み込んでみます。

ここではサンプルとしてあらかじめ最大値を設定できる数値型であるAlarmIntegerクラスを作成します。

たとえば最大値を100と決めておけば、100を超える値をセットしようとした瞬間にイベントが発生するようにします。

このイベントをOverイベントと名付けこれからいろいろと試してみましょう。手始めにOverイベントでは追加情報としてセットしようとしている値と現在の値を取得できるようにします。

イベントの追加情報以外の部分はここまでの説明を組み合わせれば作成可能なはずですからまずは自力で作成してみてください。

 

まずは追加情報は抜きにして最大値を超える値をセットするときにOverイベントを発生させる部分までの全コードを示します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class AlarmInteger

    Private m_Value As
Integer
   
Private m_Max As Integer
   
Public Event Over(ByVal sender As Object, ByVal e As EventArgs)

    Public Sub New(ByVal max As Integer)

   
    Me.Max = max

    End
Sub

    '■Value
   
''' <summary>整数値を取得または設定します。</summary>
   
Public Property Value() As Integer
       
Get
            Return
m_Value
        End
Get
       
Set(ByVal value As Integer)
            If value > Me.Max
Then
               
'セットしようとしている値が最大値を超える場合にOverイベントを発生させる。
               
RaiseEvent Over(Me, New EventArgs)
            End
If
           
m_Value = value
        End
Set
   
End Property

    '■Max
   
''' <summary>Valueプロパティの最大値を取得または設定します。</summary>
   
Public Property Max() As Integer
       
Get
            Return
m_Max
        End
Get
       
Set(ByVal value As Integer)
            m_Max = value
        End
Set
   
End Property
End
Class

■リスト6:最大値を超えるとOverイベントが発生するAlarmIntegerクラス。

これを利用する側の例も示します。ここでは試しやすいようにフォームで使用する例を作成しました。

AlarmIntegerクラスのインスタンスの名前はiで、最大値はコンストラクタを使って100にしました。Button1をクリックするごとにValueは34ずつ増加しまから、3回目にButton1をクリックした時点でOverイベントが発生します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim WithEvents i As New AlarmInteger(100) '最大値に100を指定

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    i.Value += 34

End
Sub

Private Sub i_Over(ByVal sender As Object, ByVal e As System.EventArgs) Handles i.Over

    MsgBox(
"OVER!")

End
Sub

■リスト7:フォーム側のプログラム。AlarmIntegerクラスのテスト。

この構造を図にしてみましょう。

イベント発生の仕組み

■画像1:イベント発生の仕組み

この図ではプログラムの中を値がどのように飛び交っているかを示しています。この後でどんどん複雑な例が登場しますが混乱してきたらこの図に戻って値の流れを確認して下さい。

フォーム側でi.Value += 34が実行されると、ただちにAlarmIntegerクラス側のValueプロパティのSet節が実行されます。このときに引数valueにはセットしようとした値、つまり34が渡されます。もちろん34ずつ足していくのですから、ボタンをもう1回クリックすると68、さらにクリックすると102が渡されることになります。ここまではイベントではなくプロパティの話です。

なお、図の都合上i.Valueの値がSet節のvalue引数に渡るように表現されていますが、実際にはi.Valueにセットしようとしている値がvalue引数に渡されるだけです。この時点ではValueプロパティの値はまだ変更されていません。それからプロパティの名前と引数の名前が同じなのは偶然です。

このあとSet節でRaiseEventを使ってOverイベントを発生させます。ただし、Ifで条件がついているので値が100以下の場合は発生しません。イベントが発生すると直ちにフォーム側のOverイベントプロシージャに制御が移りますが、このとき通常のメソッドの設定した引数も同時に渡されます。それでこの引数を使って情報の受け渡しができるわけです。

 

構造について理解していただいたところで、このコードを改造してOverイベントの追加情報として現在の値と変更後の値を取得できるようにします。

 

まず、追加情報を格納するためのクラスを作成します。このクラスがイベントの第2引数となるわけです。追加情報としては現在の値と変更後の値の2つの情報が必要です。

以上のことを考え合わせると作成すべきクラスはEventArgsクラスに2つのプロパティを追加したクラスと言うことになります。

ここでは「継承」という方法を使ってEventArgsクラスの機能を取り込んでしまいます。継承については中級講座で扱いますが、とりあえずクラスを宣言するときにInheritsキーワード(読み方:Inherits = インヘリツ)を使うと既に存在するクラスの機能がプログラムなしでそのまま使えるようになることだけを覚えておいてください。これが継承と言う方法です。

継承を使えば細かい部分を自分で書く必要がないので(書いたとしてもこの場合はたかが知れていますが)、実際に記述するのは2つのプロパティだけです。このクラスは次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class AlarmOverEventArgs

    Inherits EventArgs
'この1行でEventArgsのすべての機能を実装します。これが「継承」です。

   
Dim m_CurrentValue As Integer

    ''' <summary>現在の値を取得します。</summary>
   
Public Property CurrentValue() As Integer
       
Get
            Return
m_CurrentValue
        End
Get
       
Set(ByVal value As Integer)
            m_CurrentValue = value
        End
Set
   
End Property

    Dim m_NewValue As Integer

   
''' <summary>新しい値を取得します。</summary>
   
Public Property NewValue() As Integer
       
Get
            Return
m_NewValue
        End
Get
       
Set(ByVal value As Integer)
            m_NewValue = value
        End
Set
   
End Property

End
Class

■リスト8:AlarmIntegerクラスのOverイベントの第2引数となるAlarmOverEventArgsクラス

ところで、このクラスは別にInherits EventArgsの行がなくても正常に動作しますが、イベントプロシージャの第2引数はどんな場合でもEventArgsを継承するのが決まり というの全世界のプログラマの総意となっているのでここでは決まりに従って記述しました。

この決まりは人間用の決まりなのでInherits EventArgsがなくてもVBがエラーになったりすることはありません。

さて、AlarmInteger側ではイベントの宣言部分とイベントを発生させる部分のコードを改造してこの新しいAlarmOverEventArgsクラスを使用するように改造します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class AlarmInteger

    Private m_Value As
Integer
   
Private m_Max As Integer
   
Public Event Over(ByVal sender As Object, ByVal e As AlarmOverEventArgs)
    Public Sub New(ByVal max As Integer)

       
Me.Max = max

    End
Sub
    '■Value
   
''' <summary>整数値を取得または設定します。</summary>
   
Public Property Value() As Integer
       
Get
            Return
m_Value
        End
Get
       
Set(ByVal value As Integer)
            If value > Me.Max
Then
               
'セットしようとしている値が最大値を超える場合にOverイベントを発生させる。
               
Dim e As New AlarmOverEventArgs
                e.CurrentValue = Me.Value
'現在の値をセット
               
e.NewValue = value '新しい値をセット
               
RaiseEvent Over(Me, e)
            End
If
           
m_Value = value
        End
Set
   
End Property
    '■Max
   
''' <summary>Valueプロパティの最大値を取得または設定します。</summary>
   
Public Property Max() As Integer
       
Get
            Return
m_Max
        End
Get
       
Set(ByVal value As Integer)
            m_Max = value
        End
Set
   
End Property
End
Class

■リスト9:クラス側のプログラム。Overイベント発生時に追加情報を渡す。

さきほどのプログラムからの変更部分には色が付けてあります。

まずイベントの宣言の第2引数の型をEventArgs型からAlarmOverEventArgs型に変更しています。

それからイベントを発生させるときにイベントの発生直前でAlarmOverEventArgs型の変数に情報をセットしてその変数をイベントの第2引数として渡しています。このようにすることでイベントを受け取る側、つまりフォームは第2引数を経由してさまざまな情報を受け取ることができるようになるのです。

以上の変更に加えてフォーム側でも変更を行う必要があります。Overイベントプロシージャの第2引数の型を変更しなければなりません。

メモ  -  指定されているシグネチャが異なります。

フォーム側を修正する前の状態では、フォーム側のOverイベントの部分でエラーが発生し、「(前略)…。シグネチャが異なります。」というメッセージが表示されます。

これはVBがメソッドやプロパティを「名前」と「引数の型」と「引数の順番」の3つの要素で区別していることと関係があります。シグネチャとはこの3つの要素の組み合わせのことです。

Overイベントプロシージャの最初の状態ではシグネチャは「名前=i_Over, 引数の型と順番=Object, EventArgs」でした。このことをよくi_Over(Object, EventArgs)のように記述します。

ところが、後でAlarmIntegerクラスを修正したのでOverイベントのシグネチャはi_Over(Object, AlarmOverEventArgs)でなければなりません。それで「シグネチャが異なります」というエラーが発生するわけです。

なお、i_Overはイベントプロシージャなのでこの場合は名前(i_Over)は重要ではありません。後ろのHandles i.Overの方が本命です。このあたりはイベントプロシージャが他のメソッドやプロパティとちょっと違う点です。

 

本当に情報が正しく渡されてくるかを判断するためのコードも追加すると、フォーム側は次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim WithEvents i As New AlarmInteger(100) '最大値にを指定
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    i.Value += 34

End
Sub
Private Sub i_Over(ByVal sender As Object, ByVal e As AlarmOverEventArgs) Handles i.Over

    MsgBox("Overです! 現在の値は" & e.CurrentValue & ", 新しい値は" & e.NewValue)

End
Sub

■リスト10:フォーム側のプログラム。AlarmIntegerクラスのテスト。

これで第2引数を使って自由に情報を渡すことができるようになりました。

 

5.キャンセル

 

さらに第2引数を拡張して今度はキャンセルができるようにします。つまり最大値を超える値を設定された場合プログラマがその値を受け入れるか拒否するかイベントプロシージャを使って指定できるようにします。

このようなキャンセルの例としてはFormClosingイベントがあります。ただしこのイベントはVB2005で新たに追加されたもので す。VB.NET2002およびVB.NET2003では同じようなClosingイベント を使うとキャンセルを行うことができます。

自分でキャンセル機能を組み込む前にキャンセルの使い方を確認しておきましょう。

フォームのFormClosingイベント(またはClosingイベント)に次のようにコードを記述して実行してみてください。

VB2005対応

Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing

    Dim Answer As MsgBoxResult

    Answer = MsgBox("フォームを閉じますか?", MsgBoxStyle.Question Or MsgBoxStyle.YesNo Or MsgBoxStyle.DefaultButton2)

    If
Answer = MsgBoxResult.No
Then
       
e.Cancel = True
   
End If

End Sub

■リスト11:フォームのFormClosingイベントを使ったキャンセルの例

この例ではフォームを閉じるタイミングで「フォームを閉じますか?」と言うメッセージを表示し、ユーザーが[いいえ]を選択した場合にはフォームが閉じるのをキャンセルします。

実際のキャンセルはe.Cancel = Trueという1行に示されています。

このプログラムでもe、つまりイベントプロシージャの第2引数は単なる情報に過ぎずe.Cancel = True自体にフォームの閉鎖をキャンセルする機能があるわけではありません。イベントを発生させた側がイベント発生後にe.Cancelの値を調べて、もし値がTrueであるならばフォームの閉鎖を中止するようにプログラムされているのです。

この仕組みを先ほどのAlarmIntegerクラスの実装して、Overイベントが発生したときに値のセットをキャンセルできるようにしましょう。

実はいままでの説明を組み合わせればこのような動作は実現できるので意欲的な方はサンプルをご覧になる前に自分で試されてみてはいかがでしょうか?

 

まず、やるべきことはAlarmOverEventArgsクラスにCancelプロパティを追加することです。これは最早書くまでもないと思いますが一応どのようになるかコードを載せておきます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class AlarmOverEventArgs

    Inherits EventArgs
'この1行でEventArgsのすべての機能を実装します。これが「継承」です。

   
Dim m_Cancel As Boolean

    Public Property Cancel() As Boolean
   
    Get
            Return
m_Cancel
        End
Get
       
Set(ByVal value As Boolean)
            m_Cancel = value
        End
Set
   
End Property

    Dim m_CurrentValue As Integer

    ''' <summary>現在の値を取得します。</summary>
   
Public Property CurrentValue() As Integer
       
Get
            Return
m_CurrentValue
        End
Get
       
Set(ByVal value As Integer)
            m_CurrentValue = value
        End
Set
   
End Property

    Dim m_NewValue As Integer

   
''' <summary>新しい値を取得します。</summary>
   
Public Property NewValue() As Integer
       
Get
            Return
m_NewValue
        End
Get
       
Set(ByVal value As Integer)
            m_NewValue = value
        End
Set
   
End Property
End
Class

■リスト12:AlarmOverEventArgsクラスへCancelプロパティを追加

この例ではなんの工夫もなくCancelプロパティを追加しましたが、実はこのキャンセルの仕組みはよく使うのであらかじめVBに用意されています。InheritsのところでEventArgsではなく、CancelEventArgsを指定するとCancelプロパティを自分で書かなくてもすぐに使えるようになります。このCancelEventArgsクラスはSystem.Component名前空間に属しています。

ともあれ今は自分でCancelプロパティを記述しておきましょう。実際のイベントを発生させる側とイベントを処理する側の連携の仕組みを自分の目で見ていただきたいからです。

 

次にAlarmIntegerクラスのコードです。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class AlarmInteger

    Private m_Value As
Integer
   
Private m_Max As Integer
   
Public Event Over(ByVal sender As Object, ByVal e As AlarmOverEventArgs)
    Public Sub New(ByVal max As Integer)

       
Me.Max = max

    End
Sub
    '■Value
   
''' <summary>整数値を取得または設定します。</summary>
   
Public Property Value() As Integer
       
Get
            Return
m_Value
        End
Get
       
Set(ByVal value As Integer)
            If value > Me.Max
Then
               
'セットしようとしている値が最大値を超える場合にOverイベントを発生させる。
               
Dim e As New AlarmOverEventArgs
                e.CurrentValue = Me.Value
'現在の値をセット
               
e.NewValue = value '新しい値をセット
               
RaiseEvent Over(Me, e)
                If e.Cancel = True
Then
                    Return
               
End If
       
    End If
           
m_Value = value
        End
Set
   
End Property
    '■Max
   
''' <summary>Valueプロパティの最大値を取得または設定します。</summary>
   
Public Property Max() As Integer
       
Get
            Return
m_Max
        End
Get
       
Set(ByVal value As Integer)
            m_Max = value
        End
Set
   
End Property
End
Class

■リスト13:クラス側のプログラム。キャンセルを実装。

見るとわかるようにイベント発生後にCancelプロパティの値をチェックし、Trueであれば何もしないで処理を終了するようにしています。当たり前のことですがこのチェックがイベント終了後であることが重要です。イベントの終了後であるからこそイベントプロシージャでフォーム側のプログラムがe.Cacnelに何をセットしたかがわかるのです。

フォーム側のプログラムはケースバイケースになりますがとりあえずオーバーする値は単純に拒否するようにしておきましょう。

値のセットが拒否されていることがわかるようにラベルに現在の値を表示するコードも追加してみました。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim WithEvents i As New AlarmInteger(100) '最大値に100を指定

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    i.Value += 34
    Label1.Text = i.Value

End
Sub

Private Sub i_Over(ByVal sender As Object, ByVal e As AlarmOverEventArgs) Handles i.Over

    e.Cancel =
True

End Sub

■リスト14:フォーム側のプログラム。AlarmIntegerクラスのテスト。

 

6.別の視点

 

6−1.機能強化案

これで一応まとまった機能のあるクラスが完成しましたが、もうちょっと機能強化してみましょう。

以下の2つの機能を実装して見ます。

・最大値を超える値はセットできないようにする。

最大値を超える値をセットしようとしたときには値は自動的に最大値になり、Overイベントは発生しません。

たとえば、最大値が100の時に120をセットしようとすると値は100になります。

Overイベントでセットする値を自由に指定できるようにする。

たとえば、最大値が100の時に120をセットしようとしたときにどうするかはプログラムがその都度決めます。100にすることもできますし、120やその他の値にすることもできます。

 

6−2.最大値を超える値はセットできない

まずは、最大値を超える値をセットしようとしたら必ず最大値になるというのをプログラムしてみましょう。

この設計だと絶対に値がオーバーしないのでOverイベントを発生しないようにしてしまいますが、それはそれで柔軟性がなくなってしまうので今まで通りに動作するか、最大値を 超える値をセットできないように動作するか動作モードをAllowOverプロパティとして実装することにします。

AllowOver = Trueのときは従来どおりの動作です。AllowOver = Falseの時はオーバーする値をセットしようとすると自動的にMax値がセットされOverイベントは発生しません。

メモ

今回は作りませんが最大値を超える値をセットしようとしたときでもOverイベントを発生させるべきだと言う考え方もあるかもしれません。腕試しにそのように改造してみるのも一興です。ごく簡単な改造で済みます。

完成版を見る前に練習代わりにまずは自分で作ってみてください。

 

完成版は次の通りです。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class AlarmInteger

    Private
m_Value As
Integer
   
Private m_Max As Integer
   
Private m_AllowOver As Boolean = True
   
Public Event Over(ByVal sender As Object, ByVal e As AlarmOverEventArgs)
    Public Sub New(ByVal max As Integer)

        Me.New(max, True)

    End
Sub
    Public Sub New(ByVal max As Integer, ByVal allowOver As Boolean)

       
Me.Max = max
       
Me.AllowOver = allowOver

    End
Sub
    '■AllowOver
   
''' <summary>Maxプロパティの値を超える代入を許可するかを示します。</summary>
   
Public Property AllowOver() As Boolean
        Get
            Return
m_AllowOver
        End
Get
       
Set(ByVal value As Boolean)
            m_AllowOver = value
        End
Set
   
End Property
    '■Value
   
''' <summary>整数値を取得または設定します。</summary>
   
Public Property Value() As Integer
       
Get
            Return
m_Value
        End
Get
       
Set(ByVal value As Integer)
            If value > Me.Max
Then
               
If AllowOver Then
                   
'セットしようとしている値が最大値を超える場合にOverイベントを発生させる。
                   
Dim e As New AlarmOverEventArgs
                    e.CurrentValue = Me.Value
'現在の値をセット
                   
e.NewValue = value '新しい値をセット
                   
RaiseEvent Over(Me, e)
                    If e.Cancel = True
Then
                       
Return
                   
End If
               
Else
                   
value = Me.Max
                End
If
           
End If
           
m_Value = value
        End
Set
   
End Property
    '■Max
   
''' <summary>Valueプロパティの最大値を取得または設定します。</summary>
   
Public Property Max() As Integer
       
Get
            Return
m_Max
        End
Get
       
Set(ByVal value As Integer)
            m_Max = value
        End
Set
   
End Property

End
Class

■リスト15:クラス側のプログラム。最大値を超える値をセットしようとしたら必ず最大値になる。

これを試すフォーム側のプログラムは次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim WithEvents i As New AlarmInteger(100, False) '最大値に100を指定
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    i.Value += 34
    Label1.Text = i.Value

End
Sub
Private Sub i_Over(ByVal sender As Object, ByVal e As AlarmOverEventArgs) Handles i.Over

    MsgBox(
"Over!")

End
Sub

■リスト16:フォーム側のプログラム。AlarmIntegerクラスのテスト。

一応Overイベントプロシージャも書いておきましたが、これはむしろOverイベントが発生しないことを確認するためのものです。

AllowOverプロパティの値をいちいちセットするのは面倒なので、コンストラクタでAllowOverプロパティの値をセット してみました。もちろん他のプロパティと同様にフォームのLoadイベントなどでAllowOverプロパティの値をセットすることもできます。

 

6−3.Overイベントでセットする値を自由に指定できる

次にOverイベント発生時に値をどうするのか自由に指定できるように改造してみます。先ほどの改造ではキャンセルできるようにしたり、自動的に最大値がセットされたりするようにはできましたが自由な値を指定することはできませんでした。

たとえば、最大値が100の時に120をセットしようとしたら値を20にするなど、プログラム中で自由に指定できるようにします。

これもまずは自分で挑戦してみてください。

 

方針としてはやはりOverイベント回りを改造することになります。

 

Overイベントで第2引数を使ってキャンセルかどうか判断したように、新しい値も第2引数を経由して受け渡しすればよいのです。キャンセルを実装したときは第2引数にCancelプロパティを追加しましたが、今回は既にNewValueというプロパティがあるのでこれを使用しましょう。

つまり、イベントの呼び出し側ではキャンセルされなかった場合は常にNewValueに設定されている値が文字通り新しい値となるようにすればよいのです。

次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class AlarmInteger

    Private
m_Value As
Integer
   
Private m_Max As Integer
   
Private m_AllowOver As Boolean = True
   
Public Event Over(ByVal sender As Object, ByVal e As AlarmOverEventArgs)
    Public Sub New(ByVal max As Integer)

        Me
.New(max, True)

    End
Sub
    Public Sub New(ByVal max As Integer, ByVal allowOver As Boolean)

       
Me.Max = max
       
Me.AllowOver = allowOver

    End
Sub
    '■AllowOver
   
''' <summary>Maxプロパティの値を超える代入を許可するかを示します。</summary>
   
Public Property AllowOver() As Boolean
        Get
            Return
m_AllowOver
        End
Get
       
Set(ByVal value As Boolean)
            m_AllowOver = value
        End
Set
   
End Property
    '■Value
   
''' <summary>整数値を取得または設定します。</summary>
   
Public Property Value() As Integer
        Get
            Return
m_Value
        End
Get
       
Set(ByVal value As Integer)
            If value > Me.Max
Then
               
If AllowOver Then
                   
'セットしようとしている値が最大値を超える場合にOverイベントを発生させる。
                   
Dim e As New AlarmOverEventArgs
                    e.CurrentValue = Me.Value
'現在の値をセット
                  
 e.NewValue = value '新しい値をセット
                   
RaiseEvent Over(Me, e)
                    If e.Cancel = True
Then
                        Return
                   
End If
                   
value = e.NewValue
               
Else
                   
value = Me.Max
                End
If
           
End If
           
m_Value = value
        End
Set
   
End Property
    '■Max
   
''' <summary>Valueプロパティの最大値を取得または設定します。</summary>
   
Public Property Max() As Integer
   
    Get
            Return
m_Max
        End
Get
   
    Set(ByVal value As Integer)
            m_Max = value
        End
Set
   
End Property
End
Class

■リスト17:クラス側のプログラム。Overイベント発生時に値をどうするのか自由に指定できる。

このようにたった1行を追加するだけですみます。

これを試すフォーム側のコード例も紹介します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim WithEvents i As New AlarmInteger(100, True) '最大値に100を指定
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    i.Value += 34
    Label1.Text = i.Value

End
Sub
Private Sub i_Over(ByVal sender As Object, ByVal e As AlarmOverEventArgs) Handles i.Over

    e.NewValue = e.NewValue - i.Max

End
Sub

■リスト18:フォーム側のプログラム。AlarmIntegerクラスのテスト。

これを実行するとラベルの値は34 → 68 → 2 → 36 → 70 → 4 というように変化していきます。

 

7.クラス間の連携としてのイベント

 

7−1.イベントとはクラスとクラスが連携するための手段

最後にイベントの利用方法について再確認しておきます。

普段ButtonClickイベントやFormLoadイベントなどを使っていると「イベントと言うのは何かあったときに自動的にプロシージャを呼び出すことができる機能」というように思ってしまいますが、実はそれだけではありません。

イベントと言うのはクラスとクラスが連携するための1つの手段なのです。一方的な呼び出しの場合もありますし、双方向的な情報のやりとりということもあります。

もし、あなたがプログラムしているときにクラスとクラスをうまく連携させる方法が思いつかなかったらイベントのことを思い出してください。

以下で具体例を紹介しつつクラス間の連携で考慮すべきことを説明します。

 

7−2.一方向の情報

ClassAClassBから情報を受け取るにはどのような手段があるでしょうか?もっとも単純でしかも実際に役に立つ手段はClassB側にPublicなどで外部から参照できるプロパティまだはメソッドを用意しておいてClassAからそれを呼び出すと言うものです。

次はコードはそのイメージです。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class ClassA

    Public B As New ClassB

    Public Sub New()

        Dim
Info As
Object
       
Info = B.Info   'ここでClassAはClassBの情報を取得できる。

    End
Sub

End
Class

■リスト19:ClassAのプログラム。ClassBの情報を取得できる。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class ClassB

    Public
ReadOnly Property Info() As
Object
        Get
           
'ここでClassBは外部のクラスに情報を渡す。
           
Return "ここで任意の情報を返すことができます。"
       
End Get
   
End Property

End
Class

■リスト20:ClassBのプログラム。ClassAに情報を渡せるようにInfoプロパティを実装。

この段階でのClassAClassBでの情報の交換は次の通りです。

  ClassAのプログラム ClassBのプログラム
ClassAClassBの情報を
取得する
ClassBのプロパティを呼び出す。
Info = B.Info
Publicなプロパティを実装。
Public ReadOnly Property Info ...
ClassBClassAの情報を
取得する
× ×

■表2

 

7−3.プロパティを使った双方向の情報

問題が複雑になってくるのはこの先です。今後は逆にClassBClassAの情報を受け取る方法を考えてみてください。

これも多くのケースではシンプルに解決できます。ClassB側でClassA型のプロパティを用意しておけばよいのです。ClassA側ではClassBを使用するときにはまずそのプロパティに自分自身をセットするようにします。このプロパティにはコンストラクタで値をセットできるようにしておくと便利でしょう。

後はClassB側ではいつでもそのプロパティを経由してClassAの情報が受け取れるわけです。必要に応じてClassAでもPublicなど外部から参照できる形でプロパティやメソッドを用意しておきます。

この方法では次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class ClassA

    Public
B As New ClassB(Me)

    Public Sub New()

        Dim
Info As
Object
       
Info = B.Info    'ここでClassAはClassBの情報を取得できる。

    End
Sub

    Public ReadOnly Property Info() As Object
   
    Get
           
'ここでClassAは外部のクラスに情報を渡す。
       
    Return "ここで任意の情報を返すことができます。"
   
    End Get
   
End Property
End
Class

■リスト21:ClassAのプログラム。ClassBに情報を渡せるようにInfoプロパティを実装。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class ClassB

    Private m_Parent As ClassA
    Public Sub New(ByVal parent As ClassA)

        m_Parent = parent

    End
Sub
    Public ReadOnly Property Info() As Object
       
Get
           
'ここでClassBは外部のクラスに情報を渡す
           
Return "ここで任意の情報を返すことができます。"
       
End Get
   
End Property
    Public Sub DoAnything()
        Dim Info As
Object
       
Info = m_Parent.Info    'ここでClassBは外部のクラス(ClassA)の情報を取得できる。
    End
Sub

End
Class

■リスト22:ClassBのプログラム。ClassAの情報を受け取れるようにコンストラクタでClassAのインスタンスを取得する。

この方法ではClassAClassBの連携については完璧に動作します。ただし、ClassA側にInfoプロパティが用意されていることが前提です。

この段階でのClassAClassBでの情報の交換は次の通りです。

  ClassAのプログラム ClassBのプログラム
ClassAClassBの情報を
取得する
ClassBのプロパティを呼び出す。
Info = B.Info
Publicなプロパティを実装。
Public ReadOnly Property Info ...
ClassBClassAの情報を
取得する
Publicなプロパティを実装。
Public ReadOnly Property Info ...
ClassAのプロパティを呼び出す。
ClassAのインスタンスはコンストラクタで受け取る。

Info = m_Parent.Info

■表3

 

7−4.イベントを使った双方向の情報

 

同じことをイベントを使って実現することもできます。そのメリットは後で考えることにしてとりあえず例を見ていただきましょう。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class ClassA

    Public
WithEvents B As New ClassB
    Public Sub New()

        Dim
Info As
Object
       
Info = B.Info    'ここでClassAはClassBの情報を取得できる。

    End
Sub
    Private Sub B_RequestInfo(ByVal sender As Object, ByVal e As RequestInfoEventArgs) Handles B.RequestInfo

        'ここでClassAは外部のクラスに情報を渡す
        e.Info =
"ここで任意の情報を返すことができます。"

   
End Sub

End
Class

■リスト23:ClassAのプログラム。ClassBに情報を渡せるようにRequestInfoイベントプロシージャを実装。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class ClassB

    Public Event RequestInfo(ByVal sender As Object, ByVal e As RequestInfoEventArgs)

    Public ReadOnly Property Info() As Object
       
Get
           
'ここでClassBは外部のクラスに情報を渡す
           
Return "ここで任意の情報を返すことができます。"
   
    End Get
   
End Property
    Public Sub DoAnything()

        Dim
e As New RequestInfoEventArgs
        Dim
Info As
Object

       
RaiseEvent RequestInfo(Me, e)    'ここでClassBは外部のクラス(ClassA)の情報を取得できる。

        Info = e.Info

    End
Sub

End
Class

■リスト24:ClassBのプログラム。ClassAから情報を受け取るためにイベントを発生させる。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Public Class RequestInfoEventArgs

   
Inherits EventArgs

    Private
m_Info As
Object

   
Public Property Info() As Object
       
Get
            Return
m_Info
        End
Get
       
Set(ByVal value As Object)
            m_Info = value
        End
Set
   
End Property

End
Class

■リスト25:ClassBRequestInfoイベントの第2引数であるRequestInfoEventArgsクラス

この段階でのClassAClassBでの情報の交換は次の通りです。

  ClassAのプログラム ClassBのプログラム
ClassAClassBの情報を
取得する
ClassBのプロパティを呼び出す。
Info = B.Info
Publicなイベントプロシージャを実装。
Public ReadOnly Property Info ...
ClassBClassAの情報を
取得する
RequestInfoイベントプロシージャを実装して第2引数に情報を渡す。

e.Info = ...

RequestInfoイベントを発生させて第2引数から情報を受け取る。

RaiseEvent RequestInfo(Me, e)
Info = e.Info

■表4

 

7−5.どのように書くべきか

さて、イベントを使って情報を交換するメリットを考えて見ましょう。プロパティを使う場合とイベントを使う場合とでClassA側のプログラムがどのようになるか表にしてみました。

  プロパティを使う場合
ClassAのプログラム
イベントを使う場合
ClassAのプログラム
ClassBClassAの情報を取得する Publicなプロパティを実装 イベントプロシージャを実装

■表5

表を見るとわかるように結局のところClassA側にプロパティを作る必要があるのかそれともイベントプロシージャを作る必要があるのかという問題に帰着します。

そしてプロパティは必ず実装しなければエラーになってしまいますが、イベントプロシージャは必要なときだけ実装すればよいことを思い出してください。TextBoxを貼り付けたら必ずKeyPressイベントプロシージャに何か書かなくてはいけないと言うことはなかったでしょう。

また、イベントプロシージャは必要であれば簡単に実装できるのに対し、プロパティを実装しようとするとどのような型でどのような引数のプロパティが必要であるか人間が覚えておかなければならない点も重要です。たとえば「ClassBをインスタンス化するクラスには必ずObject型で引数なしのInfoプロパティを作成しなければならない」のような約束事が増えてしまって面倒なことになる可能性があるのです。

もっともこの点に関しては中級講座で扱う予定の「インターフェース」という技術を用いれば全く改善されます。

もちろんプロパティを使った方が便利な場合もあります。たとえば、さきほど「プロパティは必ず実装しなければエラーになっていしまいます」と書きましたが逆に言うと実装し忘れてしまうということがなくなります。

また、イベントを使った場合は適切な第2引数を別にクラスとして用意しなければならないのでいささか面倒です。

 

以上のことを考えると、イベントを使ったクラス間の連携は柔軟性に優れ、プロパティを使ったクラス間の連携は確実性に優れていると言えます。柔軟性よりも確実性を重視する場合もありますからどのような方法で連携すべきかはケースバイケースです。ここではイベントを使った連携が可能であることを記憶しておいて選択肢の幅を広げてください。

メモ

本文中で「プロパティを使ったクラス間の連携」のような表現を随所でしていますが、メソッドを使った連携も可能です。この場合はプロパティを使った連携とまったく同じ事情になるので説明を割愛しました。

また、プロパティおよびメソッドを使って連携する場合は継承やインターフェースのことも知っておいてください。