Reiterating Sub TextBox1_Change() efficiently - vba

Sub TextBox19_Change()
If Len(TextBox19.Value) = 4 Then TextBox19.Value = Mid(TextBox19.Value, 1, 3)
End Sub
Sub TextBox18_Change()
If Len(TextBox18.Value) = 4 Then TextBox18.Value = Mid(TextBox18.Value, 1, 3)
End Sub
Sub TextBox17_Change()
If Len(TextBox17.Value) = 4 Then TextBox17.Value = Mid(TextBox17.Value, 1, 3)
End Sub
Sub TextBox16_Change()
If Len(TextBox16.Value) = 4 Then TextBox16.Value = Mid(TextBox16.Value, 1, 3)
End Sub
How can I rephrase the above so that we don't have to duplicate the sub-routines for 100+ TextBoxes? There are more codes than just changing the textbox's value to its first 3 characters. I would appreciate a general efficient code. Thank you.
Edit: This isn't on a form. This is on PowerPoint Slides.

I wasn't sure if you're using form textboxes here, but you could also use event sinking.
So create a class clsCustomText like so
Private WithEvents t As msforms.TextBox
Private Const lMaxLength As Long = 3
Public Sub Init(tIn As msforms.TextBox)
Set t = tIn
End Sub
Private Sub t_Change()
If Len(t.Value) > lMaxLength Then t.Value = Left(t.Value, lMaxLength)
End Sub
Then in a normal module, somewhere to hold them
Public colCustomTextboxes As Collection
and then in the form like so, i did mine on click, so i can test. You'd need to move to initialize.
Private Sub UserForm_Click()
Dim c As Control
Dim t As clsCustomText
Set colCustomTextboxes = New Collection
For Each c In Me.Controls
If TypeOf c Is msforms.TextBox Then
Set t = New clsCustomText
t.Init c
colCustomTextboxes.Add t, c.Name
End If
Next c
End Sub

Related

Hiding/Revealing/Adding/Removing parts of a document with VBA

