VBA WithEvents not working - vba

I am basically creating a list of items that are generated at runtime. The items are listed on a userform as labels(the items are stored in a linked list). With each item, I want to add a spinbutton so I can move the items up and down the list. I the spinbuttons are created just fine, the events I have coded do not work?? I am not sure what I am doing wrong. Probably something simple...
This is the class module to hold the events: cls_Spin_Btn
Private WithEvents Spin_Events As SpinButton
Private Sub Spin_Events_SpinUp()
Debug.Print "Hey. Spin button worked."
End Sub
Public Property Set SetNewSpinButtion(newSpinBtn As MSForms.SpinButton)
Set Spin_Events = newSpinBtn
End Property
This is code is calling from a module:
Function AddRunToForm(f As UserForm, r As ProductionRun, top As Integer) As Integer
Dim Run_SpinBtn As MSForms.SpinButton
Dim spinBtn As cls_Spin_Btn
Set Run_SpinBtn = f.Controls.Add("Forms.SpinButton.1", r.ProdID & "_SBtn", True)
Set spinBtn = New cls_Spin_Btn
With Run_SpinBtn
.top = ProdID_Lbl.top
.Left = 5
.height = 10
.Width = 12
.height = 18
.Visible = True
End With
Set spinBtn.SetNewSpinButtion = Run_SpinBtn
AddRunToForm = ProdID_Lbl.top + ProdID_Lbl.height
End Function
This code is called from a loop in the same module creating labels and spinbuttons for each item. What am I doing wrong? Any help would be very much appreciated.

In your userform code module, put this
Private mcolSpinButtons As Collection
Public Property Get SpinButtonCollection() As Collection
If mcolSpinButtons Is Nothing Then Set mcolSpinButtons = New Collection
Set SpinButtonCollection = mcolSpinButtons
End Property
That will give you access to a module level variable that will stay in scope as long as your userform is open. When you put cls_Spin_Btn instances in that collection, they will also stay in scope.
Then in your function, once you create the new spin button class instance, add it to the collection
f.SpinButtonCollection.Add spinBtn, spinBtn.Name

Related

How to tell which dynamic control sent to an event?

This is my first attempt at working with dynamically created controls in a user form. The reason is there will always be a different amount of rows returned by some processing.
I have created a class object cControlEvent with the following code. (I cut out the code not pertaining to the checkbox)
Public WithEvents CHK As MSForms.CheckBox
Private Sub CHK_Change()
** tell me which box was changed **
End Sub
in the code module, I have the following code:
Dim CHK_Evts As New Collection
sub Form_Builder()
**non relevant code deleted****
Set Evt = New cControlEvent
If i_Columns = 1 Then
Set Evt.CHK = ctl
CHK_Evts.Add Evt
Else
** more code**
End if
end sub
What do I need to change/add to be able to get the name of the control that is firing off the change event?
EDITED TO ADD:
I have a series of dynamically created checkboxes and textboxes on each line of a user form, with a checkbox before each line, when the checkbox is checked/unchecked, I need to change the backcolor on all the textboxes in that row. Each control is named by it's type, then row then column like this CHX_1_1 would be a checkbox on row 1 column 1, and TXT_1_5 would be row 1 column 5. So, if I know what the name of the checkbox is, I have all I need to change the other controls on that row with a simple for-next loop.
I am not quite sure if I understand your question correctly. But it seems to me that it boils down to "which FormControl (linked to a particular procedure) caused this sub to run". If that's the case then you should be able to make use of the
Application.Caller
Here is a short video to demonstrate it's use in a very simple environment:
Here's hopefully a full solution showing how to get the properties from the check boxes:
Create a blank userform and add a command button to it.
Add this code to the form (note - CommandButton1_Click should be updated to the name of the button you added).
Public CHK_Evts As New Collection
Private Sub CommandButton1_Click()
Dim ChkBox As Variant
For Each ChkBox In CHK_Evts
MsgBox ChkBox.Position & vbCr & _
ChkBox.Status
Next ChkBox
End Sub
Private Sub UserForm_Initialize()
Dim tmpCtrl As Control
Dim cmbEvent As clsControlEvents
Dim X As Long
For X = 1 To 10
Set tmpCtrl = frmNameParser.Controls.Add("Forms.Checkbox.1", "Name" & X)
With tmpCtrl
.Left = 6
.Top = X * 20 + 24
.Height = 18
.Width = 150
End With
Set cmbEvent = New clsControlEvents
Set cmbEvent.CHK = tmpCtrl
CHK_Evts.Add cmbEvent, "Name" & X
Next X
End Sub
Create a class called clsControlEvents and add this code:
Public WithEvents CHK As MSForms.CheckBox
Public Property Get Position() As String
Position = CHK.Top
End Property
Public Property Get Status() As String
Status = CHK.Value
End Property
Private Sub CHK_Click()
MsgBox CHK.Name
End Sub
The two GET procedures pass information back to the CommandButton1_Click procedure so it can list information about all check boxes on the form (held in the CHK_EVTS collection).
The CHK_Click procedure gives immediate information about the check box being clicked.
http://www.cpearson.com/excel/classes.aspx

