userform control events for controls added during runtime

Jon von der Heyden

MrExcel MVP, Moderator
Joined
Apr 6, 2004
Messages
10,907
Office Version
  1. 365
Platform
  1. Windows
Hi

I am using table driven forms controls on userforms. E.g.:


Excel 2010
ABCDEFGHIJKLMNOPQRSTUVWXYZAAABACADAEAFAG
1UI NameMultiPage ParentPage ParentControlNameTopLeftHeightWidthCaptionTagControlTipTextSpecialEffectWordWrapMultiLineBorderStyleBorderColorBackColorBackStyleForeColorColumnCountColumnWidthsListStyleMultiSelectSortFilterPrimarySourceClickRight-ClickDouble-ClickViewEdit
2CustomerHeadermpMain1Forms.Label.1lblCustomerID011254Customer IDClick to Sort0TRUE1612566463TRUETRUETRUE
3CustomerHeadermpMain1Forms.Label.1lblCustomerName05512110Customer NameClick to Sort0TRUE1612566463TRUETRUEFALSE
4CustomerHeadermpMain1Forms.Label.1lblTradingName016512110Trading NameClick to Sort0TRUE1612566463TRUETRUEFALSE
5CustomerHeadermpMain1Forms.Label.1lblLegal Name027512110Legal NameClick to Sort0TRUE1612566463TRUETRUEFALSE
6CustomerHeadermpMain1Forms.Label.1lblAddress1038512100Address 10TRUE1612566463FALSETRUEFALSE
7CustomerHeadermpMain1Forms.Label.1lblAddress2048512100Address 20TRUE1612566463FALSETRUEFALSE
8CustomerHeadermpMain1Forms.Label.1lblCity05851275CityClick to Sort0TRUE1612566463TRUETRUEFALSE
9CustomerHeadermpMain1Forms.Label.1lblPostCode06601260Post CodeClick to Sort0TRUE1612566463TRUETRUEFALSE
10CustomerHeadermpMain1Forms.Label.1lblRegion07201275RegionClick to Sort0TRUE1612566463TRUETRUEFALSE
11CustomerHeadermpMain1Forms.Label.1lblCountry07951275CountryClick to Sort0TRUE1612566463TRUETRUEFALSE
12CustomerHeadermpMain1Forms.Label.1lblWebsite08701275Website0TRUE1612566463FALSETRUEFALSE
13CustomerHeadermpMain1Forms.Label.1lblIndustry09451265IndustryClick to Sort0TRUE1612566463TRUETRUEFALSE
14CustomerHeadermpMain1Forms.Label.1lblActive010101245ActiveClick to Sort0TRUE1612566463TRUETRUEFALSE
15CustomerHeadermpMain1Forms.ListBox.1lbxCustomerHeader1202001056Right-click for contextual menu016167772151354;110;110;110;100;100;75;60;75;75;75;65;4500
16CustomerHeadermpMain1Forms.Label.1lblPrimaryContact21711285> Primary ContactClick to see Primary Contact details0TRUE016777215016711680
17CustomerHeadermpMain1Forms.Label.1lblCommercialContact217861285> Commercial ContactClick to see Commercial Contact details0TRUE016777215016711680
18CustomerHeadermpMain1Forms.CommandButton.1cbtAddEdit23911948Add/EditClick to Add or Edit Customer16777215
UI


I use the following (work in progress) function to add the controls to the userform (usually added to page or frame).
Code:
Public Function AddControls(ByVal objTarget As Object, ByVal strUiName As String)
    Dim rngControls As Excel.Range, rngProperties As Excel.Range
    Dim rngControl As Excel.Range, rngProperty As Excel.Range
    Dim objControl As Object
    
    With shtFormUI
        Set rngControls = .Range("D2:D" & .Range("D" & .Rows.Count).End(xlUp).Row)
    End With
    
    For Each rngControl In rngControls
        With rngControl
            If .Offset(0, -3).Value = strUiName Then
                Set objControl = objTarget.Controls.Add(.Value)
                Set rngProperties = Intersect(shtFormUI.Range("E:X"), .EntireRow)
                For Each rngProperty In rngProperties
                    With rngProperty
                        If .Value <> "" Then
                            Call CallByName(objControl, shtFormUI.Cells(1, .Column).Value, VbLet, .Value)
                        End If
                    End With
                Next rngProperty
                If TypeName(objControl) = "ListBox" Then
                    'testing only - retrieve records using SQL recordset
                    objControl.List = Sheet1.Range("A2:M3").Value
                End If
            End If
        End With
    Next rngControl