I want to make a form where you can select which parts of a document you need for example:
You have 4 Boxes - Textbox1, Textbox2, Textbox3, Textbox4
Via a Checkboxlisti in the Userform you can select you only want Textbox1 + 4 and the document adds/removes/hides/shows only the parts you want to so in this example it would only show Textbox1 and Textbox 4 - I am not sure if this works with showing/hiding/adding/removing/ maybe from an external file?
Here is a picture how I would imagine it to look. (In the Picture example It would remove Textbox3 because it is not selected.
Does anyone have an solution? Or maybe how to approach this problem... Thanks in advance.
This works fine using numerically added textboxes
Private Sub CheckBox1_Click()
ActiveDocument.Shapes(1).Visible = CheckBox1
End Sub
Private Sub CheckBox2_Click()
ActiveDocument.Shapes(2).Visible = CheckBox2
End Sub
Private Sub CheckBox3_Click()
ActiveDocument.Shapes(3).Visible = CheckBox3
End Sub
Private Sub CheckBox4_Click()
ActiveDocument.Shapes(4).Visible = CheckBox4
End Sub
EDIT - using named textboxes for easier readability
If you copy/paste your textboxes Word will create them all with the same names. If you want to create readible code and insure your textboxes are named according to the order you created them you can preload the names.
For Each shp In ActiveDocument.Shapes
If shp.Type = msoTextBox Then
i = i + 1
shp.Name = "TextBox" & i
End If
Next shp
Private Sub CheckBox1_Click()
ActiveDocument.Shapes("TextBox1").Visible = CheckBox1
End Sub
Private Sub CheckBox2_Click()
ActiveDocument.Shapes("TextBox2").Visible = CheckBox2
End Sub
Private Sub CheckBox3_Click()
ActiveDocument.Shapes("TextBox3").Visible = CheckBox3
End Sub
Private Sub CheckBox4_Click()
ActiveDocument.Shapes("TextBox4").Visible = CheckBox4
End Sub
Example of output

Code to account for all checkboxes in a userform?

I have code on a userform that contains several checkboxes and several DTPickers.
The code looks like so:
Private Sub CheckBox11_Click()
If CheckBox11.Value = True Then
DTPicker22.Enabled = True
Else
DTPicker22.Enabled = False
End If
End Sub
Private Sub CheckBox12_Click()
If CheckBox12.Value = True Then
DTPicker24.Enabled = True
Else
DTPicker24.Enabled = False
End If
End Sub
The Userform contains a lot of checkboxes that have clauses next to them. Upon their completion the DTPicker will enable entering the date of completion.
Whilst this does what I want, it only enables one DTPicker when the checkbox is ticked per private sub. There has to be some way to make this so I wouldn't need to create different private subs for every checkbox click event.
Could you also tell me where to put it, as in, what event?
A "control array" is the typical approach for something like this.
See:
http://www.siddharthrout.com/index.php/2018/01/15/vba-control-arrays/
eg:
Class module clsEvents
Option Explicit
'Handle events for a checkbox and a date control, associated with a worksheet cell
Private WithEvents m_CB As MSForms.CheckBox
Private WithEvents m_DP As DTPicker
Private m_dateCell As Range
'set up the controls and the cell
Public Sub Init(cb As MSForms.CheckBox, dp As DTPicker, rng As Range)
Set m_CB = cb
Set m_DP = dp
Set m_dateCell = rng
If rng.Value > 0 Then
cb.Value = True
m_DP.Value = rng.Value
Else
cb.Value = False
End If
m_DP.CustomFormat = "dd/MM/yyyy"
End Sub
Private Sub m_CB_Change()
m_DP.Enabled = (m_CB.Value = True)
End Sub
Private Sub m_DP_Change()
m_dateCell.Value = m_DP.Value 'update the cell
End Sub
Userform:
Option Explicit
Dim colObj As Collection 'needs to be a Global to stay in scope
Private Sub UserForm_Activate()
Dim obj As clsEvents, i As Long, ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Set colObj = New Collection
'loop over controls and create a class object for each set
' 3 pairs of controls on my test form...
For i = 1 To 3
Set obj = New clsEvents
obj.Init Me.Controls("CheckBox" & i), _
Me.Controls("DTPicker" & i), _
ws.Cells(i, "B")
colObj.Add obj
Next i
End Sub
The first thing I'd recommend is following a proper naming convention. "CheckBox11" and "DTPciker1" are really vague and once you get further into your code, you'll forget which control is which. I would recommend naming them something that relates the two control together, like "firstDate" and "firstDateDTP". My alternate answer below uses this approach.
You could make a public function that enables the DTPicker based upon the checkbox's value.
Public Function EnableDTPicker(myPicker as String, enableBool as Boolean)
UserFormName.Controls(myPicker).Enabled = enableBool
End Function
Then, you can call the function in your CheckBox123_Click() subs like this:
Private Sub CheckBox123_Click()
EnableDTPicker("thePickerName", CheckBox123.Value)
End Sub
Alternatively, you could make a timer event that runs x number of seconds that just loops through the controls and performs the checks as needed. See this page on how to set up the timer. Using the code in the link shown, You could do something along the lines of:
'Put this in Workbook events
Private Sub Workbook_Open()
alertTime = Now + TimeValue("00:00:01")
Application.OnTime alertTime, "EventMacro"
UserForm1.Show
End Sub
'Put this in a Module
Public Sub EventMacro()
With UserForm1
For each ctrl in .Controls
If TypeName(ctrl) = "CheckBox" Then
'The code below assumes the naming convention outlined above is followed
.Controls(ctrl.Name & "DTP").Enabled = ctrl.Value
End If
Next ctrl
End With
alertTime = Now + TimeValue("00:00:01")
Application.OnTime alertTime, "EventMacro"
End Sub

Userform closes after "End Sub" without ever calling "Unload Me"

I have a userform (baseUF) that has multiple pages and buttons that all do different things. I have this baseUF being modeless because I want the user to be able to play with the sheet without closing the userform and losing all of the data they input. However, I started having a problem that might be due to the modeless nature of the baseUF.
There are other userforms that can be called from the baseUF. One executes with no issue by double clicking a textbox. However, the other userform is loaded after a button click. Once that button click sub is finished, the baseUF closes after the Exit Sub OR End Sub line. I don't remember this happening in the past and it doesn't happen with any other button click subs.
Does anybody have an idea what the issue could be? I'm pretty lost because I don't have a command to close the baseUF anywhere in that sub. Below is some code to show what is happening:
This sub is connected to a button on the spreadsheet to open the baseUF (the code is in a module).
Sub Button1_Click()
' show the userform
baseUF.Show vbModeless
End Sub
And this is the sub in the baseUF that calls an additional userform (LoadBox) which seems to be the issue.
Private Sub LoadQuery_Click()
' I Dim a bunch of stuff here
' if there are no saved queries, alert the user
If saveSht.Range("B3").Value = "" Then
MsgBox "No saved queries!"
Exit Sub
' if there is only one saved query, add it to the array and pop up the userform that allows for the user to select which save to load
ElseIf saveSht.Range("B4").Value = "" Then
save_names = saveSht.Range("B3").Value
LoadBox.Show
' otherwise, add all of the save names to the array and pop up that userform
Else
save_names = saveSht.Range(saveSht.Range("B3"),saveSht.Range("B3").End(xlDown)).Value
LoadBox.Show
End If
' if the user didn't select a save to load, stop trying to make stuff happen
If load_name = "" Then
' the userform will also close here if this turns out to be true
Exit Sub
End If
' do a bunch of stuff with the selected name here
' and after this line, the userform that contains this code closes
End Sub
EDIT: here is some code showing the two other userforms
This one is the userform with no issue that is called after a textbox is double clicked
Private Sub UserForm_Initialize()
' On start up of this form, populate the listbox with the relevant column names
' Set position
Me.StartUpPosition = 0
Me.Top = baseUF.Top + 0.5 * baseUF.Height - 0.5 * Me.Height
Me.Left = baseUF.Left + 0.5 * baseUF.Width - 0.5 * Me.Width
With FilterSelectionBox
' First grab all of the column names from the main selected table
For i = 0 To baseUF.SelectionBox.ListCount - 1
.AddItem baseUF.SelectionBox.List(i)
Next i
' Then grab all of the column names from the additional tables to be joined
If Not IsVariantEmpty(join_table_cols) Then
For n = 0 To UBound(join_table_cols)
If Not IsEmpty(join_table_cols(n)) Then
For Each col_name In join_table_cols(n)
.AddItem col_name
Next
End If
Next n
End If
End With
End Sub
Private Sub OkButton_Click()
' Initialize the variables
Dim tb As MSForms.TextBox
Dim arr() As String
Dim str As String
' tb is the textbox object that the column names will be pasted in to
Set tb = baseUF.MultiPage1.Pages(baseUF.MultiPage1.Value).Controls(Me.Tag)
' sets the str according to some logic
' This is actually where it gets sent
tb.Value = str
' And close the form
Unload Me
End Sub
And this is the code in the userform with an issue
Private Sub UserForm_Initialize()
' On initialization, populate the combobox with all of the save names present in the spreadsheet
' Set position
Me.StartUpPosition = 0
Me.Top = baseUF.Top + 0.5 * baseUF.Height - 0.5 * Me.Height
Me.Left = baseUF.Left + 0.5 * baseUF.Width - 0.5 * Me.Width
With LoadComb
' If there is more than one save present, go through the array and add each one
If IsArray(save_names) Then
For Each saved_name In save_names
.AddItem saved_name
Next
' Otherwise just add the one
Else
.AddItem save_names
End If
End With
End Sub
Private Sub LoadButton_Click()
' When the user hits the load button, first check if they actually selected anything
If LoadComb.Value = "" Then
' If they didn't, yell at them
MsgBox "No saved query selected!"
Else
' Otherwise, save the name to a global variable
load_name = LoadComb.Value
End If
' Close the form
Unload Me
End Sub
Whenever something unexpected happens with forms, consider writing End in the immediate window and pressing enter. It will kill all the unkilled instances of a form and generally any variable, thus it would be like a cold restart to the VBA program.
After doing this, it is a good idea to consider a cleaner solution, concerning VBA & UserForms, using some OOP. (Disclaimer - the first article is mine):
http://www.vitoshacademy.com/vba-the-perfect-userform-in-vba/
https://rubberduckvba.wordpress.com/2017/10/25/userform1-show/
https://codereview.stackexchange.com/questions/154401/handling-dialog-closure-in-a-vba-user-form
Although it may seem that you are achieving the same results with more code, the benefits of using this approach are quite a lot in the long term.
This is a small example of the OOP model. Imagine you have a user form like this:
It has only the following controls:
btnRun
btnExit
lblInfo
frmMain (the class)
The code withing the form is the following:
Option Explicit
Public Event OnRunReport()
Public Event OnExit()
Public Property Get InformationText() As String
InformationText = lblInfo.Caption
End Property
Public Property Let InformationText(ByVal value As String)
lblInfo.Caption = value
End Property
Public Property Get InformationCaption() As String
InformationCaption = Caption
End Property
Public Property Let InformationCaption(ByVal value As String)
Caption = value
End Property
Private Sub btnRun_Click()
RaiseEvent OnRunReport
End Sub
Private Sub btnExit_Click()
RaiseEvent OnExit
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = vbFormControlMenu Then
Cancel = True
Hide
End If
End Sub
The form is with two events, getting caught by the clsSummaryPresenter. The clsSummaryPresenter looks like this:
Option Explicit
Private WithEvents objSummaryForm As frmMain
Private Sub Class_Initialize()
Set objSummaryForm = New frmMain
End Sub
Private Sub Class_Terminate()
Set objSummaryForm = Nothing
End Sub
Public Sub Show()
If Not objSummaryForm.Visible Then
objSummaryForm.Show vbModeless
Call ChangeLabelAndCaption("Press Run to Start", "Starting")
End If
With objSummaryForm
.Top = CLng((Application.Height / 2 + Application.Top) - .Height / 2)
.Left = CLng((Application.Width / 2 + Application.Left) - .Width / 2)
End With
End Sub
Private Sub Hide()
If objSummaryForm.Visible Then objSummaryForm.Hide
End Sub
Public Sub ChangeLabelAndCaption(strLabelInfo As String, strCaption As String)
objSummaryForm.InformationText = strLabelInfo
objSummaryForm.InformationCaption = strCaption
objSummaryForm.Repaint
End Sub
Private Sub objSummaryForm_OnRunReport()
MainGenerateReport
Refresh
End Sub
Private Sub objSummaryForm_OnExit()
Hide
End Sub
Public Sub Refresh()
With objSummaryForm
.lblInfo = "Ready"
.Caption = "Task performed"
End With
End Sub
Finally, we have the modMain, which is the so-called business logic of the form:
Option Explicit
Private objPresenter As clsSummaryPresenter
Public Sub MainGenerateReport()
objPresenter.ChangeLabelAndCaption "Starting and running...", "Running..."
GenerateNumbers
End Sub
Public Sub GenerateNumbers()
Dim lngLong As Long
Dim lngLong2 As Long
tblMain.Cells.Clear
For lngLong = 1 To 10
For lngLong2 = 1 To 10
tblMain.Cells(lngLong, lngLong2) = lngLong * lngLong2
Next lngLong2
Next lngLong
End Sub
Public Sub ShowMainForm()
If (objPresenter Is Nothing) Then
Set objPresenter = New clsSummaryPresenter
End If
objPresenter.Show
End Sub

How to create Sub on dynamically created ComboBox in VBA?

I am very new to excel programming and VBA. I am stuck at a point where I have random number of dynamically created combo boxes (ComboBox1, ComboBox2.... ComboBoxN).
I need to implement a functionality where if I select a value in the ComboBox[i] (where i can be any random number between 1 to N), then it should trigger an event that will populate values in ComboBox[i+1].
How do I write a Sub for this? Is there any other way to implement this if not in a Sub?
In order to create a group events you'll need a custom class to capture the events ( ObjectListener ), public variable to keep the class references alive (usually a collection or array - ComboListener ) and a Macro to fill the collection ( AddListeners_ComboBoxes ).
Call the AddListeners_ComboBoxes Macro from the Workbook_Open(). You will need call AddListeners_ComboBoxes again if the code breaks.
Standard Module
Public ComboListener As Collection
Sub AddListeners_ComboBoxes()
Dim ws As Worksheet
Dim obj As OLEObject
Dim listener As ObjectListener
Set ComboListener = New Collection
For Each ws In Worksheets
For Each obj In ws.OLEObjects
Select Case TypeName(obj.Object)
Case "ComboBox"
Set listener = New ObjectListener
Set listener.Combo = obj.Object
ComboListener.Add listener
End Select
Next
Next
End Sub
Class ObjectListener
Option Explicit
Public WithEvents Combo As MSForms.ComboBox
Private Sub Combo_Change()
MsgBox Combo.Name
Select Case Combo.Name
Case "ComboBox2"
ActiveSheet.OLEObjects("ComboBox3").Object.ListIndex = 1
End Select
End Sub
As an alternative to the "Class" approach shown by Thomas Inzina here's a "less structured" approach:
Private Sub ComboBox1_Change()
PopulateCombo 2
End Sub
Private Sub ComboBox2_Change()
PopulateCombo 3
End Sub
Private Sub ComboBox3_Change()
PopulateCombo 4
End Sub
Private Sub ComboBox4_Change()
PopulateCombo 1 '<--| will "last" combobox populate the "first" one?
End Sub
Private Sub PopulateCombo(cbNr As Long)
With ActiveSheet.OLEObjects("ComboBox" & cbNr) '<--| reference the combobox as per the passed number
.ListFillRange = "Sheet1!J1:J10" '<--| populate it with "Sheet1" worksheet range "A1:A10"
.Object.ListIndex = 1 '<--| select its first item
End With
End Sub

Show Sum of 2 Textboxes in 3rd Textbox

I'm trying to do a direct calculation but it doesn't work.
This is what I have at the moment.
Private Sub TextBox270_AfterUpdate()
TextBox270.Value = Val(TextBox1.Value) + Val(TextBox150.Value)
End Sub
I am trying to achive
1 (in TextBox1) + 2 (in TextBox150) = 3 (in TextBox270)
You need to handle the change event of TextBox1 and Textbox150
Is this what you are trying? (Untested) I am assuming that you will be entering valid numbers in those two textboxes.
Private Sub TextBox1_Change()
GenerateSum
End Sub
Private Sub TextBox150_Change()
GenerateSum
End Sub
Sub GenerateSum()
If Len(Trim(TextBox1.Text)) <> 0 And _
Len(Trim(TextBox150.Text)) <> 0 Then
TextBox270.Text = Val(Trim(TextBox1.Text)) + _
Val(Trim(TextBox150.Text))
End If
End Sub