Userform controlled variables within a macro

Morning Guys,
I have ran into a small roadblock with my project. I'm new to VBA and am trying my best to 'learn by doing' but I cannot seem to get my head around macro/userform interactions.
I have a userform with one textbox and 9 checkboxes. This is supposed to show the userform, allow the user to dictate a sheet name, and (from a list of 9 users) select which is active or not (true or false).
In my main sub, I just have a
Allocator.show
command, as you may have guessed, allocator is my userform name.
Then I've sort of just been trying things so I don't know how right the rest of the userform code is;
Private Sub cbGo_Click()
Unload Allocator
End Sub
Private Sub cboxAlison_Click()
If Me.cboxAlison.Value = True Then
AlisonYN = True
Else
AlisonYN = False
End If
End Sub
Private Sub cboxBeverly_Click()
If Me.cboxBeverly.Value = True Then
BevelyYN = True
Else
BevelyYN = False
End If
End Sub
Private Sub cboxCallum_Click()
If Me.cboxCallum.Value = True Then
CallumYN = True
Else
CallumYN = False
End If
End Sub
Private Sub cboxEllen_Click()
If Me.cboxEllen.Value = True Then
EllenYN = True
Else
EllenYN = False
End If
End Sub
Private Sub cboxGeoff_Click()
If Me.cboxGeoff.Value = True Then
GeoffYN = True
Else
GeoffYN = False
End If
End Sub
Private Sub cboxJames_Click()
If Me.cboxJames.Value = True Then
JamesYN = True
Else
JamesYN = False
End If
End Sub
Private Sub cboxLouise_Click()
If Me.cboxLouise.Value = True Then
LouiseYN = True
Else
LouiseYN = False
End If
End Sub
Private Sub cboxMick_Click()
If Me.cboxMick.Value = True Then
MickYN = True
Else
MickYN = False
End If
End Sub
Private Sub cboxTammy_Click()
If Me.cboxTammy.Value = True Then
TammyYN = True
Else
TammyYN = False
End If
End Sub
Private Sub tbRPName_Change()
End Sub
Private Sub UserForm_Initialize()
Dim GeoffYN, TammyYN, CallumYN, JamesYN, MickYN, AlisonYN, BeverlyYN, LouiseYN, EllenYN As Boolean
Dim RP_Name As String
Me.cboxGeoff.Value = True
Me.cboxTammy.Value = True
Me.cboxCallum.Value = True
Me.cboxJames.Value = True
Me.cboxMick.Value = False
Me.cboxAlison.Value = False
Me.cboxBeverly.Value = False
Me.cboxLouise.Value = False
Me.cboxEllen.Value = False
Me.tbRPName = ""
End Sub
All of the named user variables (xxxxYN) are public in my main module.
These are the variables I want to pull back into my main macro as true or false following the user checking the desired boxes, along with the name as a string, and then continue running the original macro.
Any help would be greatly appreciated, I seem to be taking myself round in circles at the moment!
PS if it helps, my userform looks like this;
UserForm
Cheers,
Callum
You wrote "All of the named user variables (xxxxYN) are public in my main module." But we see them declared in userform's Sub UserForm_Initialize, too:
Private Sub UserForm_Initialize()
Dim GeoffYN, TammyYN, CallumYN, JamesYN, MickYN, AlisonYN, BeverlyYN, LouiseYN, EllenYN As Boolean
Dim RP_Name As Stringn
...
even if you declared the same variables as Public in any module, the Userform variables hide their Public namsakes so any Userform setting is not "seen" in other modules
so you'd better remove the Userform dimming statement of the "namesakes" and leave only the Public one
moreover in such a declaration statement as you used, every single variable not explicitly associated with a specific type is implicitly associated to a Variant type
so in the main module you should use a "dimming" statement like follows:
Public GeoffYN As Boolean, TammyYN As Boolean, CallumYN As Boolean, JamesYN As Boolean, MickYN As Boolean, AlisonYN As Boolean, BeverlyYN As Boolean, LouiseYN As Boolean, EllenYN As Boolean
But should all what above get you going, nevertheless I'd recommend you to switch to a "class" approach together with the use of Dictionary object, like follows
in the Allocator code pane place the following code
Option Explicit
Dim chkBoxes() As ChkBx_Class 'array of type "ChkBx_Class" which you define in a Class Module
Private Sub UserForm_Initialize()
Dim nControls As Integer, i As Integer
Dim namesArray As Variant, cbIniValues As Variant
UFInit = True
namesArray = Array("Geoff", "Tammy", "Callum", "James", "Mick", "Alison", "Beverly", "Louise", "Ellen") '<== set here the names to be associated to every CheckBox
cbIniValues = Array(True, True, True, True, False, False, False, False, False) '<== set here the initial values of checkboxes
nControls = UBound(namesArray) + 1 '<== retrieve the number of CheckBoxes you're going to consider in the Form
ReDim chkBoxes(1 To nControls) As ChkBx_Class 'redim the "ChkBx_Class" array
For i = 1 To nControls
Set chkBoxes(i) = New ChkBx_Class 'initialize a new instance of 'ChkBoxClass' class and store it in the array i-th position
With chkBoxes(i)
Set .ChkBox = Me.Controls("CheckBox" & i) 'assign the correct CheckBox control to its "ChkBox" property
.Name = namesArray(i - 1) ' assign the Name property of the Checkbox
.ChkBox.Value = cbIniValues(i - 1) 'set the checkbox correct initial value
Me.Controls("Label" & i) = .Name ' set the corresponding label caption
dealersDict.Add .Name, .ChkBox.Value ' fill the dictionary initial pair of Dealer-name/checkbox-value
End With
Next i
Me.tbRPName.Text = ""
UFInit = False
End Sub
Private Sub cbGo_Click()
Me.Hide
End Sub
add a "Class Module" to your project
either clicking Insert-> Class Module in the VBA IDE main Ribbon menu
or right-clicking anywhere in the VBA IDE Project Window and selecting Insert -> Class Module in subsequent sub-menus
expand the "Class Module" node in the Project Window
if you don't see the Project Window you can open it by clicking View-> Project Window in the main ribbon menu, or press "Ctrl+R"
select the new Class you added (it should be some "Class1" or the likes) and change its name to "ChkBx_Class" in the Property Window "Name" textbox
if you don't see the Property Window you can open it by clicking View-> Property Window in the main ribbon menu or press "F4"
in the Class Module code pane place the following
Option Explicit
'declare class properties: they will be associated in every instance of this class.
Public WithEvents ChkBox As MSForms.CheckBox ' "ChkBox" is now a property of the class of type CheckBox. it's associated to events
Public Name As String
' events associated to ChkBox class property
Sub ChkBox_Click()
If Not UFInit Then dealersDict.Item(Me.Name) = Me.ChkBox.Value ' set the dictionary pair of Dealer-name/checkbox-value
End Sub
edit your main sub module as follows
Option Explicit
Public dealersDict As New Scripting.Dictionary
Public UFInit As Boolean
Sub main()
myval = "io"
Dim myKey As Variant
Allocator.Show
Unload Allocator
For Each myKey In dealersDict
MsgBox myKey & ": " & dealersDict(myKey)
Next myKey
End Sub
create a reference to Microsoft Scripting Runtime Library to use Dictionaries.
this is done by choosing Tools➜References command in the Visual Basic Editor (VBE) which pops up a dialog box in whose listbox you are to find "Microsoft Scripting Runtime" to put a check mark next and press OK.
run the main sub
whenever you need to retrieve the boolean value associated to a given name you just have to use
myBool = dealersDict(name)
where name can be:
a string literal with the wanted name ("Alison", "Mick" , ..)
a string variable whose value stores the wanted name, so that somewhere in your code you may have typed:
Dim name as string
name = "Mick"
such an approach gives you a lot of flexibility, since you only have to:
set the names and their initial boolean values in those two arrays (namesArray and cbIniValues) in UserForm_Initialize
make sure you have checkboxes named after "CheckBox1", "CheckBox2", and so on as well as have labels named after "label1", "Label2", and so on
make sure that "CheckBoxX" is aligned with "LabelX"
make sure namesArray and cbIniValues have the same items number as labels and checkboxes
IDK what the actual issue is, but I tried to recreate your issue and just decided to show you what I have. See if any of this helps you at all.
All of this code is in the userform code, not at the module level. When I change the check box values, the values are stored (outside of the main sub, which is validated in the "check" sub click event).
To make you code a little shorter, you can directly assign the value of a checkbox to a variable
Dim test as Boolean
test = me.CheckBox1.Value
You can insert this into the code of your go button

