概要
MSFormsのラッパークラスを作った、ついでにイベント処理をコールバックで実装できる。
経緯
- VBAでフォームを作るときに素のMSFormsのままだと地味に使いにくい。
- ラッパークラスを作ろう!
- イベント処理をコールバックで処理するのもできるようになった。
フォーム上にある具体的なコントロールであるLabel1などにはNameプロパティがあるが、
MSForms.Label型の変数だとNameプロパティはなく、Nameを候補に出すにはMSForms.Control型に入れる必要がある。
動的にフォームを作る場合にそんな感じで個別の型とControlを使い分けるのは面倒。
なのでImplementsを使ってIControlインタフェースを作りつつ、個別のクラスのラッパーを用意すればそのあたりを意識せずに使用できるのではと作ったもの。
共通となるIControlインタフェース用のクラス
Option Explicit
Public Enum EventTypeEnum
evClick
evChange
End Enum
Public Function Init(ByVal MSControl_ As MSForms.Control) As IControl:End Function
Public Function Move(Optional ByVal Left_ As Variant, Optional ByVal Top_ As Variant, Optional ByVal Width_ As Variant, Optional ByVal Height_ As Variant = Null) As IControl:End Function
Public Function GetName() As String:End Function
Public Function GetValue() As Variant:End Function
Public Function SetCaption(ByVal Text_ As String) As IControl:End Function
Public Function SetBackColor(ByVal Color_ As Long) As IControl:End Function
Public Function GetHeight() As Double:End Function
Public Function GetTop() As Double:End Function
Public Function GetLeft() As Double:End Function
Public Function GetWidth() As Double:End Function
Public Function GetRight() As Double:End Function
Public Function SetRight(ByVal RightEnd_ As Double) As IControl:End Function
Public Function GetBottom() As Double:End Function
Public Function SetCallBack(ByVal CallBack_ As IFormCallBack, ByVal EventType_ As EventTypeEnum) As IControl: End Function
共通としてほしいプロパティをペアで用意する。
今回は後で使うサンプルを動かすの必要なものだけを用意。
せっかくなのでメソッドチェーンを使えるようにプロパティではなくGetとSetのメソッドで実装。
プロパティと違ってメソッドであれば取得の型と設定の型を一致させなくてもいいので設定時には型で拘束できるメリットもある。
サンプルにはあまりないがインタフェースのGetの型にはVariantがそこそこ出てくる。
例えばValueは文字、数値、真偽値など返す値の種類が定まっていない。
FontSizeはDoubleで良さそうだがSpinButtonには存在しないので0を返すことになるが、ないという事をエラーとしてわかりやすくNullを返すようにする場合はDoubleではなくVariantにする必要がある。
個別のクラスのサンプル(Label)
Option Explicit
Implements IControl 'インタフェースを関連付ける
Private WithEvents MyControl As MSForms.Label '本体はWithEventsで捕まえておく
Private ClickCallBack As IFormCallBack '伝えるイベント別のコールバッククラスを入れる変数
Private ClickFlag As Boolean 'イベントを多重実行させないようにするためのフラグ、TextBoxのChangeなどで特に重要
Public Event Click(ByVal MyLabel_ As MyLabel) 'コールバックではなくラッパークラスをWithEventsで捕まえて使用する場合用のEvent
'コントロールのイベントでリレーまたはコールバック処理を行う
Private Sub MyControl_Click()
If Not ClickFlag Then
ClickFlag = True
If ClickCallBack Is Nothing Then
RaiseEvent Click(Me)
Else
Call ClickCallBack.Execute(Me, evClick)
End If
ClickFlag = False
End If
End Sub
Rem 初期化は個別のクラスには要らないのでインタフェース側だけ用意する
Private Function IControl_Init(ByVal MSControl_ As MSForms.Control) As IControl
Set IControl_Init = Me
Select Case True
Case MSControl_ Is Nothing
Err.Raise 1, TypeName(Me), "コントロールを渡してください"
Case TypeOf MSControl_ Is MSForms.Label
Set MyControl = MSControl_
Case Else
Err.Raise 1, TypeName(Me), "ラベル以外のコントロールが渡されています"
End Select
End Function
'イベント別の変数にコールバッククラスを入れる
Private Function IControl_SetCallBack(ByVal CallBack_ As IFormCallBack, ByVal EventType_ As EventTypeEnum) As IControl
Select Case EventType_
Case evClick: Set ClickCallBack = CallBack_
End Select
End Function
Public Function GetName() As String
GetName = MyControl.Name '候補に出ないだけで呼び出すことはできる
End Function
Private Function IControl_GetName() As String
IControl_GetName = Me.GetName 'インタフェース側は表に出しているものをそのまま渡すだけ
End Function
'RightやBottomは元々はないが相対位置で置くときにあると便利
Public Function GetRight() As Double
GetRight = MyControl.Left + MyControl.Width
End Function
Public Function SetRight(ByVal Right_ As Double) As MyLabel
Set SetRight = Me
MyControl.Left = Right_ - MyControl.Width
End Function
'特定のコントロールだけのやつはインタフェース無しで実装する
Public Function GetSpecialEffect() As MSForms.fmSpecialEffect
GetSpecialEffect = MyControl.SpecialEffect
End Function
Public Function SetSpecialEffect(ByVal Enum_ As MSForms.fmSpecialEffect) As MyLabel
Set SetSpecialEffect = Me
MyControl.SpecialEffect = Enum_
End Function
'LabelにValueは存在しないがLabelのValue相当のものを渡すことで他のものとまとめて扱えるようになる
Private Function IControl_GetValue() As Variant
IControl_GetValue = MyControl.Caption
End Function
'Moveは省略と実際に渡した場合を区別できるようにするのでVariantを使っている
Private Function IControl_Move(Optional ByVal Left_ As Variant, _
Optional ByVal Top_ As Variant, _
Optional ByVal Width_ As Variant, _
Optional ByVal Height_ As Variant = Null) As IControl
Set IControl_Move = Me
If IsNumeric(Left_) And Not IsMissing(Left_) Then MyControl.Left = Left_
If IsNumeric(Top_) And Not IsMissing(Top_) Then MyControl.Top = Top_
If IsNumeric(Width_) And Not IsMissing(Width_) Then MyControl.Width = Width_
If IsNumeric(Height_) Then MyControl.Height = Height_
End Function
'以下インタフェースと実際の処理を実装するが今回は省略。
LabelのGetValueでCaptionを取れるようにするなどするとLabelとTextBoxをまとめてGetValueで値を持ってくるというようなことができるようになるので便利。
SetValue(ByVal Value_ As Variant)のようにインタフェース側で引数がVariantになっているものは個別の方で型を指定する場合はインタフェース側から入ったときに型でエラーを起こさないように分岐させたりOn Error Resume Nextでごまかしが必要。
MoveはIsMissingとIsNumericで値を調べて実装する。
IsNumericだけだとEmptyで通ってしまうが省略した場合にNullを渡すようにすればIsNumericだけで済む。
ただしVBEの上のプルダウンから追加する場合はインタフェース側でNullを設定していても反映されないので手直しが必要。
'インタフェース側
Public Function Sample(ByVal Argument_ As Variant = Null) As Variant: End Function
'Implementsしたクラスでプルダウンメニューから追加したときの値
Private Function Sample_Sample(ByVal Argument_ As Variant = Empty) As Variant: End Function
ラベルの拡張
Public Event Change(ByVal MyLabel_ As MyLabel)
Private LabelValue As String
Private CaptionArray(1 To 3) As String
Public Function GetValue() As String
GetValue = LabelValue
End Function
Public Function SetValue(ByVal Value_ As String) As MyLabel
CaptionArray(2) = Value_
MyControl.Caption = Join(CaptionArray, "")
RaiseEvent Change(Me)
End Function
Public Function SetPrefix(ByVal Prefix_ As String) As MyLabel
Set SetPrefix = Me
CaptionArray(1) = Prefix
End Function
Public Function SetSuffix(ByVal Suffix_ As String) As MyLabel
Set SetSuffix = Me
CaptionArray(3) = Suffix_
End Function
ラベルのValueをCaptionとイコールにせずに、内部で値を持たせるようにして単位や接頭辞を付けられるようにする。
ここでRaiseEvent Change(Me)などとして、本来は存在しないLabelのChangeイベントを作ってもいい。
これがあれば例えばAutoSizeが有効な場合にLabelのCaptionが変わると大きさが変わるのでそれを検知してFrameのスクロール範囲を調整するといった処理もできるようになる。
ただしラッパークラスを介さずに操作されると検知できないので処理方法は統一すること。
コールバック処理の実装
VBAでコールバック関数を実装を参考にして、クラスとインタフェースを使って実装。
今回はフォーム専用のコールバッククラスなので、インタフェースの引数にはイベントの発生元のコントロールラッパーとイベントの種類、他に追加の引数を渡す。
SubではなくFunctionにしているのはByRefを使って値を返すタイプのものもあったはずなのでそれを処理する結果を返せるようにするため。
必要に応じてExecuteの結果をラッパー側でいい感じに処理させる。
Option Explicit
Public Function Execute(ByVal IControl_ As IControl, ByVal EventType_ As EventTypeEnum, ParamArray Arguments_() As Variant) As Varint
End Sub
Option Explicit
Implements IFormCallBack
Private Function IFormCallBack_Execute(ByVal IControl_ As IControl, ByVal EventType_ As EventTypeEnum, ParamArray Arguments_() As Variant) As Variant
Select Case EventType_
Case evClick: Call MsgBox(IControl_.GetValue) 'クリックイベントが起こったら対象のValueをMsgBoxで表示する
End Select
End Sub
実際のコールバックの中身であるIFormCallBack_Executeはどのコントロールがどのイベントを起こしたかを区別できるので、一つのコールバックの中にClickとChangeの処理を書いてSetCallBackで違うイベントに同じコールバックを渡して処理させることもできる。
クラスを複数使って役割を分けるか、クラスが増えるとVBEは醜くなるのでコールバックを集約するかはお好みで。
IControlのGetNameやTypeOfで区別させれば一つのフォームのすべてのイベントを一つのコールバック用クラスで管理することも可能。
コールバック用のインタフェースを持っていればいいので見た目部分を作成するための処理やフォームを管理するクラスにImplementsさせてごちゃごちゃとまとめることもできる。
その場合は他のコントロールに干渉する処理を書きやすくなるが一つのクラスが肥大化していく。
使い方のサンプル
Option Explicit
Sub Sample()
Dim MyForm As UserForm1
Set MyForm = New UserForm1
Dim MSLabel1 As MSForms.Label
Set MSLabel2 = MyForm.Controls.Add("Forms.Label.1","SampleLabel1")
Dim CallBack As IFormCallBack
Set CallBack = New CallBackClickMessage
Dim MyLabel1 As MyLabel, MyLabel2 As MyLabel
Set MyLabel1 = CIControl(New MyLabel) _
.Init(MSLabel1) _
.SetCaption("表示したい文字") _
.SetCallBack(CallBack, evClick) _
.Move(5, 10, 100, 30) _
.SetBackColor(vbRed)
Dim MSLabel2 As MSForms.Label
Set MSLabel2 = MyForm.Controls.Add("Forms.Label.1","SampleLabel2")
With MyLabel1
Set MyLabel2 = CIControl(New MyLabel) _
.Init(MSLabel2) _
.SetCaption("ふたつめ") _
.SetCallBack(CallBack, evClick) _
.Move(.GetRight, .GetBottom, .GetWidth, .GetHeight) _
.SetBackColor(vbBlue)
End With
MyForm.Show vbModal
End Sub
'型変換用
Public Function CIControl(ByVal IControl_ As IControl) As IControl
Set CIControl = IControl_
End Function
Public Function CMyLabel(ByVal IControl_ As IControl) As MyLabel
Set CMyLabel = IControl_
End Function
ラッパーに使用しているメソッドが実装してあれば
ラッパーを作ってそこに実際のフォーム上のLabelを渡して設定を行ってフォームを表示する。
左上あたりに赤いラベルとその右下に青いラベルが作られる。
どちらのラベルにも個別の処理は書いていないが、クリックするとSetCallBackで渡したCallBackClickMessageクラスによってメッセージボックスが表示される。
Dim Dic As Scripting.Dictionary
Set Dic = New Scripting.Dictionary
Dic.Add TempIControl.GetName, TempIcontrol
ラッパークラスのインスタンスはMyLabel1とMyLabel2という個別の変数にしなくても一旦適当な変数に置いてからScripting.Dictionaryでまとめて管理するのが楽。