Let's see if anyone knows how to solve this problem:
I have a form with several elements: Some of them are textboxes called A1, A2, A3, A4...
Now, their AfterUpdate SubProcedure is extremely long but barely similar for each of them: A1_AfterUpdate, A2_AfterUpdate, A3_AfterUpdate...etc... are very similar but for the names of the textboxes they change.
My idea was to gather all that was equal in a subprocedure defined this way:
Private Sub Update(Box As String, Menu As Boolean)
If Menu=True{
Me!Box.Text = "This is the text that is going to change"
}
End Sub
So, the only thing I must do is to call it this way, for instance:
Update(A1, True)
But it doesn't seems to work. Any idea on how to reach this objective?
Add a class module - I've called it clsTextBoxEvents.
Add this code to the class:
Public WithEvents txt As Access.TextBox
Private Sub txt_AfterUpdate()
MsgBox txt.Name & " has been updated."
End Sub
In your form module add this code:
Public MyTextBoxes As New Collection
Private Sub Form_Open(Cancel As Integer)
Dim ctl As Control
Dim txtBoxEvent As clsTextBoxEvents
For Each ctl In Me.Controls
If TypeName(ctl) = "TextBox" Then
Set txtBoxEvent = New clsTextBoxEvents
Set txtBoxEvent.txt = ctl
txtBoxEvent.txt.AfterUpdate = "[Event Procedure]"
MyTextBoxes.Add txtBoxEvent
End If
Next ctl
End Sub
The MyTextBoxes declaration must be at the very top of the module.
This just adds the AfterUpdate event to all textboxes on the form. You'll probably want to refine that a bit to textboxes with specific text in the name, or controls that are in a specific frame on the form.
If you use a function instead of a sub:
Private Function UpdateCtl(Menu As Boolean)
If Menu Then
activecontrol = "This is the text that is going to change"
End If
End Sub
then you can call it directly from the control's AfterUpdate property: =UpdateCtl(True).
Simple and fast
I'm trying to call a change event for any Combo Box on a userform.
UPDATE
So following what others have said (For Text Boxes, which I've tested and works for Textboxes), I've create a Class Module Called: 'clsCombo_Update'
Private WithEvents MyComboBox As MSForms.comboBox
Public Property Set Control(cb As MSForms.comboBox)
Set MyComboBox = cb
End Property
Private Sub MyComboBox_Change()
Debug.Print "Change"
End Sub
And in the UserForm I have:
Private Sub UserForm_Initialize()
Dim ctrl As MSForms.Control
Dim obj As clsCombo_Update
Set cbCollection = New Collection
For Each ctrl In Me.Controls
If TypeOf ctrl Is MSForms.comboBox Then
Set obj = New clsCombo_Update
Set obj.Control = ctrl
cbCollection.Add obj
End If
Next ctrl
Set obj = Nothing
End Sub
This setup worked for textboxes but doesn't when I tried to make it work for Combo Boxes
I have a userform which assembles itself at runtime, by looking in a folder and extracting all the pictures from it into image-controls on my form. What makes the process a little more complex is that I'm also using the image-controls' events to run some code.
As a simplified example - I have a form which creates a picture at runtime, the picture has an on-click event to clear its contents. To do this I have a custom class to represent the image object
In a blank userform called "imgForm"
Dim oneImg As New clsImg 'our custom class
Private Sub UserForm_Initialize()
Set oneImg.myPic = Me.Controls.Add("Forms.Image.1") 'set some property of the class
oneImg.Init 'run some setup macro of the class
End Sub
In a class module called "clsImg"
Public WithEvents myPic As MSForms.Image
Public Sub Init() 'can't put in Class_Initialise as it is called before the set statement - so myPic is still empty at that point
myPic.Picture = LoadPicture(path/image)
End Sub
Public Sub myPic_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
onePic.Picture = Nothing
End Sub
The problem is, this doesn't display the changes, and I realised I needed a imgForm.Repaint in there somewhere - the question is, where?
Attempts
First option is to put it in the Init() sub of clsImg. (ie. have a line imgForm.Repaint at the end of the click event) That works, but not ideal as the class can then only be used with the userform of the correct name.
A 2nd idea was to pass the userform as an argument to Init()
Public Sub Init(uf As UserForm) 'can't put in Class_Initialise as it is called before the set statement - so myPic is still empty at that point
myPic.Picture = LoadPicture(path/image)
uf.Repaint
End Sub
And called with
oneImg.Init Me
That works too, but would mean that wherever I require a repaint, I would have to pass the parameter which is also not ideal - the code is in reality a lot more complex than is shown here, so I don't want to have to add in this extra parameter unless necessary
The third option which I'm currently using is to pass the userform object to the class and save it there.
So with a Public myForm As UserForm at the top of my class module, I can pass the userform with the Init(uf As UserForm) and have a
Set myForm = uf 'Works with a private "myForm"/ class Property
Or I can set it directly from the userform code with a
Set clsImg.myForm = Me 'only if "myForm" is Public
But what does this do for memory - does saving the userform as a variable in my class take up a lot of memory? Bear in mind that in my real code I declare an array of clsImgs that can be of the order of >100 instances so I don't really want to be making copies of the UF in each class if that's what this method does. Also, it's ugly
What I really want...
... is a way of telling the userform that it needs to repaint, rather than directly repainting from within the class. To me this says I need an event to occur in my class, which the userform hears with some custom event handler. Exactly how Worksheet_Change works, the sheet object raises a change event, the sheet class code handles it.
Is such a thing possible (I suppose I would have to declare clsImg WithEvents - can you do that for an array?), or is there a better alternative. I'm looking for a method which does not impede performance with a large number of classes declared, as well as one which is portable and easily readable. This is my first use of Classes so I may be missing something really obvious!
Since good practice is that classes are self-contained (as you obviously know) the clsImg should indeed not have to be aware of the UserForm and thus shouldn't tell the UserForm to repaint.
What this calls for, is indeed that the clsImg raises an event that the UserForm hooks into, so it repaints based on that event, or, in your own words: "a way of telling the userform that it needs to repaint."
I replicated your Custom Class (clsImg) as follows (wanted to use a proper Setter / Getter, functionality doesn't really change)
clsImg Code:
Private WithEvents myPic As MSForms.Image 'Because we need the click event.
Public Event NeedToRepaint() 'Because we need to raise an event that the UserForm can hook into.
Public Property Let picture(value As MSForms.Image)
Set myPic = value
End Property
Public Property Get picture() As MSForms.Image
Set picture = myPic
End Property
Public Sub myPic_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
myPic.picture = Nothing
RaiseEvent NeedToRepaint
End Sub
Next, in the UserForm we hook into this NeedToRepaint Event that's raised during the Event Handler of the MouseDown of the picture.
UserForm1 Code:
Private WithEvents oneImg As clsImg 'Our custom class
Private Sub oneImg_NeedToRepaint() 'Handling the event of our custom class
Me.Repaint
End Sub
Private Sub UserForm_Initialize()
Dim tmpCtrl As MSForms.Image
Set oneImg = New clsImg
Set tmpCtrl = Me.Controls.Add("Forms.Image.1")
tmpCtrl.picture = LoadPicture("C:\Path\image.jpg")
oneImg.picture = tmpCtrl
End Sub
The second part of your question is whether you can use this in an array.
The short answer is "no" - Each object would have to have it's own Event Handler. However, there are ways to work around this limitation by using a Collection or some similar approach. Still, this wrapper will have to be "UserForm aware" since that's where you'll be repainting. The approach would be something like in this article
EDIT: A solution / workaround for not being able to use an Array:
Since I really liked this question - Here's another approach.
We can apply somewhat of a PubSub pattern as follows:
I did a quick build for CommandButtons, but no reason that it can not be made for other classes of course.
Publisher class:
Public Event ButtonClicked(value As cButton)
Public Sub RegisterButtonClickEvent(value As cButton)
RaiseEvent ButtonClicked(value)
End Sub
'Add any other events + RegisterSubs.
In a regular class, I setup a factory routine to keep this specific Publisher a singleton (as in: It will always be the very same in memory object that you're pointing at):
Private pub As Publisher
Public Function GetPublisher() As Publisher
If pub Is Nothing Then
Set pub = New Publisher
End If
Set GetPublisher = pub
End Function
Next, we have the UserForm (I just made one with 4 buttons) and the button class to utilize this Publisher. The Userform will just subscribe to the event it raises:
Userform code:
Private WithEvents pPub As Publisher 'Use the Publishers events.
Private button() As cButton 'Custom button array
Private Sub pPub_ButtonClicked(value As cButton) 'Hook into Published event.
MsgBox value.button.Caption
End Sub
Private Sub UserForm_Initialize()
Set pPub = GetPublisher 'Private publisher for getting it's event. Will be always the same object as long as you use "GetPublisher"
Dim i As Integer
Dim btn As MSForms.CommandButton
'Create an array of the buttons:
i = -1
For Each btn In Me.Controls
i = i + 1
ReDim Preserve button(0 To i)
Set button(i) = New cButton
button(i).button = btn
Next btn
End Sub
Last we have the cButton class, that centralizes the button events (through the array). Instead of handling each event individually, we just tell the publisher that an Event has been raised.:
Private WithEvents btn As MSForms.CommandButton
Private pPub As Publisher
Public Event btnClicked()
Private Sub btn_Click()
pPub.RegisterButtonClickEvent Me 'Pass the events to the publisher.
End Sub
Public Property Let button(value As MSForms.CommandButton)
Set btn = value
End Property
Public Property Get button() As MSForms.CommandButton
Set button = btn
End Property
Private Sub Class_Initialize()
Set pPub = GetPublisher
End Sub
With this approach we have one "Publisher" that can handle any event from specific classes that register the right event with it. You could also add image events, workbook events, etc.
The publisher itself raises the events we need based on what gets passed to it.
This way the UserForm can be agnostic of the button class and vice versa.
Based on what is supported in VBA, I'm quite confident this is the cleanest approach for your scenario. If anyone has a better idea, I'd love to see another answer.
I did the following, If you pass the control as a control, you can use the parent.
In my form
Public c As Collection
Private Sub UserForm_Initialize()
Dim ctl As Control
Dim cls As clsCustomImage
Set c = New Collection
For Each ctl In Me.Controls
If TypeName(ctl) = "Image" Then
Set cls = New clsCustomImage
cls.init ctl
c.Add cls, CStr(c.Count)
End If
Next ctl
End Sub
and in my class, clsCustomImage
Private WithEvents i As MSForms.Image
Private frm As UserForm
public event evtRepaint
Public Sub init(c As control)
Set frm = c.parent
Set i = c
End Sub
Private Sub Class_Initialize()
End Sub
Private Sub Class_Terminate()
Set frm = Nothing
Set i = Nothing
End Sub
'
Private Sub i_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
i.Picture = Nothing
frm.Repaint
raiseevent evtRepaint
End Sub
EDIT
To have a single handler, you'd need to look at something along these lines, in a class called say clsHoldAndHandle
Private c As Collection
Private f As UserForm
Private WithEvents cls As clsCustomImage
Public Sub AddControl(ctl As Control)
Set cls = new clsCustomImage
If f Is Nothing Then Set f = ctl.Parent
cls.init ctl
c.Add cls, CStr(c.Count)
End Sub
Private Sub Class_Initialize()
Set c = New Collection
End Sub
Private Sub cls_evtRepaint()
f.Repaint
End Sub
I have a userform in the VBE with 12 textboxes (1 for each month of the year). There's an additional textbox that is supposed to display the sum of the values in each of the first 12 boxes, updating and recalculating itself as the values in the other textboxes change.
Some help in implementing a solution to this problem is much appreciated.
The code I'm trying to implement is taken from the post at:
VBA: Detect changes in any textbox of the userform
However I'm unsure what to put in the event section below of the class.
NB: This is my first attempt at using classes so I'm just learning.
If Class1 is the name of your class module, Userform1 the name of your userform and if you want the sum of all textbox values to be in TextBox13 then,
in your Class1 module insert
Private WithEvents txtbox As MSForms.TextBox
Dim ctlr As Control
Public sum As Integer
Public Property Set TextBox(ByVal t As MSForms.TextBox)
Set txtbox = t
End Property
Private Sub txtbox_Change()
sum = 0
For Each ctlr In UserForm1.Controls
sum = sum + Val(ctlr)
Next ctlr
UserForm1.TextBox13 = sum - Val(UserForm1.TextBox13)
End Sub
and in the UserForm1 module insert
Private myEventHandlers As Collection
Private Sub UserForm_Initialize()
Dim txtbox As Class1
Set myEventHandlers = New Collection
Dim c As Control
For Each c In Me.Controls
If TypeName(c) = "TextBox" Then
Set txtbox = New Class1
Set txtbox.TextBox = c
myEventHandlers.Add txtbox
End If
Next c
End Sub
I am trying to write some code for a VBA userform that has about 100 checkboxes. I was wondering if there is a way that I could have one piece of code that applies to any checkbox or if I have to write 100 seperate functions for checkbox1_click, checkbox2_click, checkbox3_click, etc.
Thanks for any help in advance :)
edit: I realized that it would help to explain exactly what I am trying to do. There will be 100 check boxes and whenever one is clicked I would like to do this:
Call CheckBoxClicked("checkboxname")
Put this in a class moduled named clsCheckBoxHandler
Public WithEvents chk As MSForms.CheckBox
Private Sub chk_Click()
MsgBox chk.Caption & " Clicked!"
End Sub
then in the Userform
Dim chkCollection As Collection
Private Sub UserForm_Initialize()
Dim cCont As Control
Dim chkH As clsCheckBoxHandler
Set chkCollection = New Collection
For Each cCont In Me.Controls
If TypeName(cCont) = "CheckBox" Then
Set chkH = New clsCheckBoxHandler
Set chkH.chk = cCont
chkCollection.Add chkH
End If
Next cCont
End Sub
this is just a simple handler for checkboxes that has a click event but can be extended to cover multiple controls and events.