applying event handler to dynamic control - vba

I have a userform that dynamically places a commandButton onto the user form but I can't seem to get the dynamic event handler set up properly: Below shows the code for how I have set up the dynamic button:
Set cButton = Me.Controls.Add("Forms.CommandButton.1")
With cButton
.Left = 150
.Top = 0
.Width = 300
.Height = 140
End With
I have also defined dim WithEvents cButton as Commandbutton outside of any sub or procedure and then finally I have the event handler for the dynamic button, which I simply want to output a message for now:
Private Sub cButton_Click()
MsgBox "Dynamic Handler functioning correctly"
End Sub
Now that I am able to create event handlers for dynamic events for individual controls, however I am creating multiple controls and they all have the same name cButton so how would I be able to create individual event handlers for each of them. Below shows the code that is used to create the multiple controls:
If TextBox1 <> vbNullString Then
For i = 1 To TextBox1.Value
Set cButton = Me.Controls.Add("Forms.CommandButton.1")
With cButton
.Left = 150
.Top = 0
.Width = 300
.Height = 140
End With
Next i
End IF

cButton should only be declared once:
Dim WithEvents cButton As CommandButton
Sub UserForm_Initialize()
Set cButton = Me.Controls.Add("Forms.CommandButton.1")
With cButton
.Left = 150
.Top = 0
.Width = 300
.Height = 140
End With
End Sub
Private Sub cButton_Click()
MsgBox "Dynamic Handler functioning correctly"
End Sub

Related

Create Button Dynamically and add OnClick Event

In Outlook 2010 I have a userform called UserForm1.
Here I create a button dynamically and show the form.
How can I add an event (the sub btnLoad_OnClick) when the button was clicked?
Here is my code:
Dim btnLoad As MSForms.CommandButton
Sub btnLoad_OnClick()
MsgBox ("Button Clicked")
End Sub
Sub SaveAttachment()
Set btnLoad = UserForm1.Controls.Add("Forms.CommandButton.1", "btnLoad", True)
With btnLoad
.Caption = "Click Me"
.Left = 30
.Top = 30
.Height = 30
.Width = 60
End With
With UserForm1
.Width = 850
.Show
End With
End Sub
In your UserForm1 module add
Private Sub btnLoad_Click()
MsgBox "Button Clicked"
End Sub
and save it.
Whenever you dynamically add the button btnLoad and click it the message should pop up.

Referencing Control Objects Created in Different Module