End Function

Now I need a means of trapping the controls events. I thought I could use a class, e.g.:
cFormEvents
Code:
Option Explicit


Public WithEvents lblLabel As MSForms.Label
Public WithEvents tbxTextBox As MSForms.TextBox
Public WithEvents cbxComboBox As MSForms.ComboBox
Public WithEvents lbxListBox As MSForms.ListBox
Public WithEvents cbtCommandButton As MSForms.CommandButton


Private Sub lblLabel_Click()
    Dim strProcName As String
    
    On Error Resume Next
        strProcName = Application.VLookup(lblLabel.Object.Name, shtFormUI.Range("E:AC"), 29, False)
    On Error GoTo 0
    
    If strProcName <> "" Then
        Call Application.Run(ThisWorkbook.Name & "!" & strProcName)
    End If
End Sub

It seems I cannot reference the controls because I they are added at runtime. For the given example, I want to run whatever procedure name appears for lblPrimaryContact in column AC (click event). So in my userform module I instantiate the class, but I get an error when I try and reference the control:
Code:
Set m_clsFormEvents.lblLabel = Me.lblPrimaryContact

Error is "method or data member not found".

Can anyone suggest an alternative method to grab the click event for the control added at runtime?
 

Excel Facts

How can you automate Excel?
Press Alt+F11 from Windows Excel to open the Visual Basic for Applications (VBA) editor.
In case anyone is wondering - it seems to work if I use the USerForm controls collection:
Code:
Set m_clsFormEvents.lblLabel = Me.Controls("lblPrimaryContact")

Happy days - I think this is much nicer than having to store the event code and write it out each time the control is added... Particularly since I have a few common procs to run on control click and I can use Application.Run to call the proc and populate the arguments... :)
 
Upvote 0
Wouldn't it be easier to set the object reference of the events class at the same time as adding them to the userform rather than looping through the form again? - So something like:
Code:
            If .Offset(0, -3).Value = strUiName Then
                Set objControl = objTarget.Controls.Add(.Value)
                Set rngProperties = Intersect(shtFormUI.Range("E:X"), .EntireRow)
                For Each rngProperty In rngProperties
                    With rngProperty
                        If .Value <> "" Then
                            Call CallByName(objControl, shtFormUI.Cells(1, .Column).Value, VbLet, .Value)
                        End If
                    End With
                Next rngProperty
                If TypeName(objControl) = "ListBox" Then
                    'testing only - retrieve records using SQL recordset
                    objControl.List = Sheet1.Range("A2:M3").Value
					Set m_clsFormEvents = New cFormEvents
					Set m_clsFormEvents.lbxListBox = objControl
					MyEventsCollection.Add m_clsFormEvents
                End If
 
Upvote 0
I actually tried that Kyle - but it doesn't even instantiate the class. I *think* it may be because the AddControls function is separate from the UserForm class...
 
Upvote 0
Glad you figured out the referencing.
I have a Q though...
Does "Object" in lblLabel.Object.Name throw an error for you?
Wouldn't lblLabel.Name get the correct attribute for the vlookup?


strProcName = Application.VLookup(lblLabel.Object.Name, shtFormUI.Range("E:AC"), 29, False)
 
Upvote 0
Good question! You're absolutely correct. When I finally got the class to work it threw a RT error and I quickly fixed it...
 
Upvote 0
Thanks Mike. I am using a collection (well now I am :) ) - but for starters just to make sure it was working I was testing a single control. The controls are added via a function in a standard module. The trouble was setting the class control object by referring to the control name, e.g.
Code:
Set m_clsFormControls.lblLabel = Me.lblPrimaryContact

I didn't have a reference from when the control was added (because it is added via the function in the other module - adding heaps of controls because it is table driven)

It seems I have to use
Code:
Set m_clsFormControls.lblLabel = Me.Controls("control.name")
 
Upvote 0
You can do this by passing the events to the userform. This approach requires two custom classes, clsRTControls, which is a collection of instances of the custom class clsRTControl.
For this example, we are restricting ourselves to TextBoxes, ListBoxes, and Labels. The only events we are using are Change, Click and DblClick
This is the code that goes in clsRTControl. Each kind of control has a Public WithEvents variable of that type. And each of the events for those controls fires one of the custom event listed.
Code:
' in class clsRTControl

Rem control properties
Public RTControl As MSForms.Control
Public WithEvents RTLabel As MSForms.Label
Public WithEvents RTListBox As MSForms.ListBox
Public WithEvents RTTextBox As MSForms.TextBox
Public RTParent  As clsRTControls

