Worksheetを継承したクラスを作る(8)
ラップしたオブジェクトのイベントを検知する
めっちゃ久しぶりに
の続き。
ちょっと何言ってるかわからないかも知れないので説明する。
たとえば、WorksheetオブジェクトをラップしたPoweredSheetという自作クラスがあるとする。
当然、Worksheetオブジェクトには数々のイベントがある。
PoweredSheetにも同様のイベントを生やしたいのだ。
要するに、PoweredSheetに包まれたWorksheetオブジェクトで発火したイベントをPoweredSheet側で検知したい、ということだ。
どうも、イベントについては頭がこんがらがって、うまく説明できる自信がない。(いつか、「Custom Event? ははは。簡単じゃねーか、あんなの!」と笑える日が来るのだろうか。)
自身の覚書のつもりで書き残しておく。
目次
イベントのしくみ
Custom Eventの作成に必要なのは、「Event」、「RaiseEvent」、「WithEvents」の三つのキーワード。
手順としては、
- イベント名の宣言(
Eventキーワード) - イベント発火タイミングの設定(
RaiseEventキーワード) - イベントリスナ側にイベントプロシージャを作成(
WithEventsキーワード)
という順になる。
イベント名の宣言
イベントを生やしたいオブジェクトモジュールの宣言セクション(プロシージャよりも上の部分)に、「Event」キーワードを用いてイベント名を宣言する。
たとえば、Activateという名前のイベントを宣言するときは、
Option Explicit Public Event Activate()
と書くだけ。
これで、既にイベントの作成自体はおしまい。
オブジェクト ブラウザーで見ると、

ちゃんとイベントが設定されている。
もちろん、これだけではイベントは発火しない。発火するタイミングがないのだから。
イベント発火タイミングの設定
イベントを発火させるタイミングを設定するには、「RaiseEvent」キーワードを用いる。
イベントを発火させたい場所に、
RaiseEvent イベント名(引数リスト)
と書くだけ。
たとえば、次のようなshowMessageというメソッドがあるとする。
Pubilc Sub showMessage(ByVal Message As String) Call MsgBox(Message) Message = Message & vbCrLf & "ち~んw" Call MsgBox(Message) End Sub
で、このプロシージャ内の1回目のMsgBox実行直後に「Activate」というイベントを発火させたいとする。
その場合は、
Pubilc Sub showMessage(ByVal Message As String) Call MsgBox(Message) RaiseEvent Activate Message = Message & vbCrLf & "ち~んw" Call MsgBox(Message) End Sub
と書けば良い。
これで、めでたくshowMessageメソッド実行時の1回目のMsgBox実行直後にActivateイベントが発火する。
発火するだけだけど。
発火したイベントを拾う
イベントが発火しても、それを拾うプロシージャがなければ、無視されて終わり。
ちょうど、Worksheetではそれこそ四六時中イベントが発火しているにもかかわらず、Worksheetモジュールにイベントプロシージャを置いていなかったら何も起こらないのと同じ。
イベントを拾う、すなわち、イベントプロシージャを作成するには、「WithEvents」キーワードを使う。
ただし、
イベントを拾うことができるのは、オブジェクトモジュールに限られる
ということに注意が必要。
標準モジュールではイベントが拾えない。すなわち、「WithEvents」が使えない。
とりあえず、手近なオブジェクトモジュールであるシートモジュールに書くことにする。
たとえば、シートモジュールの宣言セクションに、
Private WithEvents m_PoweredSheet As PoweredSheet
と書いてみる。
すると、VBEのオブジェクトリストに、

このようにm_PoweredSheetが追加されているのがわかる。
こいつを選択してやると、

