3.3 イベントクラスの管理と実装(前編)

前章では、どのレベルのイベントも別クラスでフックできることを示しました。では、それらのイベントをフックしたクラスは、どのようにして管理すればよいのでしょうか。このための非常に有効な方法が、あるホームページで紹介されていたDecoratorとデザインパターンをVBAで使用できるように修正したテクニックです。ここではこのテクニックをベースとしてExcelのクラスの管理方法を考えます。

まずは、Excelの構成要素について考えてみます。Excelは非常に多くの機能からなります。マクロを組むようになって、VBAのヘルプでクラス構成を見て、膨大さに気が遠くなったこともあります。しかしながら、その中でイベントを処理する要素は以下の3層として表すことができます。

このことから、イベントをフックするクラスは、この3層それぞれについてついて作ればいいことになります。これからは機能を明示するため、イベントをフックするクラスの名称を「〜Decorator」という名前で統一し、以下のようにします:

そして、これらDecoratorを、アプリケーション、ブック、シートの入れ子構造と同様に実装します。つまり、マクロを含むブック自身はAppDecoratorだけを直接扱い、AppDecoratorはBookDecoratorを、BookDecoratorはSheetDecoratorをそれぞれ管理するようにします。そして、Decoratorは、親に当たるDecoratorへの参照を持って、両方向の参照を可能にします。親の参照は、ExcelVBAのクラスの慣用句と同様にparentを使います。

とはいえ、実際にやってみないと感じがわからないかと思います。そこで、ブックとシートの関係を考えてみます。前章であつかった、別ブックfoo.xlsを保存する時に確認メッセージを出すマクロを含むブックに、同じくfoo.xlsのシート1か2をクリックしたときに、それぞれシートの名前を示すダイアログを表示する機能を追加してみましょう。

まずは、シート1,2をクリックしたときにシートの名前を示すダイアログを表示する機能そのものを持つSheetDecoratorを作ってみましょう。


[SheetDecorator]

Public WithEvents wsheet As Worksheet

Sub Initialize(ws As Worksheet)
    Set wsheet = ws
End Sub

Private Sub wsheet_Activate()
    MsgBox wsheet.Name
End Sub

では、今つくったSheetDecoratorのテストプログラムです。


[Module1]

Private SheetDecorator1 As SheetDecorator
Private SheetDecorator2 As SheetDecorator

Sub Test()
    Dim wb As Workbook
    Set wb = Workbooks("foo.xls")
    SheetDecorator1.Initialize wb.Worksheets(1)
    SheetDecorator2.Initialize wb.Worksheets(2)
End Sub

ここでは2枚のシートに対して別々の変数(SheetDecorator1および2)でフックしています。しかし、もっと多くのシートで同じ機能が使いたくなることがあるでしょう。その場合にはCollectionを使うと柔軟に対応できます。Collectionを使った例を次に示します。


[Module2]

Private sheetDecorators As New Collection 'of SheetDecorator

Sub Test2()
    Dim wb As Workbook
    Set wb = Workbooks("foo.xls")
    sheetDecorators.Add Item := New SheetDecorator
    sheetDecorators(1).Initialize wb.Worksheets(1)
    sheetDecorators.Add Item := New SheetDecorator
    sheetDecorators(2).Initialize wb.Worksheets(2)
End Sub

sheetDecoratorsの宣言に'of SheetDecoratorというコメントをつけてあります。Collectionオブジェクトは何でも格納することが出来ますので、一応明示しているわけです。まあ、Excelでのコレクションの慣用句をそのままに、格納する型の複数形を変数名にしているのでそれでもわかるのですけれどね。

コードに戻りましょう。このプロシージャでは、SheetDecoratorを確保し、それをsheetDecoratorsコレクションに格納し、SheetDecoratorのInitializeメソッドを呼び出しています。この処理は必ず一緒に呼び出されます。確保したSheetDecoratorにシートをフックしないのでは意味がありませんからね。そこで、この部分をまとめて別関数にすると便利ですし、可読性も高まります。改善例を示します。


[Module3]

Private sheetDecorators As New Collection 'of SheetDecorator

Sub Test3()
    Dim wb As Workbook
    Set wb = Workbooks("foo.xls")
    SetSheetDecorator wb.Worksheets(1)
    SetSheetDecorator wb.Worksheets(2)
End Sub

Private Sub SetSheetDecorator(ws As Worksheet)
    Dim sd As New SheetDecorator
    sd.Initialize ws, Me
    sheetDecorators.Add Item:=sd
End Sub

次に、ブックのイベントをフックしたクラス−今後はBookDecoratorと呼びますが−と、今作ったSheetDecoratorクラスを結合させます。そのためには、

という3つの処理を追加します。やってみましょう。


[SheetDecorator]

Public WithEvents wsheet As Worksheet
Private parent as BookDecorator

Sub Initialize(ws As Worksheet, bd As BookDecorator)
    Set wsheet = ws
    Set parent = bd
End Sub

Private Sub wsheet_Activate()
    MsgBox wsheet.Name
End Sub

[BookDecorator]

Public WithEvents wbook As Workbook
Private sheetDecorators As Collection 'of SheetDecorator

Public Sub Initialize(wb As Workbook)
    Set wbook = wb
    SetSheetDecorator wb.Worksheets(1)
    SetSheetDecorator wb.Worksheets(2)
End Sub

Private Sub SetSheetDecorator(ws As Worksheet)
    Dim sd As New SheetDecorator
    sd.Initialize ws, Me
    sheetDecorators.Add Item:=sd
End Sub

Private Sub wbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
    Dim f As Boolean
    f = MsgBox("ブックを本当に保存してよろしいでしょうか。", vbYesNo)
    If f = vbNo Then Cancel = True
End Sub

では、foo.xlsにこの機能を追加するテストプログラムを作ってみましょう。とはいえ、前節のプログラムのクラス名を変えただけだったりします。


[module4]

public bdeco As New BookDecorator

Sub Test()
    bdeco.Initialize Workbooks("foo.xls")
End Sub

ここで、bdeco.Initializeを呼び出すと、その中で2つのSheetDecoratorが自動設定されます。

ついでに、シート1,2をクリックしたときに、ブック名とシート名の両方を表示するようにActivateイベントを変更してみましょう。そのためにはシートが含まれているブックの名前を得なければなりません。この時に、先ほど設定したparent変数が使えます。このparentには、自分自身がフックしてあるシートが含まれるブックのデコレータが入っています。BookDecoratorには、フックしているワークブックがwbook変数に格納されています。従って、あるSheetDecoratorからWorkbookオブジェクトにアクセスしたい場合には、parent.wbook.[メソッドorプロパティ名]という形を取ればokです。従って、以下のようになります:


Private Sub wsheet_Activate()
    Dim s As String
    s = parent.wbook.Name
    MsgBox s & ":" & wsheet.Name
End Sub

ここではメンバ変数wbookをpublicとして直接アクセスしています。もちろん、これをprivateにして、BookDecoratorにGetBookName()などという関数を書いた方が正当でしょうが、ただ値を返すだけの関数が一杯になるのも困るので直で書いています。

ちょっと疲れました。この続きは後編にて。