Actions for multiple similar dynamically created command buttons

I am writing a program in Excel VBA that will basically start with one textbox and one command button, and the command button will create a new textbox and command button underneath it, and that command button will in turn create a new textbox and command button, and so on. Hope you followed that mess.
I can create the initial button no problem (it has to be dynamically created so it has the opportunity to be deleted later). My problem is then with creating the click() event handler. I need all the click() events to do the same thing, but name the new Objects relative to its own name. This is all blowing my mind, I would really appreciate a little help.
Feel free to ask for specific information, but I haven't really been able to wrap my head around the topic well enough to write some test code yet.
Create a custom class module called CEventClass (Insert - Class Module, F4 to change the name). Type this code into the class module
'These are declared WithEvents so the events are
'exposed to us
Public WithEvents cmdEvent As MSForms.CommandButton
Public WithEvents tbxEvent As MSForms.TextBox
'This will fire for any control
'assigned to cmdEvent
Private Sub cmdEvent_Click()
MsgBox cmdEvent.Caption
End Sub
'This will fire for any control
'assigned to tbxEvent
Private Sub tbxEvent_Change()
If Len(tbxEvent.Text) < 6 Then
tbxEvent.BackColor = vbYellow
Else
tbxEvent.BackColor = vbWhite
End If
End Sub
Now create a Userform with no controls on it. Put this code in the form's code module
'These will keep the class instances in
'scope for as long as the form is loaded
Private mEventButtons As Collection
Private mEventTexts As Collection
Private Sub UserForm_Initialize()
Dim cmd As MSForms.CommandButton
Dim txt As MSForms.TextBox
Dim clsEventClass As CEventClass
Set mEventButtons = New Collection
Set mEventTexts = New Collection
'Create two commandbuttons
Set cmd = Me.Controls.Add("Forms.CommandButton.1", "FirstName")
cmd.Top = 10
cmd.Left = 10
cmd.Caption = "First"
'Create a new instance of CEventClass and
'assign the button to cmdEvent
Set clsEventClass = New CEventClass
Set clsEventClass.cmdEvent = cmd
mEventButtons.Add clsEventClass
Set cmd = Me.Controls.Add("Forms.CommandButton.1", "SecondName")
cmd.Top = 50
cmd.Left = 10
cmd.Caption = "Second"
Set clsEventClass = New CEventClass
Set clsEventClass.cmdEvent = cmd
mEventButtons.Add clsEventClass
'Create two textboxes and assign them to new instances
'of the class
Set txt = Me.Controls.Add("Forms.TextBox.1", "ThirdName")
txt.Top = 10
txt.Left = 150
Set clsEventClass = New CEventClass
Set clsEventClass.tbxEvent = txt
mEventTexts.Add clsEventClass
Set txt = Me.Controls.Add("Forms.TextBox.1", "FourthName")
txt.Top = 50
txt.Left = 150
Set clsEventClass = New CEventClass
Set clsEventClass.tbxEvent = txt
mEventTexts.Add clsEventClass
End Sub
Now when you run the form, those two events will fire if you click/change the control.
You may note that there is no AfterUpdate event for the textbox. That event is not actually a textbox event, but an event of the control container for the textbox, so you can't expose it this way. That's one reason I prefer to create all the controls at design time and hide or unhide them as needed. I still might use WithEvents for some controls just so I don't have to repeat code so much. But for things like TextBox_AfterUpdate, I just create all the event procedures at design time.
Update:
If you want the event to create new buttons, you have to do a couple more things. First, you have to expose the collection outside of the userform. You add this to your Userform module
Public Property Get EventButtons() As Collection
Set EventButtons = mEventButtons
End Property
Then you change your commandbutton event code to create a new button
Private Sub cmdEvent_Click()
Dim cmd As MSForms.CommandButton
Dim clsEventClass As CEventClass
Set cmd = cmdEvent.Parent.Controls.Add("Forms.CommandButton.1", cmdEvent.Caption & "1")
cmd.Top = cmdEvent.Top + 40
cmd.Left = cmdEvent.Left
cmd.Caption = cmdEvent.Caption & "1"
Set clsEventClass = New CEventClass
Set clsEventClass.cmdEvent = cmd
cmdEvent.Parent.EventButtons.Add clsEventClass
End Sub
This creates a new button 40 points below whichever was clicked. You don't say what your logic is for naming or positioning, so I assume you can work that out. Use cmdEvent.Parent to get a reference to the Userform.

