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
Related
I have a custom created class btnClass based on CommandButton class.
Public WithEvents ButtonEvent As MsForms.CommandButton
Private Sub ButtonEvent_Click()
End sub
I have a UserForm1 that have one ListBox, one Label, and hundreds of dynamically created CommandButtons. I assigned btnClass to Buttons. When clicked on buttons, I want the Click event has the following result:
If number of buttons selected (selQty) less than the Label.Caption (totalQty), and this button hasn't been selected before, add value to listBox and change BackColor.
if this button selected previously, change color and decrease number of buttons selected (selQty) by 1.
I tried creating Public variables, but cannot get the result I want. Is this doable?
P.S. when the UserForm1 is activated, it means no button selected; as I click button I change the color of the button and accept it as selected.
Seems like you want to do something like this
clsButtonClick:
Option Explicit
Public WithEvents ButtonEvent As MSForms.CommandButton
Private Sub ButtonEvent_Click()
'pass the button to the procedure in the userform
ButtonEvent.Parent.HandleClick ButtonEvent
End Sub
Userform code:
Option Explicit
Const CLR_SEL As Long = vbRed 'selected color
Const CLR_NOT_SEL As Long = vbGreen 'unselected color
Dim btnCol As Collection
Dim maxQty As Long 'max number selectable
Dim currQty As Long 'number currently selected
'perform some setup
Private Sub UserForm_Activate()
Const NUM_BUTTONS As Long = 10
Dim i As Long, btn As MSForms.CommandButton
Dim o As clsButtonClick
currQty = 0 'number selected
maxQty = 5 'max selectable
Set btnCol = New Collection
'add some buttons
For i = 1 To NUM_BUTTONS
Set btn = Me.Controls.Add("Forms.CommandButton.1", "btn" & i, True)
btn.BackColor = CLR_NOT_SEL
btn.Height = 18
btn.Left = 20
btn.Top = 20 * i
btn.Caption = "Button " & i
Set o = New clsButtonClick
Set o.ButtonEvent = btn
btnCol.Add o
Next i
End Sub
'handle a button click event (button is passed in)
Sub HandleClick(btn As MSForms.CommandButton)
If btn.BackColor = CLR_SEL Then
btn.BackColor = CLR_NOT_SEL
currQty = currQty - 1
Else
If currQty = maxQty Then
MsgBox "no more selections available"
Else
btn.BackColor = CLR_SEL
currQty = currQty + 1
End If
End If
End Sub
Here's something to get you started.
Create a wrapper class which wraps each button and handles its click event. Then when the form loads, loop through the controls and wrap the buttons.
A module level collection is required to hold the references of wrapped buttons (wrapper classes).
The ButtonWrapper class:
Option Explicit
Private WithEvents objButton As MsForms.CommandButton
'Wrap button
Public Function WrapCommandButton(btn As MsForms.CommandButton) As ButtonWrapper
Set objButton = btn
Set WrapCommandButton = Me
End Function
'Button's event handler
Private Sub objButton_Click()
MsgBox objButton.Caption & " was clicked."
End Sub
'Clean up
Private Sub Class_Terminate()
Set objButton = Nothing
End Sub
The code behind the Form:
Option Explicit
Private m_handlers As Collection
'Initialize
Private Sub UserForm_Initialize()
Set m_handlers = New Collection
Dim ctl As Control
For Each ctl In Me.Controls
If TypeName(ctl) = "CommandButton" Then
With New ButtonWrapper
m_handlers.Add .WrapCommandButton(ctl)
End With
End If
Next ctl
End Sub
'Clean up
Private Sub UserForm_Terminate()
Set m_handlers = Nothing
End Sub
Hope this helps.
I am using this excellent tutorial as a base to create a simple "Hello World" Excel VBA project leveraging on Mathieu Guindon's concept of writing Object-Oriented Programming VBA code, discussed in a series of articles on the https://rubberduckvba.wordpress.com/ blog.
I have created a "bare bones" project without any Model containing an Excel worksheet (HelloSheet), a View, a ViewAdapter (including ViewCommands and ViewEvents interfaces) and a Controller. The VBA project compiles without errors but when I try to run the "application entry" macro I get the dreaded "Run-time error 438: Object doesn't support this property or method". This happens inside the Class_Initialize() sub of my View class where I have declared "Private WithEvents sheetUI As HelloSheet" and try to set "sheetUI = HelloSheet".
Here is an overview of my project tree, as seen in the RubberDuck VBIDE.
I have tried updating the VBA project references to exactly match those of the "Battleship" sample project. I also tried the two different approaches to implementing the Lazy Object / Weak Reference in the View class - the one in the "Battleship (WorksheetView).xlsm" linked in the original article vs the approach used in the latest version on GitHub, more specifically:
Private adapter As ***IWeakReference***
Private WithEvents sheetUI As HelloSheet
Private Sub Class_Initialize()
sheetUI = HelloSheet
End Sub
Private Property Get ViewEvents() As ISheetViewEvents
Set ViewEvents = adapter ***.Object***
End Property
VS
Private adapter As ***SheetViewAdapter***
Private WithEvents sheetUI As HelloSheet
Private Sub Class_Initialize()
sheetUI = HelloSheet
End Sub
Private Property Get ViewEvents() As ISheetViewEvents
Set ViewEvents = ***adapter***
End Property
..but the "Run-time error 438: Object doesn't support this property or method" persisted.
Below is all relevant code split in sheets, classes, interfaces etc.:
1) HelloSheet (regular Excel sheet code-behind):
'#Folder("HelloWorld.View.Worksheet")
Option Explicit
Public Event DoubleClick(ByVal clickedRow As Integer)
Public Sub HideShape(shapeName As String)
Dim currentShape As Shape
Set currentShape = Me.Shapes(shapeName)
currentShape.Visible = msoFalse
End Sub
Public Sub ShowShape(shapeName As String)
Dim currentShape As Shape
Set currentShape = Me.Shapes(shapeName)
currentShape.Visible = msoTrue
End Sub
Public Sub OnLaunchCommand()
ShowShape ("WarningTriangle")
End Sub
Public Sub TempManualHide()
HideShape ("WarningTriangle")
End Sub
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
Cancel = True
RaiseEvent DoubleClick(Target.Row)
End Sub
Public Sub PreviewSelectedRecord(ByVal selectedRow As Integer)
Me.Cells(1, 1).Value2 = "Row is " & CStr(selectedRow)
End Sub
2) SheetView class:
'#Folder("HelloWorld.View.Worksheet")
Option Explicit
Implements ISheetViewCommands
Private adapter As SheetViewAdapter ' IWeakReference
Private WithEvents sheetUI As HelloSheet
Private Sub Class_Initialize()
sheetUI = HelloSheet
End Sub
Private Property Get ViewEvents() As ISheetViewEvents
Set ViewEvents = adapter '.Object
End Property
':GameSheet event handlers
':Messages sent from the view
':***************************
Private Sub sheetUI_DoubleClick(ByVal clickedRow As Integer)
ViewEvents.PreviewSelectedRecord clickedRow
End Sub
':IGridViewCommands
':Messages sent from the controller
':*********************************
Private Property Set ISheetViewCommands_Events(ByVal value As ISheetViewEvents)
Set adapter = value ' WeakReference.Create(Value)
End Property
Private Property Get ISheetViewCommands_Events() As ISheetViewEvents
Set ISheetViewCommands_Events = adapter '.Object
End Property
Private Sub ISheetViewCommands_OnLaunchCommand()
sheetUI.OnLaunchCommand
End Sub
Private Sub ISheetViewCommands_OnPreviewSelectedRecord(ByVal selectedRow As Integer)
sheetUI.PreviewSelectedRecord selectedRow
End Sub
3) ISheetViewEvents interface:
'#Folder("HelloWorld.View")
'#Interface
Option Explicit
Public Sub PreviewSelectedRecord(ByVal selectedRow As Integer)
End Sub
4) ISheetViewCommands interface:
'#Folder("HelloWorld.View")
'#Interface
Option Explicit
'#Description("Gets/sets a weak refererence to the view events.")
Public Property Get Events() As ISheetViewEvents
End Property
Public Property Set Events(ByVal value As ISheetViewEvents)
End Property
Public Sub OnLaunchCommand()
End Sub
Public Sub OnPreviewSelectedRecord(ByVal selectedRow As Integer)
End Sub
5) SheetViewAdapter class (PredeclaredId / has default instance):
'#Folder("HelloWorld.View")
Option Explicit
'#PredeclaredId
Implements ISheetViewCommands
Implements ISheetViewEvents
Public Event OnPreviewCurrentSelectedRecord(ByVal selectedRow As Integer)
Private Type TAdapter
SheetViewCommands As ISheetViewCommands
End Type
Private this As TAdapter
Public Function Create(ByVal view As ISheetViewCommands) As SheetViewAdapter
With New SheetViewAdapter
Set .SheetViewCommands = view
Set view.Events = .Self
Set Create = .Self
End With
End Function
Public Property Get Self() As SheetViewAdapter
Set Self = Me
End Property
'#Description("Gets/sets a reference that exposes commands to send to the view.")
Public Property Get SheetViewCommands() As ISheetViewCommands
Set SheetViewCommands = this.SheetViewCommands
End Property
Public Property Set SheetViewCommands(ByVal value As ISheetViewCommands)
Set this.SheetViewCommands = value
End Property
':IGridViewEvents
':Messages sent from the view
':***************************
Private Sub ISheetViewEvents_PreviewSelectedRecord(ByVal selectedRow As Integer)
RaiseEvent OnPreviewCurrentSelectedRecord(selectedRow)
End Sub
':IGridViewCommands
':Messages sent from the controller
':*********************************
Private Property Set ISheetViewCommands_Events(ByVal value As ISheetViewEvents)
Err.Raise 5, TypeName(Me), "Invalid use of property"
End Property
Private Property Get ISheetViewCommands_Events() As ISheetViewEvents
Set ISheetViewCommands_Events = Me
End Property
Private Sub ISheetViewCommands_OnLaunchCommand()
this.SheetViewCommands.OnLaunchCommand
End Sub
Private Sub ISheetViewCommands_OnPreviewSelectedRecord(ByVal selectedRow As Integer)
this.SheetViewCommands.OnPreviewSelectedRecord selectedRow
End Sub
6) HelloController class:
'#Folder("HelloWorld")
Option Explicit
Private viewCommands As ISheetViewCommands
Private WithEvents viewAdapter As SheetViewAdapter
Public Sub Launch(ByVal adapter As SheetViewAdapter)
Set viewAdapter = adapter
Set viewCommands = adapter
viewCommands.OnLaunchCommand
End Sub
Private Sub viewAdapter_OnPreviewCurrentSelectedRecord(ByVal selectedRow As Integer)
viewCommands.OnPreviewSelectedRecord selectedRow
End Sub
7) And finally the "Macros" standard module which serves as an entry point. This is where I encounter the error (the "Set view = New SheetView" line):
'#Folder("HelloWorld")
'#Description("Application entry points.")
Option Explicit
'#Ignore MoveFieldCloserToUsage
Private controller As HelloController
Public Sub LaunchWorksheetInterface()
Dim view As SheetView
Set view = New SheetView
Set controller = New HelloController
controller.Launch SheetViewAdapter.Create(view)
End Sub
Supposing I could get around the entry-level error, I would expect a very simple functionality:
1) A hidden Excel shape is made visible on the HelloSheet (the OnLaunchCommand);
2) When double-clicking on a cell, the row it is located on would be reported in cell A1 of the same worksheet (the Worksheet_BeforeDoubleClick event).
Obviously this amount of code for such simple tasks is overkill - my idea is once I get these basics working to add Model classes to the project and map them to certain areas (i.e. Tables/ListObjects) inside the Workbook.
Any help will be greatly appreciated! And kudos to anyone who has made it to the end of this rather long post :)
Private WithEvents sheetUI As HelloSheet
Private Sub Class_Initialize()
sheetUI = HelloSheet
End Sub
sheetUI is an object reference, assigning it requires the Set keyword:
Private WithEvents sheetUI As HelloSheet
Private Sub Class_Initialize()
Set sheetUI = HelloSheet
End Sub
Error 438 is thrown whenever you try to access the default member of a Worksheet class, since Worksheet has no default member - this code reproduces the error from the immediate pane:
?Sheet1
Or:
foo = Sheet1
Rubberduck inspections should have warned about this, under "Code Quality Issues":
Object variable 'sheetUI' is assigned without the 'Set' keyword.
This question already has answers here:
VBA: Using WithEvents on UserForms
(2 answers)
Create event handlers for multiple dynamic controls
(2 answers)
Closed 4 years ago.
Untill now I have created one event for each control in my userform.
Private Sub TextBox_Integrate_Indexes_Change()
Call LabView.textBoxChange(TextBox_Integrate_Indexes)
End Sub
Private Sub TextBox_Integrate_InputFile_Change()
Call LabView.textBoxChange(Me.TextBox_Integrate_InputFile)
End Sub
Private Sub TextBox_Integrate_OutputFile_Change()
Call LabView.textBoxChange(Me.TextBox_Integrate_OutputFile)
End Sub
As seen these events all just send its object to my method which then handles the event(check if it has changed its value, and if so store the updated value in a config.json file)
However instead of making an event for all my userform textboxes, optionbuttons listboxs,checkboxes and comboxes, I wasa wandering if there is a way to detect if any event happens to that userform, get the item that triggered the event and if this one of the above type, then send itself to my method.
Yes, you can create a custom class that holds a private textbox variable.
You can then capture the event in that class and pass it to your Labview class.
In the UserForm you can just create a collection of the custom class, and set the userform's textboxes as the private variables in the custom class.
Example code:
cTextBox Class:
Private WithEvents p_TextBox As MSForms.TextBox
Public Property Let txtBox(value As MSForms.TextBox)
Set p_TextBox = value
End Property
Public Property Get txtBox() As MSForms.TextBox
Set txtBox = p_TextBox
End Property
Private Sub p_TextBox_Change()
Call Labview.textboxchange(p_TextBox)
End Sub
Labview class:
Public Sub textboxchange(val As MSForms.TextBox)
MsgBox val.Name
End Sub
Userform code:
Private t As MSForms.Control
Private ctb As cTextBox
Private cTextBoxes As Collection
Private Sub UserForm_Initialize()
Set cTextBoxes = New Collection
For Each t In Me.Controls
If TypeName(t) = "TextBox" Then
Set ctb = New cTextBox
ctb.txtBox = t
cTextBoxes.Add ctb
End If
Next t
End Sub
And a routine to test the whole thing:
Public Labview As New Labview
Sub test()
UserForm1.Show
End Sub
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
As a follow-up to this question, where a userform allows the creation of x # of tabs in a multipage during runtime, each with the same controls, I am wondering how to set the .OnClick behaviour of the buttons. I am trying to achieve this with the following code:
For Each ctl In MultiPage1.Pages(NumSegs - 1).Controls
If TypeOf ctl Is MSForms.CommandButton Then
ctl.Name = "Segment" & NumSegs & "Button"
ctl.OnClick = "Span_Form_Click_Handler"
End If
Next
But of course there doesn't seem to be a .OnClick method...
You define a variable with events.
A minimal example: UserForm + CommandButton on it. Code in UserForm Module:
Public WithEvents btn As MSForms.CommandButton
Private Sub btn_Click()
MsgBox "Bla"
End Sub
Private Sub UserForm_Click()
Set btn = CommandButton1
End Sub
After a click in the form, the button CommandButton gets the sub btn_Click assigned.
To be able to call everytime the same sub, you need to place the sub and the withevents variable into a class and create an instance of this class for each CommandButton you find.