t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA クラスモジュールに自作のEventを実装する

今回はクラスモジュールに、自作のEventを実装し、シートモジュールでクラスのイベントを受け取る処理をやってみる。

まずはEventを使用しないクラスサンプル

クラスモジュールを挿入し、以下のコードを貼り付け。
※サンプルなのでクラスモジュールのオブジェクト名はClass1のままとする。

Private num As Long

Property Get Value() As Long
    Value = num
End Property

Property Let Value(x As Long)
    num = x
End Property

次にSheet1モジュールに以下を貼り付け

Private C As Class1

Sub Init()
    Set C = New Class1
End Sub

Sub Plus10()
    C.Value = C.Value + 10
    Debug.Print C.Value
End Sub

まずInitにカーソルを合わせてF5で実行すると、Sheet1モジュールの変数CがClass1のオブジェクトを保持する。
次にPlus10マクロをF5で実行すると、実行するたびにイミディエイトウインドウに数値が出力される。
まぁ、ここまでは今までやってきたクラスモジュールの扱いとなんら変わらない。

Eventを使ったクラスモジュール

さて、早速Class1モジュールの改造を始めよう。

Class1にイベントを実装するので、以下のように書き換える。

Private num As Long
Public Event Change(v As Long)

Property Get Value() As Long
    Value = num
End Property

Property Let Value(x As Long)
    num = x
    RaiseEvent Change(num)
End Property

変わったのはまず以下の部分。

Public Event Change(v As Long)

これはイベントの宣言である。変数などと同じようにイベントも宣言が必要だ。

そしてValueプロパティのLet部に書いた次のコード。

RaiseEvent Change(num)

これが、実際にイベントを発動させるコードである。

つまりこれで、Class1はValueが変更された際にChangeというイベントを発動するようになったわけだ。

次に、Sheet1モジュールのコードを、イベントを受け取れるように書き換える。

Private WithEvents C As Class1
Sub Init()
    Set C = New Class1
End Sub

Sub Plus10()
    C.Value = C.Value + 10
End Sub

Private Sub C_Change(v As Long)
    Debug.Print v
End Sub

まず変数Cの宣言部に付いたWithEventsキーワードに注目。

Private WithEvents C As Class1

これで、変数Cはイベントを受信可能なオブジェクトになった。

すると、コード上部のオブジェクト選択ボックスに、変数Cが現れるようになる。
f:id:t-hom:20161012231618p:plain

ここでCを選択すると、自動的に以下のコードが挿入される。

Private Sub C_Change(v As Long)

End Sub

この時、仮引数のvにはクラスから送信されたnumが入る。
こんなイメージ。
f:id:t-hom:20161012232758p:plain

最終的には受け取ったパラメータをDebug.Printで表示させている。

Private Sub C_Change(v As Long)
    Debug.Print v
End Sub

また、Plus10プロシージャに書いたDebug.Printを削除する。

さて、これでまたInitを実行すれば準備完了。
Plus10を実行するたびにイベントが発動され、イミディエイトウインドウに数値が出力される。

以上がイベントの実装方法である。

自作Eventはどんな時に使うのか。

さきほどこんなマクロを作ってみた。
f:id:t-hom:20161012224746g:plain

これはADOのRecordSetの動きをわかりやすく説明するために擬似的に動作を再現しようと作成中のものである。
※完成ではないが、今回のイベントの説明にあたって支障はない。

ちなみに赤いカーソルは▲を書式設定でマイナス90度回転させたもの。
f:id:t-hom:20161012233608p:plain
これもテクニックとして使えるので覚えておくと良いと思う。

さて、カーソルを動かすために、内部ではクラスモジュールCursorを使用している。
といってもシートのカーソル書き換えはシートモジュールが行っており、Cursorモジュールの役割はあくまで現在値の管理と変更された際のイベント発動である。

以下がクラスモジュールCursorのコード。
イベント宣言に注目。パラメーターとしてcurrent(現在値)とprevious(ひとつ前の値)を設定している。