dr (no circumstantial details): I am trying to reference a control created programmatically in another module, but it is throwing
"Compile Error: Variable not defined."
Here's what I'm trying to do (and perhaps my approach is wrong entirely, if so, let me know): I am trying to create a userform that uses a listbox to act as a menu in which the user can select an item and see the controls (labels, textboxes) relevant to that item. For instance: item "General Information" would a submitter name, department, date, etc; item "Report request" would have report application, desired app name, etc. Selecting "General Information" on the listbox should .visible=true the frame housing the "GI" controls and hide other frames, and selecting "Report Request" item would .visible=false General Information frame but .visible=true Report Request frame, and so on.
Because I want the controls to be placed in the same place on the userform, I created them all programmatically in a Construct module. It looks like this:
Public Sub GeneralInformationCreator()
Dim i As Integer
Dim labelGeneral As MSForms.Label
Dim frameGeneral As MSForms.Frame
Dim frameRequestInformation As MSForms.Frame
Dim frameSubmitterInformation As MSForms.Frame
Dim labelAppSelect As MSForms.Label
Dim listAppSelect As MSForms.ListBox
Dim labelDateSelect As MSForms.Label
Dim comboMonthSelect, comboDaySelect, comboYearSelect As MSForms.ComboBox
Dim labelSubmitterFName, labelSubmitterLName As MSForms.Label
Dim inputSubmitterFName, inputSubmitterLName As MSForms.TextBox
Dim labelSponsorFName, labelSponsorLName As MSForms.Label
Dim inputSponsorFName, inputSponsorLName As MSForms.TextBox
Dim labelDepartmentName As MSForms.Label
Dim inputDepartmentName As MSForms.TextBox
Set labelGeneral = formRequestWizard.Controls.Add("Forms.Label.1", "labelGeneral", True)
With labelGeneral
.Font.Size = 12
.Top = 12
.Left = 192
.Height = 14.25
.Width = 42
.Caption = "General Information"
.Name = labelGeneral
End With
Set frameGeneral = formRequestWizard.Controls.Add("Forms.Frame.1", "frameGeneral", True)
With frameGeneral
.Top = 30
.Left = 192
.Height = 310
.Width = 384
.Caption = ""
.BorderColor = RGB(255, 255, 255)
Set frameRequestInformation = frameGeneral.Controls.Add("Forms.Frame.1", "frameRequestInformation", True)
With frameRequestInformation
.Top = 15
.Left = 15
.Height = 100
.Width = 350
.Caption = "Request Information"
.BorderStyle = 1
.BorderColor = RGB(0, 0, 0)
Set labelAppSelect = frameRequestInformation.Controls.Add("Forms.Label.1", "labelAppSelect", True)
With labelAppSelect
.Caption = "Select an application:"
.Top = 15
.Left = 15
.Width = 100
.Height = 20
.AutoSize = True
End With
The Construct methods are called at runtime using UserForm_Initialize() like this:
Private Sub UserForm_Initialize()
Call Construct.GeneralInformationCreator
Call Construct.ApplicationDetailsCreator
With formRequestWizard.listMenu
.AddItem ("General Information")
.AddItem ("Application Details")
'.Selected(0) = True
End With
End Sub
Here's the issue: in the userform code, I have a listMenu_Change() that looks like this:
Private Sub listMenu_Change()
If (listMenu.Selected(0) = True) Then
labelGeneral.Visible = True
frameGeneral.Visible = True
Else
labelGeneral.Visible = False
frameGeneral.Visible = False
End If
If (listMenu.Selected(1) = True) Then
labelAppDetails.Visible = True
frameAppDetails.Visible = True
Else
labelAppDetails.Visible = False
frameAppDetails.Visible = False
End If
End Sub
Selecting an item from the listbox throws the error from the tl;dr: "Compile Error: Variable not defined" on the first variable "labelGeneral".
I have tried putting all of the script into the userform code window, and putting the control Dims outside of Subs. The research I've done has mostly led to discussions about how to create event handlers for controls created at runtime, but I've decided to place the listMenu object via the object view so I'm not sure that's applicable.
GeneralInformationCreator should be a class in its own right - even the name says so (it's a name like a class/type that is something, not a verb like a procedure that does something).
See all these declarations you have at procedure scope, live and die at procedure scope - they're local variables that nobody else can see:
Dim labelGeneral As MSForms.Label
Dim frameGeneral As MSForms.Frame
Dim frameRequestInformation As MSForms.Frame
Dim frameSubmitterInformation As MSForms.Frame
Dim labelAppSelect As MSForms.Label
Dim listAppSelect As MSForms.ListBox
Dim labelDateSelect As MSForms.Label
Dim comboMonthSelect [As Variant], comboDaySelect [As Variant], comboYearSelect As MSForms.ComboBox
Dim labelSubmitterFName [As Variant], labelSubmitterLName As MSForms.Label
Dim inputSubmitterFName [As Variant], inputSubmitterLName As MSForms.TextBox
Dim labelSponsorFName [As Variant], labelSponsorLName As MSForms.Label
Dim inputSponsorFName [As Variant], inputSponsorLName As MSForms.TextBox
Dim labelDepartmentName As MSForms.Label
Dim inputDepartmentName As MSForms.TextBox
By moving them up to instance level fields (i.e. module-scope variables) in a class module, you can expose a Public Property Get accessor that makes anyone with an instance of that class able to access these objects.
So instead of this:
Private Sub UserForm_Initialize()
Call Construct.GeneralInformationCreator
Call Construct.ApplicationDetailsCreator
With formRequestWizard.listMenu
.AddItem ("General Information")
.AddItem ("Application Details")
'.Selected(0) = True
End With
End Sub
You will have that:
Private generalInfoControls As New GeneralInformationCreator
Private appDetailsControls As New ApplicationDetailsCreator
Private Sub UserForm_Initialize()
generalInfoControls.Create Me.Controls
appDetailsControls.Create Me.Controls
With Me.listMenu 'don't refer to the default instance in the form's code-behind... EVER!
.AddItem "General Information"
.AddItem "Application Details"
'.Selected(0) = True
End With
End Sub
Where Create is a procedure that takes a Controls parameter - by giving it Me.Controls we are passing the collection of controls for the current instance of the user form, so the method can look like this:
Public Sub Create(ByVal parent As Controls)
Set labelGeneral = parent.Add("Forms.Label.1", "labelGeneral", True)
With labelGeneral
.Font.Size = 12
.Top = 12
.Left = 192
.Height = 14.25
.Width = 42
.Caption = "General Information"
.Name = labelGeneral
End With
'...
Notice it's no longer coupled with the default instance of your formRequestWizard form, and will work with whatever form instance's Controls collection it's given.
I'm not sure if you're still following my train of thought here, but this means your handler can now do this:
Private Sub listMenu_Change()
generalInfoControls.SetVisibility listMenu.Selected(0)
appDetailsControls.SetVisibility listMenu.Selected(1)
End Sub
That implies that your XxxxCreator classes have a SetVisibility method that looks something like this:
Public Sub SetVisibility(ByVal isVisible As Boolean)
labelGeneral.Visible = isVisible
frameGeneral.Visible = isVisible
'...
Define all your variables like this:
Dim comboMonthSelect As MSForms.Combobox
Dim comboDaySelect As MSForms.Combobox
Dim comboYearSelect As MSForms.ComboBox
With
Dim comboMonthSelect, comboDaySelect, comboYearSelect As MSForms.ComboBox,
only comboYearSelect is declared as a MSForms.ComboBox and the other two as Variant.
Then write Option Explicit and make sure that every variable is defined in every Sub/Function. In your case is should be:
Dim labelGeneral As MSForms.Label
in the Sub listMenu_Change, where the error is happening. MSDN Option Explicit reference.
As a quick and dirty way, you can do something like this:
If (listMenu.Selected(0) = True) Then
Controls("labelGeneral").Visible = True
Controls("someOtherName").Visible = True
End If
However, this breaks completely any MVC pattern and may cause any non-VBA people to feel dizzy and to publish things like this:
If you want to do it the "correct way", read this article form the ex-StackOverflow Documentation, it is quite nice:
http://www.riptutorial.com/vba/example/19036/best-practices.

Dynamic created User-Form, with 2 dependent Combo-Boxes

I am trying to create a dynamic User_form, where all the Controls are created at Run-Time.
I have 2 array of Combo-Boxes, first array of Combo-Boxes is "Catgeory" (CatCBArr) , and the second array of Combo-Boxes is "Item" (ItemCBArr).
I want, that once I select a value from the first Combo-Box of "Category", let's say CatCBArr(0), that only the related Items in ItemCBArr(0) will be displayed.
Issue: I can't figure out how to modify the second Combo-box (ItemCBArr(0)) according to the value selected in the first Combo-box (CatCBArr(0))
User_Form Code (relevant section)
Option Explicit
Dim ItemsNumofRows As Long
Dim QtyTB As MSForms.TextBox
Dim CatCB As MSForms.ComboBox
Dim ItemCB As MSForms.ComboBox
Dim Key As Variant
' dynamic Form controls (related to new Classes)
Dim CatCBArr() As New cComboBox
Dim ItemCBArr() As New cComboBox
Dim QtyTBArr() As New cTextBox
Private Sub UserForm_Initialize()
' reset flags
ItemsNumofRows = 5
TasksNamesUpd = False
TasksColUpd = False
ItemsRows_ControlsInit '<-- upload all Controls at run-time
Check_FormHeight
End Sub
'======================================================
Private Sub ItemsRows_ControlsInit()
For ItemRow = 0 To ItemsNumofRows
' add Category Combo-boxes
Set CatCB = Me.Controls.Add("Forms.ComboBox.1", "Cb" & ItemRow, True)
With CatCB
' loop through Dictionay items (view category)
For Each Key In Dict.Keys
.AddItem Key
Next Key
.SpecialEffect = fmSpecialEffectSunken
.Left = 40
.Width = 100
.Height = 18
.Top = 54 + 20 * ItemRow
ReDim Preserve CatCBArr(0 To ItemRow)
Set CatCBArr(ItemRow).ComboBoxEvents = CatCB
End With
' add Item Combo-boxes
Set ItemCB = Me.Controls.Add("Forms.ComboBox.1", "Cb_" & ItemRow, True)
With ItemCB
.SpecialEffect = fmSpecialEffectSunken
.Left = 160
.Width = 100
.Height = 18
.Top = 54 + 20 * ItemRow
ReDim Preserve ItemCBArr(0 To ItemRow)
Set ItemCBArr(ItemRow).ComboBoxEvents = ItemCB
End With
Next ItemRow
End Sub
cComboBox Class Code
Public WithEvents ComboBoxEvents As MSForms.ComboBox
Private Sub ComboBoxEvents_Change()
Dim CBIndex As Long
' get for ID number (row number), from third character in String Name.
' e.g "Cb1" will result 1)
CBIndex = CInt(Mid(ComboBoxEvents.Name, 3))
' ??? How do I get the Value, and update the second combo-box Items
Select Case ComboBoxEvents.Value
End Select
End Sub
GUI User_Form screen-shot
Alright, here's the basics.
Your class cCombobox I replicated as follows:
Private WithEvents ComboBoxEvents As MsForms.ComboBox
Private Sub ComboBoxEvents_Change()
Select Case ComboBoxEvents.value
Case "1":
UserForm1.DependentBox.Clear
UserForm1.DependentBox.AddItem "3"
UserForm1.DependentBox.AddItem "4"
Case "2":
UserForm1.DependentBox.Clear
UserForm1.DependentBox.AddItem "5"
UserForm1.DependentBox.AddItem "6"
Case Default:
'Do Nothing
End Select
End Sub
Public Property Let box(value As MsForms.ComboBox)
Set ComboBoxEvents = value
End Property
Public Property Get box() As MsForms.ComboBox
Set box = ComboBoxEvents
End Property
Next, I created a UserForm1 that adds 2 comboboxes, one of which I add to a local variable of type cComboBox.
Public DependentBox As MsForms.ComboBox
Private InitialBox As cComboBox
Private Sub UserForm_Initialize()
Dim cBox As MsForms.ComboBox
Set InitialBox = New cComboBox
Set cBox = Me.Controls.Add("Forms.ComboBox.1", "initial", True)
With cBox
.Left = 6
.Width = 100
.Height = 25
.Top = 6
.AddItem "1"
.AddItem "2"
End With
InitialBox.box = cBox
Set DependentBox = Me.Controls.Add("Forms.ComboBox.1", "dependent", True)
With DependentBox
.Top = 6
.Left = 126
.Height = 25
.Width = 100
End With
End Sub
Even though this works, the above approach is not very clean, since your class is not self-contained - It has to be aware of the UserForm. A better way would be to link the boxes in the class already and then just pass them from the Userform when you initialize your arrays of controls.
Then it would be:
cComboBox class:
Private WithEvents p_ComboBoxEvents As MSForms.ComboBox
Private p_DependBox As MSForms.ComboBox
Private Sub p_ComboBoxEvents_Change()
Select Case p_ComboBoxEvents.value
Case "1":
p_DependBox.Clear
p_DependBox.AddItem "3"
p_DependBox.AddItem "4"
Case "2":
p_DependBox.Clear
p_DependBox.AddItem "5"
p_DependBox.AddItem "6"
Case Default:
'Do Nothing
End Select
End Sub
Public Property Let TriggerBox(value As MSForms.ComboBox)
Set p_ComboBoxEvents = value
End Property
Public Property Get TriggerBox() As MSForms.ComboBox
Set TriggerBox = p_ComboBoxEvents
End Property
Public Property Let DependBox(value As MSForms.ComboBox)
Set p_DependBox = value
End Property
Public Property Get DependBox() As MSForms.ComboBox
Set DependBox = p_DependBox
End Property
Here you see you already link the boxes in a self-contained class.
In the event handler you could create a lookup for the values, etc.
Then in the UserForm1 code you initialize them as follows:
Option Explicit
Private LinkedComboBox As cComboBox
Private Sub UserForm_Initialize()
Dim cBox As MSForms.ComboBox
Set LinkedComboBox = New cComboBox
Set cBox = Me.Controls.Add("Forms.ComboBox.1", "initial", True)
With cBox
.Left = 6
.Width = 100
.Height = 25
.Top = 6
.AddItem "1"
.AddItem "2"
End With
LinkedComboBox.TriggerBox = cBox
Set cBox = Me.Controls.Add("Forms.ComboBox.1", "dependent", True)
With cBox
.Top = 6
.Left = 126
.Height = 25
.Width = 100
End With
LinkedComboBox.DependBox = cBox
End Sub
EDIT:
Based on the fact that it needs to be an array, you can amend the userform as follows:
Option Explicit
Private LinkedComboBox(0 To 4) As cComboBOx
Private Sub UserForm_Initialize()
Dim cBox As MSForms.ComboBox
Dim i As Integer
For i = 0 To 4
Set LinkedComboBox(i) = New cComboBOx
Set cBox = Me.Controls.Add("Forms.ComboBox.1", "initial", True)
With cBox
.Left = 6
.Width = 100
.Height = 25
.Top = 6 + (i * 25)
.AddItem "1"
.AddItem "2"
End With
LinkedComboBox(i).TriggerBox = cBox
Set cBox = Me.Controls.Add("Forms.ComboBox.1", "dependent", True)
With cBox
.Top = 6 + (i * 25)
.Left = 126
.Height = 25
.Width = 100
End With
LinkedComboBox(i).DependBox = cBox
Next i
End Sub
In the array you can access each box as LinkedComboBox(i).DependBox and LinkedComboBox(i).TriggerBox. You won't need the two seperate arrays anymore, since everything is already contained in this LinkedComboBox array

Creating and populating combobox from vba module

In Excel 2010, I am trying to create a userform dynamically in my vba code. However, after running the code, the list does not show when opening the dropdown. If I put a breakpoint before ".show", I can inspect the form in the design window and I see that the list is populated. Does the .show method clear the list or what is happening? The solutions I have found focus on populating from cell ranges or placing the ".additem"s in the initialization code of the userform. I don't want to do anything that requires me to create a second file. Everything should be in this vba code if possible. Any help appreciated. My code follows.
Sub make_combobox_form()
' Makes a form with only a simple combobox
Dim myForm As Object
Dim cb As MSForms.ComboBox
'Create the User Form
Set myForm = ThisWorkbook.VBProject.VBComponents.Add(vbext_ct_MSForm)
With myForm
.Properties("Width") = 400
.Properties("Height") = 300
End With
Set cb = myForm.Designer.Controls.Add("Forms.ComboBox.1")
With cb
.Top = 100
.Left = 100
.Height = 20
.Width = 200
.AddItem "Item_1"
.AddItem "Item_2"
.AddItem "Item_3"
.value = "Item_1"
End With
VBA.UserForms.Add(myForm.name).Show
ThisWorkbook.VBProject.VBComponents.Remove myForm
End Sub
Sub make_combobox_form()
'Create the User Form as component first
Dim myFormComponent As VBComponent
Set myFormComponent = ThisWorkbook.VBProject.VBComponents.Add(vbext_ct_MSForm)
' Get reference to user form from the UserForms collection then
Dim cb As MSForms.ComboBox
Dim myForm As Variant
Set myForm = VBA.UserForms.Add(myFormComponent.Name)
With myForm
.Width = 400
.Height = 300
End With
Set cb = myForm.Controls.Add("Forms.ComboBox.1")
With cb
.Top = 100
.Left = 100
.Height = 20
.Width = 200
.AddItem "Item_1"
.AddItem "Item_2"
.AddItem "Item_3"
.Value = "Item_1"
End With
myForm.Show
ThisWorkbook.VBProject.VBComponents.Remove myFormComponent
End Sub

Excel VBA ComboBox Identification

I have 4+ ComboBoxes on a user form. When they fire, they fire the same event. What I am trying to do is find out which ComboBox triggered the event. The ComboBoxes are created depending on how many components there are. The code generating the ComboBoxes is shown below:
For j = 0 To UBound(ComponentList) - 1
'Set Label
num = j + 1
Set control = UserForm1.Controls.Add("Forms.Label.1", "ComponentLabel" & CStr(num) & ":", True)
With control
.Caption = "Component " & CStr(num)
.Left = 30
.Top = Height
.Height = 20
.Width = 100
.Visible = True
End With
'set ComboBox
Set combo = UserForm1.Controls.Add("Forms.ComboBox.1", "Component" & num & ":", True)
With combo
.List = ComponentList()
.Left = 150
.Top = Height
.Height = 20
.Width = 50
.Visible = True
Set cButton = New clsButton
Set cButton.combobox = combo
coll.Add cButton
End With
Height = Height + 30
Next j
This works well and I can get the value the user selected, BUT I can not find which ComboBox has been used. This code below is the event that it fires (clsButton):
Public WithEvents btn As MSForms.CommandButton
Public WithEvents combobox As MSForms.combobox
Private combolist() As String
Private Sub btn_Click()
If btn.Caption = "Cancel" Then
MsgBox "Cancel"
Unload UserForm1
Variables.ComponentSelectionError = False
ElseIf btn.Caption = "Enter" Then
MsgBox "enter"
Unload UserForm1
Variables.ComponentSelectionError = True
End If
End Sub
Private Sub combobox_Click()
MsgBox combobox.Value
End Sub
This bit of code above was kindly worked on by Doug Glancy to get the events working with the code generated ComboBoxes.
How do I get the ComboBox that triggered the event? i.e. the name or some other form of identification.
I have managed to finally answer my own question after searching over 500 webpages (took a long time)
this is what i used and it works and fires when the certain comboboxes are clicked:
Private Sub combobox_Click()
MsgBox combobox.Value
If combobox = UserForm1.Controls("Component0") Then
MsgBox "Success1"
End If
If combobox = UserForm1.Controls("Component1") Then
MsgBox "Success2"
End If
End Sub
hopefully this can be used for other people who need it.
Within the class .Name will not appear in the intellisense list for the combobox as MSForms.ComboBox does not actually have a name property itself (take a look at it in the F2 object browser), rather that property is provided by the Control base class:
Private Sub combobox_Click()
MsgBox combobox.Value
MsgBox combobox.Name '// no hint but still works
'//cast to a Control to get the formal control interface with .Name
Dim ctrl As Control: Set ctrl = combobox
MsgBox ctrl.Name
End Sub
Maybe reference back to btn.Combobox again? Similar to how you assigned the combobox to the button in the first place, but then in reverse:
set combobox = btn.Combobox
Is there a reason you don't just add a property to your custom class and set that property when you register in the Collection?
For j = 0 To UBound(ComponentList) - 1
'Set Label
num = j + 1
Set control = UserForm1.Controls.Add("Forms.Label.1", "ComponentLabel" & CStr(num) & ":", True)
With control
.Caption = "Component " & CStr(num)
.Left = 30
.Top = Height
.Height = 20
.Width = 100
.Visible = True
End With
'set ComboBox
Set combo = UserForm1.Controls.Add("Forms.ComboBox.1", "Component" & num & ":", True)
With combo
.List = ComponentList()
.Left = 150
.Top = Height
.Height = 20
.Width = 50
.Visible = True
Set cButton = New clsButton
'*******EDIT********
with cButton
.combobox = combo
.Indx = j
end With 'cButton
'*******************
coll.Add cButton
End With
Height = Height + 30
Next j
Class Module
Public WithEvents btn As MSForms.CommandButton
Dim WithEvents mCombobox As MSForms.comboBox
Private combolist() As String
'*******EDIT********
Public Indx As Long
Property Let comboBox(cb As MSForms.comboBox)
Set mCombobox = cb
End Property
'*******************
Private Sub btn_Click()
If btn.Caption = "Cancel" Then
MsgBox "Cancel"
Unload UserForm1
Variables.ComponentSelectionError = False
ElseIf btn.Caption = "Enter" Then
MsgBox "enter"
Unload UserForm1
Variables.ComponentSelectionError = True
End If
End Sub
Private Sub mCombobox_Click()
'*******EDIT********
MsgBox "Combobox " & Indx & Chr(9) & mComboBox.Value
'*******************
End Sub
Since you require many to one mapping of the events, I assume you have a common call-back in your actual code, so you could also do this...
In a Standard Module
Public Sub cbCallBack(ocb As clsButton)
MsgBox ocb.Indx
End Sub
In clsButton (replacing the event handler)
Private Sub mCombobox_Click()
cbCallBack Me
End Sub