Class for Custom Button Event Handling - excel

1) I have a Form with some buttons (its in Access, but I guess it applies for Excel as well).
2) I have a custom class that helps me debug that form (and future forms that I may add).
The class simply logs when form events fire, such as loaded, unloaded, dirty, exited.
I'd like that class to have the capability to log when buttons are clicked.
I know this can be done by using a standard module, and loading a public collection there. Or by directly using the form's events. Or by storing in a collection behind the form.
But I would like, if possible, to encapsulate it all in my debugging class. Then its a simple two lines added to the Form_Load event of each new form I add.
My simplified attempt below is only capturing the event for the last button that gets added in the class collection, ie. Button3.
TestButtons (A Form with Button1, Button2, & Button3)
Private Buttons As CButtons
Private Sub Form_Load()
Set Buttons = New CButtons
Buttons.LoadButtons Me
End Sub
CButtons (Class):
Public WithEvents btn As Access.CommandButton
Private AllButtons As Collection
Const MODE_DEBUG As Boolean = True
Public Sub LoadButtons(ByRef TheForm As Access.Form)
Dim ctl As Control
Set AllButtons = New Collection
For Each ctl In TheForm.Controls
If ctl.ControlType = acCommandButton Then
Set btn = ctl
btn.OnClick = "[Event Procedure]"
AllButtons.Add btn
End If
Next ctl
End Sub
Private Sub btn_Click()
If MODE_DEBUG Then debug.print btn.Name & "_Click"
End Sub
Wondering if anyone's got any advice, thanks!

You can't handle events from a collection. The easiest solution is to use a separate class to handle the button events, make a collection of those classes in your multiple buttons handler, and pass the button from the class handling the single button to the class handling multiple ones on an event.
Class CSingleButton
Public buttonsHandler As CButtons
Public WithEvents btn As Access.CommandButton
Private Sub btn_Click()
buttonsHandler.HandleClick btn
End Sub
Class CButtons
Private ButtonHandlers As Collection
Const MODE_DEBUG As Boolean = True
Public Sub LoadButtons(ByRef TheForm As Access.Form)
Dim ctl As Control
Dim btnHandler As CSingleButton
Set ButtonHandlers = New Collection
For Each ctl In TheForm.Controls
If ctl.ControlType = acCommandButton Then
Set btnHandler = New CSingleButton
Set btnHandler.btn = ctl
Set btnHandler.buttonsHandler = Me
ctl.OnClick = "[Event Procedure]"
ButtonHandlers.Add btnHandler
End If
Next ctl
End Sub
Public Sub HandleClick(btn As Access.CommandButton)
If MODE_DEBUG Then debug.print btn.Name & "_Click"
End Sub

Related

Store a class module as a variable in VBA

I wrote some code in a class module and userform_initialize_event. All are okay. But when I Dim the new class module and applies in the initialize event it says that variable not defined. Here is my code -
Dim Buttons() As New BtnClass
Private Sub UserForm_Initialize()
Dim ButtonCount As Integer
Dim ctl As Control
' Create the Button objects
ButtonCount = 0
For Each ctl In fmHover.Controls
If TypeName(ctl) = "Label" Then
ButtonCount = ButtonCount + 1
ReDim Preserve Buttons(1 To ButtonCount)
Set Buttons(ButtonCount).ButtonGroup = ctl
End If
Next ctl
End Sub
If I Dim the Buttons inside the event it does not throws any error and the code also don't works. I searched in a lot of place. Every one Dim the new class before the Initialize event. So, why my one doesn't work? Kindly suggest where is my mistake.
Here is my class module code -
Public WithEvents ButtonGroup As MSForms.Label
Private Sub ButtonGroup_Click()
Msg = "You clicked " & ButtonGroup.Name
MsgBox Msg
ButtonGroup.Name
End Sub
Thanks in advance.
Each label on your form should have it's own instance of the class. The form needs a way to remember each instance - stored in a collection at the form level.
Not sure what the ButtonCount is doing, so have removed it.
Your form code:
Option Explicit '!!!Add option explicit to the top of each module!!!
'It helps avoid so many errors that I don't think three
'exclamations marks is enough!!!!
'>Tools>Options>tick Require variable declaration
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'Each instance of the class is stored here.
Private colLabels As Collection
Private Sub UserForm_Initialize()
Dim BtnEvents As BtnClass
'Initialise the collection to hold the class instances.
Set colLabels = New Collection
Dim ctrl As Control
For Each ctrl In Me.fmHover.Controls
If TypeName(ctrl) = "Label" Then
Set BtnEvents = New BtnClass 'New instance of the class.
Set BtnEvents.AssignButton = ctrl 'Assign the label to it.
colLabels.Add BtnEvents 'Add it to the collection so it's remembered.
End If
Next ctrl
End Sub
Your class module will look like:
Option Explicit 'More exclamation marks!!!!!!!!!
Public WithEvents ButtonGroup As MSForms.Label
'Let the class know what control it's assigned to.
Public Property Set AssignButton(ctrl As Control)
Set ButtonGroup = ctrl
End Property
Private Sub ButtonGroup_Click()
Dim Msg As String
Msg = "You clicked " & ButtonGroup.Name
MsgBox Msg
End Sub