Reading Userform Object Values

I created a Userform (manually in the VBA Projectbrowser). I have written VBA code, which fills this Userform with different Objects in runtime (Labels, Optionbuttons etc.). So far everything worked fine
The Userform is filled with data read from my Excel sheets and correctly displayed. However I'm not able to read the inputs from the objects on it (for example Optionbutton - TRUE or FALSE). These objects do not appear anywhere (except on the userform) so that I can link them and use them in another Module.
I guess they are only displayed and not really read into the memory or whatever (initialized !?).
There are two ways to go about it.
WAY 1
Declare your option button object as Public.
Module Code
Public theOpBut As Object
Sub Fill()
If theOpBut.Value = True Then
ActiveSheet.Cells(1, 5) = 1
Else
ActiveSheet.Cells(1, 5) = "NO"
End If
End Sub
Userform Code
Private Sub UserForm_Initialize()
Set theOpBut = UserForm1.Controls.Add("Forms.optionbutton.1", "OptionButton", True)
With theOpBut
.Caption = "Test Button"
'.GroupName = OpButGroupCounter
.Top = 10
.Left = 20
.Height = 16
.Width = 50
.Font.Size = 12
.Font.Name = "Ariel"
End With
End Sub
Private Sub CommandButton1_Click()
Call Fill
End Sub
WAY 2
Declare a Boolean Variable and create a click event of the Option button and then set the value of the Boolean Variable in that click event. To create the click event of the Option button at Run Time, see THIS EXAMPLE
You can then check the value of Boolean Variable in Sub Fill() and act accordingly.

