【ExcelVBA・マクロ】ユーザーフォームにコントロールを動的に追加する方法|Controls.Addで実行時にテキストボックスを生成する【コピペOK】

ExcelVBA

はじめに

ユーザーフォームを作るとき、「入力項目の数が毎回違う」「マスタの件数に応じてテキストボックスを増やしたい」という場面に出会うことがあります。あらかじめ最大数分のコントロールを配置しておく方法もありますが、項目数が読みにくくなったり無駄な余白ができたりします。

この記事では、VBAのControls.Addメソッドを使って、フォーム起動時にラベルやテキストボックスを実行時に動的生成する方法を解説します。あわせて、動的に追加したコントロールでもイベント(値の変更など)を検知できるようにする、クラスモジュールを使ったテクニックも紹介します。

スポンサーリンク
スポンサーリンク

Controls.Addの基本構文

Controls.Addは、フォームやフレームに対してコントロールを実行時に追加するメソッドです。

Set ctl = Me.Controls.Add("Forms.TextBox.1", "txtItem1", True)
  • 第1引数:追加するコントロールのプログラムID(Forms.TextBox.1Forms.Label.1Forms.CommandButton.1など)
  • 第2引数:コントロールの名前(省略可)
  • 第3引数:Trueにすると表示状態で追加される

追加したコントロールはObject型(またはMSForms.Control型)の変数で受け取り、LeftTopWidthHeightなどのプロパティで位置とサイズを指定します。

サンプル:項目数に応じてラベル+テキストボックスを生成する

以下は、シートの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 ExplicitUserForm_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

これで、動的に追加したテキストボックスの内容が変更されるたびに、clsTextBoxHandlerTargetBox_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を組み合わせるのがポイントです。マスタの件数に応じて入力項目が変わるようなツールを作る際に、ぜひ活用してみてください。

スポンサーリンク
スポンサーリンク
ExcelVBA
シェアする
いがぴをフォローする

コメント

タイトルとURLをコピーしました