このように、今のところPoweredSheetクラス唯一のイベントであるActivateイベントのイベントプロシージャが挿入された。
このm_PoweredSheet_Activateプロシージャ内にコードを書けば、m_PoweredSheetにぶち込まれたPoweredSheetオブジェクト内で発火したイベントを拾って、何らかの動作をさせることができる。
この説明では、シートモジュールによって、PoweredSheetオブジェクトで発火したイベントを拾った。
このシートモジュールが、「イベントリスナ(Event listener)」である。
よそで起こったイベントを聞き取って反応するやつ、ぐらいの意味だろうか。
以上が、Custom Eventのしくみである。
PoweredSheetにイベント装着
イベントの宣言
これはアホみたいに簡単。
上の「イベント名の宣言」でも書いたように、PoweredSheetクラスモジュールの宣言セクションに、
Public Event Activate()
と書くだけ。余裕。
イベント発火のタイミング設定
PoweredSheetオブジェクトは、実在するWorksheetオブジェクトをラップしているだけなので、PoweredSheetオブジェクトのActivateイベントは、実在のWorksheetオブジェクトのActivateイベントが発火したときに発火しないといけない。
一見ややこしそうに見えるけど、めっちゃ簡単。
要は、PoweredSheetオブジェクトが、その中に隠し持っている実在のWorksheetオブジェクトのイベントリスナになれば良い。
したがって、上の「発火したイベントを拾う」に書いたように、実在のWorksheetオブジェクトをぶち込む変数「m_RealSh」の宣言にWithEventsを付けるだけ。

こうすることによって、

このように、m_RealSh_Activateというイベントプロシージャを作ることができる。
このm_RealSh_Activateは、m_RealShがActivateされたときに発動する。
よって、このプロシージャ内でRaiseEventキーワードを用いれば、m_RealShのActivateイベントを拾ってPoweredSheetのActivateイベントを発火させることができることになる。
つまり、
Private Sub m_RealSh_Activate() RaiseEvent Activate End Sub
こうするわけだ。
整理すると、
m_RealShにぶち込まれたWorksheetでActivateイベントが発火するPoweredSheetオブジェクトがm_RealShのActivateイベントを拾うm_RealSh_Activateプロシージャ内でRaiseEvent Activateが実行されるPoweredSheetオブジェクトのActivateイベントが発火する
このような次第。
イベントリスナをクラスモジュールで作成
クラスモジュールTestListenerを挿入し、次のコードを書く。
リスト1 クラスモジュールTestListener
Option Explicit Private WithEvents m_PoweredSheet As PoweredSheet 'Property' Public Property Get PoweredSheet() As PoweredSheet Set PoweredSheet = m_PoweredSheet End Property 'Constructor' Private Sub Class_Initialize() Set m_PoweredSheet = New PoweredSheet End Sub Public Sub init(ByVal WorksheetEntity As Worksheet) Call m_PoweredSheet.init(WorksheetEntity) End Sub
変数m_PoweredSheetをWithEventsキーワード付きで宣言しているので、このオブジェクトはPoweredSheetオブジェクトのイベントを拾うことができるようになった。
準備は以上。後は実験あるのみ。
実験
次のコードで実験してみる。
ただし、このままではPoweredSheetのActivateイベントが無事に発動されたかどうかがわからないので、PoweredSheet内でRaiseEventを実行した直後に、イミディエイトにメッセージを出力することとする。すなわち、PoweredSheetクラスモジュール内のm_RealSh_Activateプロシージャを次のようにする。
Private Sub m_RealSh_Activate() RaiseEvent Activate Debug.Print "Activate event has surely been raised..." End Sub
イミディエイトにメッセージが出力されていたら、RaiseEvent Activateが実行された証となるはずだ。
リスト2 標準モジュール
Private Sub test01() Dim tl As TestListener Set tl = New TestListener Dim Sh As Worksheet Set Sh = PoweredSheetProject.Sh01 Call tl.init(Sh) Debug.Print "Sh02をActivateするよ。" Call PoweredSheetProject.Sh02.Activate Debug.Print "Sh02をActivateしたよ。" Debug.Print "Sh01をActivateするよ。" Call PoweredSheetProject.Sh01.Activate '……(*)' Debug.Print "Sh01をActivateしたよ。" End Sub
変数tlにぶち込んだTestListenerオブジェクトの内部には、PoweredSheetProject.Sh01が指し示すWorksheetオブジェクトがぶち込まれている。
一旦PoweredSheetProject.Sh02がActivateされ、その後PoweredSheetProject.Sh01がActivateされることになる。
(*)のところでイベントが発火するので、「Sh01をActivateするよ。」と「Sh01をActivateしたよ。」の間に、「Activate event has surely raised...」が出力されるはずだ。
上記リスト2を実行すると、

バッチリ。
おわりに
これで、PoweredSheetプロジェクトの完成にまた一歩近づいた。
過去記事
しかしまあ、Custom Eventの実装は、割と頭がこんがらがるなあ。