はじめに
ユーザーフォームを作るとき、「入力項目の数が毎回違う」「マスタの件数に応じてテキストボックスを増やしたい」という場面に出会うことがあります。あらかじめ最大数分のコントロールを配置しておく方法もありますが、項目数が読みにくくなったり無駄な余白ができたりします。
この記事では、VBAのControls.Addメソッドを使って、フォーム起動時にラベルやテキストボックスを実行時に動的生成する方法を解説します。あわせて、動的に追加したコントロールでもイベント(値の変更など)を検知できるようにする、クラスモジュールを使ったテクニックも紹介します。
Controls.Addの基本構文
Controls.Addは、フォームやフレームに対してコントロールを実行時に追加するメソッドです。
Set ctl = Me.Controls.Add("Forms.TextBox.1", "txtItem1", True)
- 第1引数:追加するコントロールのプログラムID(
Forms.TextBox.1、Forms.Label.1、Forms.CommandButton.1など) - 第2引数:コントロールの名前(省略可)
- 第3引数:
Trueにすると表示状態で追加される
追加したコントロールはObject型(またはMSForms.Control型)の変数で受け取り、Left・Top・Width・Heightなどのプロパティで位置とサイズを指定します。
サンプル:項目数に応じてラベル+テキストボックスを生成する
以下は、シートのA列にある項目名リストを読み込み、その数だけ「ラベル+テキストボックス」のペアをユーザーフォーム上に生成する例です。フォームにはUserForm1という名前を使用しています。
' UserForm1 のコードモジュール
Option Explicit
Private Sub UserForm_Initialize()
Dim ws As Worksheet
Dim lastRow As Long
Dim i As Long
Dim topPos As Single
Dim lbl As MSForms.Label
Dim txt As MSForms.TextBox
Set ws = ThisWorkbook.Worksheets("項目マスタ")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
If lastRow < 1 Then Exit Sub
topPos = 10
For i = 1 To lastRow
' ラベルを追加
Set lbl = Me.Controls.Add("Forms.Label.1", "lblItem" & i, True)
With lbl
.Caption = ws.Cells(i, "A").Value
.Left = 10
.Top = topPos
.Width = 80
.Height = 18
End With
' テキストボックスを追加
Set txt = Me.Controls.Add("Forms.TextBox.1", "txtItem" & i, True)
With txt
.Left = 100
.Top = topPos
.Width = 120
.Height = 18
.Tag = CStr(i) ' 後で値を取り出すときのキーとして使う
End With
topPos = topPos + 24
Next i
Me.Height = topPos + 40
End Sub
Tagプロパティに項目番号を入れておくことで、後から「何番目の項目のテキストボックスか」を判定できるようにしています。
動的に追加したコントロールでイベントを検知する
Controls.Addで追加したコントロールは、通常のフォームデザイナー上に配置したコントロールのようにtxtItem1_Changeのようなイベントプロシージャを直接書くことができません。イベントを検知したい場合は、クラスモジュールとWithEventsを組み合わせます。
まず、クラスモジュールclsTextBoxHandlerを新規作成します。
' クラスモジュール clsTextBoxHandler
Option Explicit
Public WithEvents TargetBox As MSForms.TextBox
Private Sub TargetBox_Change()
Debug.Print TargetBox.Tag & "番目の項目が変更されました:" & TargetBox.Value
End Sub
次に、UserForm側でテキストボックスを追加するたびに、このクラスのインスタンスを生成して紐付けます。生成したインスタンスはコレクションに保持しておかないと、スコープを抜けた時点でイベントが働かなくなる点に注意してください。
先ほどのUserForm_Initializeを、以下のコードで丸ごと置き換えます(先頭のOption ExplicitとUserForm_Initializeはモジュール内に1つだけになるようにしてください)。
' UserForm1 のコードモジュール(UserForm_Initializeを置き換える)
Option Explicit
Private colHandlers As Collection
Private Sub UserForm_Initialize()
Dim ws As Worksheet
Dim lastRow As Long
Dim i As Long
Dim topPos As Single
Dim lbl As MSForms.Label
Dim txt As MSForms.TextBox
Dim handler As clsTextBoxHandler
Set colHandlers = New Collection
Set ws = ThisWorkbook.Worksheets("項目マスタ")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
If lastRow < 1 Then Exit Sub
topPos = 10
For i = 1 To lastRow
Set lbl = Me.Controls.Add("Forms.Label.1", "lblItem" & i, True)
With lbl
.Caption = ws.Cells(i, "A").Value
.Left = 10: .Top = topPos: .Width = 80: .Height = 18
End With
Set txt = Me.Controls.Add("Forms.TextBox.1", "txtItem" & i, True)
With txt
.Left = 100: .Top = topPos: .Width = 120: .Height = 18
.Tag = CStr(i)
End With
' クラスインスタンスを生成してテキストボックスと紐付け、コレクションに保持
Set handler = New clsTextBoxHandler
Set handler.TargetBox = txt
colHandlers.Add handler
topPos = topPos + 24
Next i
Me.Height = topPos + 40
End Sub
これで、動的に追加したテキストボックスの内容が変更されるたびに、clsTextBoxHandlerのTargetBox_Changeイベントが発火するようになります。
入力された値をまとめて取得する
登録ボタンなどが押されたタイミングで、動的生成したテキストボックスの値をまとめて取得したい場合は、Me.Controlsをループで走査し、Tagプロパティで判定します。
Private Sub btnRegister_Click()
Dim ctl As MSForms.Control
Dim results() As String
Dim maxIndex As Long
Dim idx As Long
maxIndex = colHandlers.Count
ReDim results(1 To maxIndex)
For Each ctl In Me.Controls
If TypeName(ctl) = "TextBox" And ctl.Tag <> "" Then
idx = CLng(ctl.Tag)
results(idx) = ctl.Value
End If
Next ctl
Dim msg As String
Dim i As Long
For i = 1 To maxIndex
msg = msg & i & ": " & results(i) & vbCrLf
Next i
MsgBox msg, vbInformation
End Sub
注意点
- 動的に追加したコントロールは、フォームを閉じると情報が失われます。フォームを再度開いたときに同じ内容を復元したい場合は、シートなどに保存しておく必要があります
- コントロールを大量に追加すると描画が遅くなることがあります。数十個程度であれば問題ありませんが、100個を超えるような場合はスクロール可能なフレームに分割するなどの工夫を検討してください
- クラスのインスタンスをコレクションで保持し忘れると、イベントが発火しなくなります(ガベージコレクションで解放されるため)。必ず
colHandlersのようなモジュールレベル変数に格納してください
まとめ
Controls.Addを使うことで、項目数が可変のユーザーフォームでも柔軟に対応できます。イベントを検知したい場合はクラスモジュールとWithEventsを組み合わせるのがポイントです。マスタの件数に応じて入力項目が変わるようなツールを作る際に、ぜひ活用してみてください。


コメント