Ignore Changes on a Specific MultiPage Page

I have a MultiPage control on a userform. I also have a class that detects any changes in textboxes, checkboxes and comboboxes on the whole form. (Example, if I type or select something, the class detects it and makes a "Save" button visible).
On Page3 of the MultiPage control I have some textboxes. I do not want the class file to detect changes on this 'tab'. Is there a way to tell my class to ignore any changes on that page?
EDIT:
Essentially I don't want the below code to run while I'm working on the 3rd tab (page) of the MultiPage control (which resides in my userform)
Option Explicit
Private WithEvents MyTextBox As MSForms.TextBox
Private WithEvents MyComboBox As MSForms.ComboBox
Private WithEvents MyCheckBox As MSForms.CheckBox
Public Property Set ControlTB(tb As MSForms.TextBox)
Set MyTextBox = tb
End Property
Public Property Set ControlCB(cb As MSForms.ComboBox)
Set MyComboBox = cb
End Property
Public Property Set ControlCH(ch As MSForms.CheckBox)
Set MyCheckBox = ch
End Property
Private Sub MyTextBox_Change()
Call MainCode
End Sub
Private Sub MyComboBox_Change()
Call MainCode
End Sub
Private Sub MyCheckBox_Change()
Call MainCode
End Sub
Private Sub MainCode()
f_Main.btnUpdateProjectData.Enabled = True
f_Main.btnUpdateProjectData.Visible = True
f_Main.Is_Saved = False
End Sub

Adding item in combobox when combobox is created on click event

I am trying to add items in comboBox. I know how to do this, with myBox.AddItem "words" However I have an issue to implement this to my code as I create the comboBox on click event :
Private Sub CommandButton2_Click()
Dim editBox As MSForms.Control
Dim testBox As ComboBox
Static i
Set editBox = Me.Controls.Add("Forms.ComboBox.1")
i = i + 1
With editBox
.Name = "cmBox" & i
.Top = i * editBox.Height + 10
.Left = 130
End With
End Sub
Do you have any suggestions ?
Thank you
Assuming the items are known at the time you're creating the controls, ...you can do that in the handler directly. Just set testBox to your editBox reference:
Set testBox = editBox 'basically cast from MSForms.Control to MSForms.ComboBox
testBox.Add "test1"
testBox.Add "test2"
On the other hand, if the items are not known at the time you're creating the controls, you have a problem:
Dim testBox As ComboBox
The object is locally-scoped, and goes out of scope as soon as the click handler exits.
You need to hold on to it. Move that declaration to module-level (and qualify it, for consistency):
Private testBox As MSForms.ComboBox
Then you can invoke testBox.Add.... the problem is that you're going to be adding more than one control, so you can't just have one field like this. Have a Collection instead:
Private dynamicControls As Collection
Private Sub UserForm_Initialize()
Set dynamicControls = New Collection
End Sub
Now when you create a dynamic control, add it to the collection with a key:
Set editBox = Me.Controls.Add("Forms.ComboBox.1")
'...
dynamicControls.Add editBox, editBox.Name
If you need to handle events for these dynamic controls, you'll need a different setup though, with an instance of a custom class for each dynamic control:
'Class1
Option Explicit
Private WithEvents box As MSForms.ComboBox
Private Sub box_Change()
'...
End Sub
Public Property Get Control() As MSForms.ComboBox
Set Control = box
End Property
Public Property Set Control(ByVal value As MSForms.ComboBox)
Set box = value
End Property
Then when you create a dynamic control, you add it to a new instance of that class instead:
Set editBox = Me.Controls.Add("Forms.ComboBox.1")
'...
Dim dynamicHandler As Class1
Set dynamicHandler = New Class1 'todo rename that class
Set dynamicHandler.Control = editBox
dynamicControls.Add dynamicHandler, editBox.Name
Now when you're ready to add items to a given box, retrieve the control from the dynamicControls collection:
With dynamicControls("cmBox1")
.Add "test1"
.Add "test2"
End With

Click Event for buttons inside Frames for VBA in Excel