Rem other properties
Public Index As Long

Rem event declarations to pass to clsRTControls
Event Change(ctrl As MSForms.Control)
Event Click(ctrl As MSForms.Control)
Event DblClick(ctrl As MSForms.Control, ByRef Cancel As MSForms.ReturnBoolean)


Property Get Name() As String
    Name = RTControl.Name
End Property

Rem events triggered by controls
Private Sub RTLabel_Click()
    Set RTParent.EventedRTControl = Me
    RaiseEvent Click(RTControl)
End Sub
Private Sub RTLabel_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Set RTParent.EventedRTControl = Me
    RaiseEvent DblClick(RTControl, Cancel)
End Sub

Private Sub RTListBox_Change()
    Set RTParent.EventedRTControl = Me
    RaiseEvent Change(RTControl)
End Sub

Private Sub RTListBox_Click()
    Set RTParent.EventedRTControl = Me
    RaiseEvent Click(RTControl)
End Sub
Private Sub RTListBox_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Set RTParent.EventedRTControl = Me
    RaiseEvent DblClick(RTControl, Cancel)
End Sub

Private Sub RTTextBox_Change()
    Set RTParent.EventedRTControl = Me
    RaiseEvent Change(RTControl)
End Sub
Private Sub RTTextBox_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
    Set RTParent.EventedRTControl = Me
    RaiseEvent DblClick(RTControl, Cancel)
End Sub
when clsRTControl fires an event, the EventedRTControl object in clsRTControls captures that event and, in turn, passes it (and the control which fired the event) back to the userform.

Code:
' in class module clsRTControls

Rem collections
Public Item As Collection
Public RT_Controls As Collection

Rem properties
Public Parent As Object

Rem object to receive events passed up by clsRTControl
Public WithEvents EventedRTControl As clsRTControl

Rem event declarations to be passed to userform
Event DblClick(ctrl As MSForms.Control, ByRef Cancel As MSForms.ReturnBoolean)
Event Change(ctrl As MSForms.Control)
Event Click(ctrl As MSForms.Control)

Property Get Count() As Long
    Count = Item.Count
End Property

Function Add(progID As Variant, Optional Name As String = "", Optional isVisible As Boolean = True) As MSForms.Control
    Dim newControl As MSForms.Control
    Dim newRTControl As clsRTControl

    If TypeName(progID) = "String" Then
        If Name = vbNullString Then
            Set newControl = Parent.Controls.Add(progID, visible:=isVisible)
        Else
            Set newControl = Parent.Controls.Add(bstrProgid:=progID, Name:=Name, visible:=isVisible)
        End If
    Else
        Set newControl = progID
    End If
    
    If Not (newControl Is Nothing) Then
        Set newRTControl = New clsRTControl
        With newRTControl
            Set .RTParent = Me
            Set .RTControl = newControl
            Select Case TypeName(.RTControl)
                Case "Label"
                    Set .RTLabel = newControl
                Case "ListBox"
                    Set .RTListBox = newControl
                Case "TextBox"
                    Set .RTTextBox = newControl
                Case Else
                    Set newRTControl = Nothing
            End Select
        End With
    End If
    If newRTControl Is Nothing Then
        MsgBox "Bad progID"
        Parent.Controls.Remove newControl.Name
        Set newControl = Nothing
    Else
        RT_Controls.Add Item:=newRTControl, Key:=newControl.Name
        Item.Add Item:=newControl, Key:=newControl.Name
        newRTControl.Index = Item.Count
    End If
    Set Add = newControl
End Function

Private Sub EventedRTControl_Change(ctrl As MSForms.Control)
    RaiseEvent Change(ctrl)
End Sub

Private Sub EventedRTControl_Click(ctrl As MSForms.Control)
    RaiseEvent Click(ctrl)
End Sub

Private Sub EventedRTControl_DblClick(ctrl As MSForms.Control, ByRef Cancel As MSForms.ReturnBoolean)
    RaiseEvent DblClick(ctrl, Cancel)
End Sub

Private Sub Class_Initialize()
    Set Item = New Collection
    Set RT_Controls = New Collection
End Sub

Private Sub Class_Terminate()
    Dim oneObject As Object
    For Each oneObject In RT_Controls
        oneObject.RTParent.Controls.Remove oneObject.Name
        Set oneObject = Nothing
    Next oneObject
    For Each oneObject In Item
        Set oneObject = Nothing
    Next oneObject
    Set RT_Controls = Nothing
    Set Item = Nothing