Assign event handlers to controls on user form created dynamically in VBA

I have found many resources on the internet that do almost what i want to do, but not quite.I have a named range "daylist". For each day in the dayList, i want to create a button on a user form that will run the macro for that day. I am able to add the buttons dynamically but dont know how to pass the daycell.text from the named range, to the button, to the event handler, to the macro :S Heres the code i have to create the user form:
Sub addLabel()
ReadingsLauncher.Show vbModeless
Dim theLabel As Object
Dim labelCounter As Long
Dim daycell As Range
Dim btn As CommandButton
Dim btnCaption As String
For Each daycell In Range("daylist")
btnCaption = daycell.Text
Set theLabel = ReadingsLauncher.Controls.Add("Forms.Label.1", btnCaption, True)
With theLabel
.Caption = btnCaption
.Left = 10
.Width = 50
.Top = 20 * labelCounter
End With
Set btn = ReadingsLauncher.Controls.Add("Forms.CommandButton.1", "runButton", True)
With btn
.Caption = "Run Macro for " & btnCaption
.Left = 80
.Width = 80
.Top = 20 * labelCounter
' .OnAction = "btnPressed"
End With
labelCounter = labelCounter + 1
Next daycell
End Sub
To get around the above issue i currently prompt the user to type the day they want to run (e.g. Day1) and pass this to the macro and it works:
Sub B45runJoinTransactionAndFMMS()
loadDayNumber = InputBox("Please type the day you would like to load:", Title:="Enter Day", Default:="Day1")
Call JoinTransactionAndFMMS(loadDayNumber)
End Sub
Sub JoinTransactionAndFMMS(loadDayNumber As String)
xDayNumber = loadDayNumber
Sheets(xDayNumber).Activate
-Do stuff
End Sub
So for each of my runButtons, it needs to display daycell.text, and run a macro that uses that same text as a parameter to select the worksheet to do its stuff on.
Any help would be awesome. Ive seen responses that dynamically writes the vba code, to handle the macros, but i believe there must be someway it can be done a little more elegantly through passing parameters, just not sure how. Many thanks in advance!
I know you have accepted a solution now that will work for you and is much simpler than the below, but if you're interested, this would be the more direct answer to your question.
You need to create a class to handle the button clicks, so every time the button is clicked it uses the event in the class, you only need to do this once then create a new instance of it for every button. To stop these classes going out of scope and being lost, they need storing in a class level declaration. In the below I've moved your code around a little.
In the class module (I've called it cButtonHandler)
Public WithEvents btn As MSForms.CommandButton
Private Sub btn_Click()
MsgBox btn.Caption
End Sub
With events is used as it allows you to use most of the events for the control. I've moved the button generation code into the userform as below:
Dim collBtns As Collection
Private Sub UserForm_Initialize()
Dim theLabel As Object
Dim labelCounter As Long
Dim daycell As Range
Dim btn As CommandButton
Dim btnCaption As String
'Create a variable of our events class
Dim btnH As cButtonHandler
'Create a new collection to hold the classes
Set collBtns = New Collection
For Each daycell In Range("daylist")
btnCaption = daycell.Text
Set theLabel = ReadingsLauncher.Controls.Add("Forms.Label.1", btnCaption, True)
With theLabel
.Caption = btnCaption
.Left = 10
.Width = 50
.Top = 20 * labelCounter
End With
Set btn = ReadingsLauncher.Controls.Add("Forms.CommandButton.1", "runButton", True)
With btn
.Caption = "Run Macro for " & btnCaption
.Left = 80
.Width = 80
.Top = 20 * labelCounter
'Create a new instance of our events class
Set btnH = New cButtonHandler
'Set the button we have created as the button in the class
Set btnH.btn = btn
'Add the class to the collection so it is not lost
'when this procedure finishes
collBtns.Add btnH
End With
labelCounter = labelCounter + 1
Next daycell
End Sub
Then we can call the useform from a separate routine:
Sub addLabel()
ReadingsLauncher.Show vbModeless
End Sub
Classes in VBA aren't particularly well covered in many VBA books (generally you need to read VB6 books to get an understanding), however once you understand them and how they work, they become incredibly useful :)
Hope this helps
EDIT - to address additional queries
To refer to objects in a collection, this is either done through the key or the index. To use the key, you need to add it as you add the item to the collection, so:
collBtns.Add btnH
Would become
collBtns.Add btnH, btnCaption
For this reason, keys must be unique. You can then refer as follows:
'We refer to objects in a collection via the collection's key
'Or by it's place in the collection
'So either:
MsgBox collBtns("Monday").btn.Caption
'or:
MsgBox collBtns(1).btn.Caption
'We can then access it's properties and methods
'N.B you won't get any intellisense
collBtns("Monday").btn.Enabled = False
You can also add additional properties/method to your class if required, so for example:
Public WithEvents btn As MSForms.CommandButton
Private Sub btn_Click()
MsgBox btn.Caption
End Sub
Public Property Let Enabled(value As Boolean)
btn.Enabled = value
End Property
Would then be accessed:
collBtns("Monday").Enabled = False
Does this help? For further reading I would point you towards Chip Pearson's site, he has great stuff on most topics http://www.cpearson.com/excel/Events.aspx
Just remember that VBA is based on VB6 so is not a fully fledged OO language, for example, it does not support inheritance in the normal sense, only interface inheritance
Hope this helps :)
Example of catching click on worksheet. Put this in the worksheet module:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
' e.g., range(A1:E1) is clicked
If Not Application.Intersect(Target, Range("A1:E1")) Is Nothing Then
MsgBox "You clicked " & Target.Address
End If
End Sub