Private innerValue As Long
Public Event Change(current As Long, previous As Long)

Sub Init(n As Long)
    innerValue = n
End Sub

Property Get Value() As Long
    Value = innerValue
End Property

Property Let Value(n As Long)
    Dim pv As Long: pv = innerValue
    innerValue = n
    RaiseEvent Change(innerValue, pv)
End Property

Sub MoveNext()
    Value = innerValue + 1
End Sub

Sub MovePrevious()
    Value = innerValue - 1
End Sub

またMoveNext、MovePreviousメソッドではinnerValueを直接変更せず、Valueプロパティ経由で変更するようにしている。そのため初期設定を行うInitメソッドを別にすれば、値の変更は必ずValueプロパティでイベントを発動させることになる。

またValueプロパティ(Let)の内部では、実際にinnerValueを変更する前に変数pvに控えておき、イベントでは変更後と変更前を両方パラメーターとして送信するようにした。

次にこのイベントを受け取るSheet1モジュールのコード。

Private WithEvents Cur As Cursor

Sub 初期化_Clicked()
    Set Cur = New Cursor
    Cur.Init 3
    Range("B3").Value = "▲"
End Sub

Sub MoveNext_Clicked()
    Cur.MoveNext
End Sub

Sub MovePrevious_Clicked()
    Cur.MovePrevious
End Sub

Private Sub Cur_Change(current As Long, previous As Long)
    Range("B" & previous).ClearContents
    Range("B" & current).Value = "▲"
End Sub

初期化ボタンをクリックした際にCursorクラスのインスタンスが生成され、Curにセットされる。
そしてCurの値(行を表す)が3に初期化され、実際にB3セルにカーソルを書き込む処理も行っている。

MoveNextボタンやMovePreviousボタンをクリックした際は、Cur変数が保持しているCursorオブジェクトのメソッドによりValueプロパティが変化し、イベントが発動される。

するとSheet1プロシージャに予め作成しておいたCur_Changeプロシージャに制御が移り、前のカーソルを削除と新しいカーソルの描画が行われる。

MoveNextをクリックした際の制御の流れはこのようになる。
f:id:t-hom:20161013000607p:plain

Eventを利用すると何がどう便利なのか

たとえばEventを利用せずにカーソルの描画を行う場合、以下の4パターンが考えられる。

  1. Sheet1のMoveNext_Clickedの中で処理する。
  2. Sheet1のMoveNext_Clickedから、カーソル描画用のプロシージャを呼び出す。
  3. CursorのMoveNextメソッドで処理する。
  4. CursorのValueプロパティで処理する。

1はまあ、却下である。なぜならMovePreviousでも同じような処理をするのに、個別のプロシージャに書いたら重複ができてしまいメンテ時に困るから。
2は、1よりマシだけど、各ボタンマクロがCursorの操作に加えて共通プロシージャの呼び出しまで実施しなければならない。
3と4はCursorクラスがSheet1の実装に大きく依存してしまい、汎用性がなくなる。

Eventを利用した場合はどうなるかというと、

  • 各ボタンマクロは、Cursorを操作するだけで良くなる。
  • カーソルの描画は、Cursorの変化というイベント処理で一元化できる。
  • Cursorは変化をイベントとして発行するだけなので、Sheet1の実装には依存しない。

このように、先ほどのデメリットが払拭されている。

まとめ

さて、今回はEventを実装する方法とそのメリットを簡単に説明した。といっても私自身、Eventを実装したのが今回初めてで、機能としては知っていたものの何がどう便利なのかわかっていなかった。だから皆さんにもうまく伝えられたかどうか分からない。

まだEvent機能については勉強しはじめたところなので、また「こういう場合に使うと便利」ってのが見つかったら都度紹介しようと思う。

参考書籍

既に絶版しているし、英語だけれど自作イベント処理等に言及されている素晴らしい書籍。

VBA Developer's Handbook

VBA Developer's Handbook

以下で詳しくレビューしているので興味のある方はどうぞ。
thom.hateblo.jp

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。