In Excel I insert an ActiveX Frame into a worksheet. Right clicking this frame allows me to select:
Frame Object>Edit
Now I am able to add a button to this frame. Great.
How do I add a _Click event to this button so that it will run a macro?
Basically, what you need to do is to create you own class, for instance, "XButton". Inside this 'XButton' there will be an event handler for the button object that is inside the frame.
So you can handle all of the events that are sent by 'btn' and forward it further. Then you will have to create a custom interface (empty class) IXButtonEventHandler, that will look something like this:
Option Explicit
Public Sub Click(Sender as XButton)
End Sub
So, your custom class XButton will look like this:
Private WithEvents btn as MSForms.CommandButton
Private mEventHandler as IXButtonEventHandler
Public Sub CreateObject(EventHandlerOf as MSForms.CommandButton, EventHandler as IXButtonEventHandler)
Set btn = EventHandlerOf
Set mEventHandler = EventHandler
End Sub
Private Sub btn_Click()
If not mEventHandler is Nothing then mEventHandler.Click(Me)
End Sub
Let's say, your Workbook will be the event handler and will need to implement the IXButtonEventHandler interface, for instance:
Implements IXButtonEventHandler
Private Sub IXButtonEventHandler_Click(Sender as XButton)
'your code
End Sub
On Workbook_Load or whatnot you will need to create a collection of XButtons and attach them to your frame controls:
Dim xbtn as Collection
Private Sub AttachButtons()
Set xbtn = New Collection
Dim i as Long
For i = 0 to 3
Dim xb as New XButton
xb.CreateObject <YourFrame>.Controls("CommandButton" & Cstr(i)), Me
xbtn.Add xb
Next i
End Sub

Put code inside a loop to exit the loop after any mouse click

On a Userform, I'm blinking a frame Off/On by toggling its visiblity. It blinks a variable number of times and then stops, but in between blinks it checks for user activity. If there has been a mouse click anywhere on the form or on any of the contained controls then the blinking stops immediately.
This is what my blinker looks like.
For i = 1 To numberOfBlinks
<blink twice>
DoEvents
If <click detected> Then Exit Sub
Next i
Everything works fine except for the <click detected> part. How do I do that from inside the loop?
Did you tried to change a global boolean variable on the mouseclick event to true (default false)?
Then try to check if this global boolean variable is true in <click detected>.
This seems to work ok, but it looks like a lot of code just to detect a mouse click. For instance, I thought it should be possible to create a Class that contains all the Form Controls, so I could detect a click on any of them in one go, without having to check on each kind of control separately. I couldn't make that work and I'm hoping somebody can improve on this.
Just to restate what this does: On a Userform, a large frame named mapFrame holds any number of other frames and labels, and all those contained frames can hold any number of other frames and labels, but that's as deep as the nesting goes. I want to start a loop, (in this case the loop blinks a control off and on, but it could be any other loop) and wait for the user to click on any of the contained Frames or Labels to signal an exit from the loop. I also want to get the name of the control that was clicked.
I took the suggestion by therealmarv and used the click to set a public Boolean which gets tested inside the loop.
In a new Class Module:
Option Explicit
Public WithEvents classLabels As msForms.Label
Private Sub classLabels_Click()
clickedControlName = "" '<== Public String
With classLabels
If .Parent.Name = "mapFrame" Or _
.Parent.Parent.Name = "mapFrame" Then
isClickDetected = True '<== Public Boolean
clickedControlName = .Name
End If
End With
End Sub
In another new Class Module:
Option Explicit
Public WithEvents classFrames As msForms.Frame
Private Sub classFrames_Click()
clickedControlName = "" '<== Public String
With classFrames
If .Name = "mapFrame" Or _
.Parent.Name = "mapFrame" Or _
.Parent.Parent.Name = "mapFrame" Then
isClickDetected = True '<== Public Boolean
clickedControlName = .Name
End If
End With
End Sub
In a Form Module:
Option Explicit
Dim frames() As New clsFrames
Dim labels() As New clsLabels
Private Sub createFrameListeners()
Dim ctl As msForms.Control
Dim frameCount as Long
For Each ctl In Me.Controls
' Debug.Print TypeName(ctl): Stop
If TypeName(ctl) = "Frame" Then
frameCount = frameCount + 1
ReDim Preserve frames(1 To frameCount)
'Create the Frame Listener objects
Set frames(frameCount).classFrames = ctl
End If
Next ctl
End Sub
Private Sub createLabelListeners()
Dim ctl As msForms.Control
Dim LabelCount as Long
For Each ctl In Me.Controls
' Debug.Print TypeName(ctl): Stop
If TypeName(ctl) = "Label" Then
LabelCount = LabelCount + 1
ReDim Preserve labels(1 To LabelCount)
'Create the Label Listener objects
Set labels(LabelCount).classLabels = ctl
End If
Next ctl
End Sub
Function blinkThisControl(ctrl As Control, ByVal blinkCount As Long)
isClickDetected = False
Dim i As Integer
For i = 1 To blinkCount
' <blink ctrl twice>
DoEvents
If isClickDetected Then Exit Function
'name of clicked control will be in clickedControlName
Next i
End Function
Private Sub userform_initialize()
Call createFrameListeners
Call createLabelListeners
' do other stuff
End Sub

Resources