End Sub

This is used by a userform with code like.
Code:
' in userform code module
Public WithEvents RunTimeControls As clsRTControls

Private Sub UserForm_Initialize()
    Set RunTimeControls = New clsRTControls
    Set RunTimeControls.Parent = Me
    
    With RunTimeControls
        With .Add("forms.TextBox.1")
            .Height = 22: .Width = 100
            .Top = 5: .Left = 5
            .AutoWordSelect = False
        End With
    
        With .Add("forms.Label.1")
            .Height = 22: .Width = 100
            .Top = 5: .Left = 105
            .BorderStyle = fmBorderStyleSingle
            .Caption = "Made Label"
        End With
        
        With .Add("forms.ListBox.1")
            .Height = 100: .Width = 105
            .Top = 30: .Left = 5
            .AddItem "alpha"
            .AddItem "Beta"
            .AddItem "Gamma"
        End With
    End With
End Sub


Private Sub RunTimeControls_Change(ctrl As MSForms.Control)
    Select Case TypeName(ctrl)
        Case "Label"
            Rem labels dont change
        Case "ListBox"
            With ctrl
                If .ListIndex = -1 Then
                    MsgBox "Change: Nothing is selected from " & ctrl.Name
                Else
                    MsgBox "Change: " & ctrl.Name & " has item # " & (.ListIndex + 1) & " selected." _
                        & vbCr & "Changed to " & .List(.ListIndex)
                End If
            End With
        Case "TextBox"
            MsgBox "The textbox named " & ctrl.Name & " changed to " & ctrl.Text
    End Select
End Sub

Private Sub RunTimeControls_Click(ctrl As MSForms.Control)
    Select Case TypeName(ctrl)
        Case "Label"
            MsgBox ctrl.Name & " clicked"
        Case "ListBox"
            With ctrl
                If .ListIndex = -1 Then
                    MsgBox "Nothing is selected from " & ctrl.Name
                Else
                    MsgBox "Listbox " & ctrl.Name & " clicked."
                End If
            End With
        Case "TextBox"
            Rem textBoxes dont click
    End Select
End Sub

Private Sub RunTimeControls_DblClick(ctrl As MSForms.Control, Cancel As MSForms.ReturnBoolean)
    MsgBox ctrl.Name & " has been double clicked"
End Sub


Private Sub UserForm_Terminate()
    Set RunTimeControls = Nothing
End Sub

Notice that the custom classes don't need to be touched. (Other to add more controls or more events). All the coding for what happens when an event fires is in the userform's code module. As the above example shows, differentiating what to do when different types of controls fire the event can be tedious. One way around that would be for the userform to have a different clsRTControls object for each kind of control.
Code:
' in useform2 code module

Public WithEvents myAddedTextBoxes As clsRTControls
Public WithEvents myAddedListBoxes As clsRTControls
Dim ufEventsDisabled As Boolean

Private Sub UserForm_Initialize()
    Set myAddedTextBoxes = New clsRTControls
    Set myAddedListBoxes = New clsRTControls
    Set myAddedTextBoxes.Parent = Me
    Set myAddedListBoxes.Parent = Me
    
    With myAddedTextBoxes.Add("forms.TextBox.1", Name:="myTextBox1")
        .Top = 25: .Left = 115
        .Height = 22: .Width = 100
    End With
    With myAddedListBoxes.Add("forms.ListBox.1", Name:="myListBox1")
        .Top = 10: .Left = 5
        .Height = 80: .Width = 100
        .AddItem "Smith"
        .AddItem "Jones"
        .AddItem "Williams"
    End With
    
    With myAddedTextBoxes.Add("forms.TextBox.1", Name:="myTextBox2")
        .Top = 105: .Left = 115
        .Height = 22: .Width = 100
    End With
    With myAddedListBoxes.Add("forms.ListBox.1", Name:="myListBox2")
        .Top = 90: .Left = 5
        .Height = 100: .Width = 100
        .AddItem "Able"
        .AddItem "Baker"
        .AddItem "Charlie"
    End With
    
End Sub

Private Sub myAddedListBoxes_Change(ctrl As MSForms.Control)
    Dim matchingTextBox As MSForms.TextBox
    ufEventsDisabled = True
    With ctrl
        Set matchingTextBox = myAddedTextBoxes.Item(Replace(.Name, "myListBox", "myTextBox"))
        If .ListIndex = -1 Then
            matchingTextBox.Text = vbNullString
        Else
            matchingTextBox.Text = .List(.ListIndex)
        
        End If
    End With
    ufEventsDisabled = False
