Worksheetを継承したクラスを作る(8)

ラップしたオブジェクトのイベントを検知する

めっちゃ久しぶりに

akashi-keirin.hatenablog.com

の続き。

ちょっと何言ってるかわからないかも知れないので説明する。

たとえば、WorksheetオブジェクトをラップしたPoweredSheetという自作クラスがあるとする。

当然、Worksheetオブジェクトには数々のイベントがある。

PoweredSheetにも同様のイベントを生やしたいのだ。

要するに、PoweredSheetに包まれたWorksheetオブジェクトで発火したイベントをPoweredSheet側で検知したい、ということだ。

どうも、イベントについては頭がこんがらがって、うまく説明できる自信がない。(いつか、「Custom Event? ははは。簡単じゃねーか、あんなの!」と笑える日が来るのだろうか。)

自身の覚書のつもりで書き残しておく。

目次

イベントのしくみ

Custom Eventの作成に必要なのは、「Event」、「RaiseEvent」、「WithEvents」の三つのキーワード。

手順としては、

  1. イベント名の宣言(Eventキーワード)
  2. イベント発火タイミングの設定(RaiseEventキーワード)
  3. イベントリスナ側にイベントプロシージャを作成(WithEventsキーワード)

という順になる。

イベント名の宣言

イベントを生やしたいオブジェクトモジュールの宣言セクション(プロシージャよりも上の部分)に、「Event」キーワードを用いてイベント名を宣言する。

たとえば、Activateという名前のイベントを宣言するときは、

Option Explicit

Public Event Activate()

と書くだけ。

これで、既にイベントの作成自体はおしまい。

オブジェクト ブラウザーで見ると、

f:id:akashi_keirin:20201220110227j:plain

ちゃんとイベントが設定されている。

もちろん、これだけではイベントは発火しない。発火するタイミングがないのだから。

イベント発火タイミングの設定

イベントを発火させるタイミングを設定するには、「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のオブジェクトリストに、

f:id:akashi_keirin:20201220110237j:plain

このようにm_PoweredSheetが追加されているのがわかる。

こいつを選択してやると、

f:id:akashi_keirin:20201220110240j:plain

このように、今のところ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を付けるだけ。

f:id:akashi_keirin:20201220110234j:plain

こうすることによって、

f:id:akashi_keirin:20201220110243j:plain

このように、m_RealSh_Activateというイベントプロシージャを作ることができる。

このm_RealSh_Activateは、m_RealShActivateされたときに発動する。

よって、このプロシージャ内でRaiseEventキーワードを用いれば、m_RealShActivateイベントを拾ってPoweredSheetActivateイベントを発火させることができることになる。

つまり、

Private Sub m_RealSh_Activate()
  RaiseEvent Activate
End Sub

こうするわけだ。

整理すると、

  1. m_RealShにぶち込まれたWorksheetActivateイベントが発火する
  2. PoweredSheetオブジェクトがm_RealShActivateイベントを拾う
  3. m_RealSh_Activateプロシージャ内でRaiseEvent Activateが実行される
  4. 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_PoweredSheetWithEventsキーワード付きで宣言しているので、このオブジェクトはPoweredSheetオブジェクトのイベントを拾うことができるようになった。

準備は以上。後は実験あるのみ。

実験

次のコードで実験してみる。

ただし、このままではPoweredSheetActivateイベントが無事に発動されたかどうかがわからないので、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.Sh02Activateされ、その後PoweredSheetProject.Sh01Activateされることになる。

(*)のところでイベントが発火するので、「Sh01をActivateするよ。」と「Sh01をActivateしたよ。」の間に、「Activate event has surely raised...」が出力されるはずだ。

上記リスト2を実行すると、

f:id:akashi_keirin:20201220110246j:plain

バッチリ。

おわりに

これで、PoweredSheetプロジェクトの完成にまた一歩近づいた。

過去記事

しかしまあ、Custom Eventの実装は、割と頭がこんがらがるなあ。