End Sub

Private Sub myAddedListBoxes_Click(ctrl As MSForms.Control)
    Dim matchingListBox
    'ufEventsDisabled
    
End Sub

Private Sub myAddedTextBoxes_Change(ctrl As MSForms.Control)
    Dim matchingListBox As MSForms.ListBox
    Set matchingListBox = myAddedListBoxes.Item(Replace(ctrl.Name, "myTextBox", "myListBox"))
    ufEventsDisabled = True
    With matchingListBox
        If -1 < .ListIndex Then
            ufEventsDisabled = True
            .List(.ListIndex) = ctrl.Text
        End If
    End With
    ufEventsDisabled = False
End Sub


Private Sub UserForm_Terminate()
    Set myAddedListBoxes = Nothing
    Set myAddedTextBoxes = Nothing
End Sub
One could even have different objects (different code) for different kinds of TextBoxes. NumbericTextBoxes vs. AlphabeticTextBoxes are an option. Notice that the .Add function of clsRTControls will also accept an existing control as an argument, so that control would be controlled by the same event code as the other members in the collection class.

Put this code in a userform that has a design-time TextBox (TextBox1)
Code:
' in userform3

Public WithEvents NumericTextBoxes As clsRTControls
Public WithEvents AlphabeticTextBoxes As clsRTControls
Dim ufEventsDisabled As Boolean

Private Sub AlphabeticTextBoxes_Change(ctrl As MSForms.Control)
    ufEventsDisabled = True
    ctrl.Text = Application.Proper(ctrl.Text)
    ufEventsDisabled = False
End Sub

Private Sub NumericTextBoxes_Change(ctrl As MSForms.Control)
    If Not (IsNumeric(ctrl.Text & "0")) Then
        MsgBox "numeric entry only"
    End If
End Sub

Private Sub UserForm_Initialize()
     Dim nullTop As Single
    nullTop = TextBox1.Top + TextBox1.Height + 10
    
    Set NumericTextBoxes = New clsRTControls
    Set AlphabeticTextBoxes = New clsRTControls
    Set NumericTextBoxes.Parent = Me
    Set AlphabeticTextBoxes.Parent = Me
    
    Dim i As Long
    For i = 0 To 5
        With NumericTextBoxes.Add("forms.TextBox.1")
            .Left = 5: .Top = nullTop + i * 28
            .Height = 22: .Width = 75
        End With
        With AlphabeticTextBoxes.Add("forms.TextBox.1")
            .Left = 105: .Top = nullTop + i * 28
            .Height = 22: .Width = 140
        End With
    Next i
    NumericTextBoxes.Add TextBox1
End Sub

Private Sub UserForm_Terminate()
    Set NumericTextBoxes = Nothing
    Set AlphabeticTextBoxes = Nothing
End Sub
Notice that all three of these userforms, with their various actions, need only the two classes above.
 
Upvote 0
Wow! (y)

I particularly like that I can enforce data types in certain types of controls, something I have to do in this project.

Thanks for sharing Mike - it's going to take some work on my part to undo my junk code and integrate this, but seems it will be well worth the effort! :)
 
Upvote 0
That's really nice Mike, couple of suggestions if I may; since you have circular references, you can't tear them down in the terminate event of the RTControls class since it never fires; even though the class is explicitly set to nothing.

The clsRTControls needs a dispose subroutine that is called on the Userform Terminate Routine:
Code:
Private Sub UserForm_Terminate()
    RunTimeControls.Dispose
    'Set RunTimeControls = Nothing - This doesn't actually do anything, it is ignored due to the circular reference
End Sub
Then in clsRTControls:
Code:
Public Sub Dispose()    Dim oneObject As Object
    For Each oneObject In RT_Controls
        oneObject.RTParent.Parent.Controls.Remove oneObject.Name 'Added extra parent to get to the form
        Set oneObject = Nothing
    Next oneObject
    For Each oneObject In Item
        Set oneObject = Nothing
    Next oneObject
    Set RT_Controls = Nothing
    Set Item = Nothing
    Debug.Print "Collection Terminate"
End Sub




Private Sub Class_Terminate()


End Sub

This is easily testable by putting the following in the RTControl Class:
Code:
Private Sub Class_Terminate()
    Debug.Print "Terminate", RTControl.Name
End Sub
 
Last edited:
Upvote 0

Forum statistics

Threads
1,215,417
Messages
6,124,783
Members
449,188
Latest member
Hoffk036

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top