Display controls with order during For-Next loop vba - excel

I have a VBA application with a lot of controls.
I would like accessing the controls with an order of reading during For-Next loop.
' Parcours des contrôles de l'userform
For Each cCont In Me.Controls
' TypeName(cCont)
MsgBox (cCont.Name)
Next cCont
Actually, I think I am accessing with creation date...
Do you know if I could configure the order of reading ?
Thanks

One way to do this is to sort them by the TabIndex property. Set the tab indices to the desired order, then use this:
Private Sub test()
Dim cCont As Control
Dim i As Integer
Dim maxIndex As Integer
Dim controls As Object
Dim key As Variant
Set controls = CreateObject("Scripting.Dictionary")
'Add controls to dictionary, key by tabindex property
For Each cCont In Me.controls
maxIndex = IIf(maxIndex < cCont.TabIndex, cCont.TabIndex, maxIndex)
controls.Add cCont.TabIndex, cCont
Next cCont
'Get controls in order
For i = 0 To maxIndex
If controls.exists(i) Then
MsgBox controls(i).Name
End If
Next i
End Sub

The posted code is a great solution, which I made workable for me with this minor change. I passed the user form, because I used the code in a module. I excluded Label, CommandButton, and Image in order to make Valon Miller's code work for me, otherwise the depicted runtime error wouldruntime error '-2147352573 (800 200037': Member not found show.
Private Sub test(frm As UserForm)
Dim cCont As Control
Dim i As Integer
Dim maxIndex As Integer
Dim controls As Object
Dim key As Variant
Set controls = CreateObject("Scripting.Dictionary")
'Add controls to dictionary, key by tabindex property
For Each cCont In frm.controls
If TypeName(cCont) <> "Label" And _
TypeName(cCont) <> "Image" And _
TypeName(cCont) <> "CommandButton" Then
maxIndex = IIf(maxIndex < cCont.TabIndex, cCont.TabIndex, maxIndex)
controls.Add cCont.TabIndex, cCont
End If
Next cCont
'Get controls in order
For i = 0 To maxIndex
If controls.exists(i) Then
Debug.Print controls(i).Name & vbTab & i
MsgBox controls(i).Name
End If
Next i
End Sub
--------------------------------------------
Originally I needed a solution to get the order of sql statements synced with the order of my form controls. I wanted to do this:
fld1 = recordset1.value
fld2 = recordset2.value
fld3 = recordset3.value
was looking for a solution to get my controls and my SQL statement in order like field1 -> recordset.value1.
So instead of ordering my controls using the taborder, I created control arrays. i.e.
sub useControlArray()
dim a as variant, rs as new recordset, strSQL as string
strSQL = "select fld1, fld2, fld3 from table"
rs.open strSQL, connection
a = array("fld1", "fld2", "fld3")
'This array would help like this:
for i = lbound(a) to ubound(a)
frm.controls(a(i)) = rs(i)
debug.print frm.controls(a(i)) ' Form is a user form
next i
end sub
This would restrict the controls to the number of controls needed to fill them using the same order as in my SQL statement and I did not need to pay attention to whether or not a control would exist.
I hope this is helpful.

Related

Adding String to Content Control Box / Replace Dropdown with Content Control

I've got a working code but I'd like to replace my Drop-Down with Content Control, because I need to be able to also manually type in a value.
The value inside is a list from a https, this string works completely fine, so please ignore.
Here's my code:
Dim MyRequest As Object
Dim Data() As String
Dim i As Integer
Dim j As Integer
Dim maxi As Integer
Set MyRequest = CreateObject("WinHttp.WinHttpRequest.5.1")
MyRequest.Open "GET", _
"https... (This is hidden for security resons, /csv/)"
' Send Request.
MyRequest.Send
'And we get this response
'MsgBox MyRequest.ResponseText
Data = Split(MyRequest.ResponseText, "|")
If UBound(Data()) > 25 Then
maxi = 25
Else
maxi = UBound(Data())
End If
For j = 1 To 6
ActiveDocument.FormFields("Dropdown" & j).DropDown.ListEntries.Clear
For i = 0 To maxi - 1
ActiveDocument.FormFields("Dropdown" & j).DropDown.ListEntries.Add Name:=Data(i)
Next i
Next j
End Sub
You should not use content controls and formfields in the same document. They were not designed to be used that way and doing so is a known source of problems.
As you observed, dropdown formfields don’t support text entry. To provide that facility, you could provide an option in the dropdown for 'free text' and use an on-exit macro with an Inputbox to insert the user’s 'free text' into the dropdown. For example, suppose you have a dropdown with 5 items, the last of which offers free text entry (e.g. an 'Other' option). Adding the following on-exit macro to the formfield will provide that:
Sub FreeText()
Dim StrNew As String, i As Long
With Selection.FormFields(1).DropDown
i = .ListEntries.Count
If .Value = i Then
StrNew = Trim(InputBox("Input your text", "Data Entry", .ListEntries(i).Name))
If StrNew = vbNullString Then Exit Sub
.ListEntries(i).Delete
.ListEntries.Add StrNew
.Value = i
End If
End With
End Sub

VBA ComboBox Change Event not triggered

I have this issue with the ComboBox Event Handler.
I managed to create (and fill with items) the Comboboxes I wanted, the code seems to work fine. But after the program has run, if I try to pick one general item inside one of the comboboxes, it seems like the _Change Method is not called --> I cannot handle change events.
Here is my class module (class name: "DB_ComboBox")
Option Explicit
Public WithEvents DB_ComboBoxEvents As MSForms.ComboBox
Private DB_ComboBox_Line As Integer
Private Sub DB_ComboBoxEvents_Change()
MsgBox ("Line : " & DB_ComboBox_Line)
'Here I want handle The comboboxes changes
'But this routine is not called!
End Sub
Sub Box(CBox As MSForms.ComboBox)
Set DB_ComboBoxEvents = CBox
End Sub
Public Property Let Line(value As Integer)
DB_ComboBox_Line = value
End Property
Public Property Get Line() As Integer
Line = DB_ComboBox_Line
End Property
And here is my "Main module", in which I create the comboboxes and pass them to a Collection of "DB_ComboBox"
Sub CreateComboBox(IncCBoxes)
Dim curCombo As MSForms.ComboBox
Dim rng As Range
Dim tot_items As Integer
Dim incAddItem As Integer
Dim incAddItemBis As Integer
Dim itemBaseArray() As String
Dim TEMP_ComboBoxInst As New DB_ComboBox
Set rng = ActiveSheet.Range("J" & IncCBoxes)
Set curCombo = ActiveSheet.OLEObjects.Add(ClassType:="Forms.ComboBox.1", Link:=False, DisplayAsIcon:=False, Left:=rng.Left, Top:=rng.Top, Width:=rng.Width, Height:=rng.Height).Object
'Add the items
itemBaseArray = Split(Foglio7.Cells(IncCBoxes, DBColFileComboIndexErrori), ";")
For incAddItem = 0 To UBound(itemBaseArray)
Dim itemLastArray() As String
itemLastArray = Split(itemBaseArray(incAddItem), ",")
For incAddItemBis = 0 To UBound(itemLastArray)
curCombo.AddItem (itemLastArray(incAddItemBis))
Next
Next
TEMP_ComboBoxInst.Box curCombo
TEMP_ComboBoxInst.Line = IncCBoxes
customBoxColl.Add TEMP_ComboBoxInst
End Sub
Can anyone please tell me what I'm missing?
Thank you very much
This looks like a timing-issue:
Running this code in another open file will work. In same file it does not.
Seperate the adding to your class from the adding of the OLEControl i.e.:
use Application.ontime now
see code below:
Private customBoxColl As New Collection
Sub CreateComboBox(IncCBoxes As Long)
Dim curCombo As MSForms.ComboBox
Dim rng As Range
Dim tot_items As Integer
Dim incAddItem As Integer
Dim incAddItemBis As Integer
Dim itemBaseArray() As String
Dim itemLastArray() As String
Set rng = ActiveSheet.Range("J" & IncCBoxes)
With ActiveSheet.OLEObjects.Add(ClassType:="Forms.ComboBox.1", Link:=False, DisplayAsIcon:=False, Left:=rng.Left, Top:=rng.Top, Width:=rng.Width, Height:=rng.Height)
Set curCombo = .Object
End With
'Add the items
itemBaseArray = Split(Foglio7.Cells(IncCBoxes, DBColFileComboIndexErrori), ";")
For incAddItem = 0 To UBound(itemBaseArray)
itemLastArray = Split(itemBaseArray(incAddItem), ",")
For incAddItemBis = 0 To UBound(itemLastArray)
curCombo.AddItem (itemLastArray(incAddItemBis))
Next
Next
Application.OnTime Now, "'CallToClass """ & curCombo.Name & """,""" & IncCBoxes & "'"
End Sub
Sub CalltoClass(ctl As String, myline As Long)
Dim TEMP_ComboBoxInst As New DB_ComboBox
TEMP_ComboBoxInst.Box ActiveSheet.OLEObjects(ctl).Object
TEMP_ComboBoxInst.line = myline
customBoxColl.Add TEMP_ComboBoxInst
End Sub
I know this doesn't apply to your specific problem, but I'll just post this here for any others who may have this problem. In my case, the events stopped firing because I had just copied my database into a new Github repo.
On reopening Access, the events weren't firing while they had been fine the day before, which completely stumped me, especially since none of the SO answers seemed to address my issue. Basically, Access blocks macros and code, and requires it to be reenabled by clicking OK on the little yellow warning at the top of the screen.

Select variable object with counter

Background:
I have a collection of objects (for this example Listbox objects) in a userform using standardized names, I would like to rename them dynamically using a counter cycle.
Problem:
I have not figured a way if what I am asking is even possible, however, I would like to confirm it.
Solution approach:
Nothing so far, like I said (refer to the image above) I need a way to set the values of the objects within the for cycle, something like this:
For CounterItems = 1 To 18 'Hours in Template
ListBox_Time(CounterItems).Value="Dummy" & CounterItems
Next CounterHours
However, I am clueless on how to do so (or if it is achievable).
Question:
Is there any way to use a counter to cast a variable/object?
No, you can't edit the name while the userform is in use, you'll get error 382
What you'd like to do is this
Option Explicit
Sub test()
Dim myForm As UserForm
Set myForm = UserForm1
Dim myCtrl As Control
Dim i As Long
Dim myCount As Long
myCount = 1
For Each myCtrl In myForm.Controls
If TypeName(myCtrl) = "ListBox" Then
myCtrl.Name = "Dummy" & myCount 'error
myCount = myCount + 1
End If
Next
End Sub
But you'll error when you try to write to the name property. You can print the names or set other properties, but this isn't something you can do as far as I know.
For use with ListBox controls on a UserForm
If you want to change only certain ListBox controls by number:
Public Sub ListBoxNameChange()
Dim ctrl As Control
Dim ctrlName As String, ctrlNum As Integer
For Each ctrl In Me.Controls
If TypeName(ctrl) = "ListBox" Then
ctrlName = ctrl.Name
ctrlNum = CInt(Replace(ctrlName, "ListBox_Time", ""))
If ctrlNum > 0 And ctrlNum < 19 Then
ctrl.AddItem "Dummy" & ctrlNum, 0
End If
End If
Next ctrl
End Sub
If you want to change ALL ListBox controls:
Public Sub ListBoxNameChange2()
Dim ctrl As Control
Dim ctrlName As String
For Each ctrl In Me.Controls
If TypeName(ctrl) = "ListBox" Then _
ctrl.AddItem "Dummy" & Replace(ctrl.Name, "ListBox_Time", ""), 0
Next ctrl
End Sub
I treat them like Shapes and test their pre-defined Names:
Sub ShapeRenamer()
Dim s As Shape
For Each s In ActiveSheet.Shapes
If s.Name = "List Box 6" Then s.Name = "Sixth"
Next s
End Sub
Before:
and after:
You would update this to examine the Shapes in your userform.
You could also do this with an indexing counter.

Get Function() result as Control

Background:
I needed a function to correlate Controls efficiently based on a STD name and the text analyzed. IG:
a) Some other input throws the variable "mytext"
b) If the ListBox1.Value has "mytext" then I have to relate it with ToggleButton1
Approach:
I made the following function which partially works
Code:
Private Function RelateControl_ToggleVsList(ToggleCtrl As Control) As Control
Dim ItemControl As Control
Dim myControl As Object
For Each ItemControl In Me.Controls
If TypeName(ItemControl) = "ListBox" Then ' 1. If TypeName(ItemControl) = "Label"
'text lenghts const 13 for ListBox_TimeXX and 22 for ToggleButton_PriorityXX
If Mid(ItemControl.Name, 13, 2) = Mid(ToggleCtrl.Name, 22, 2) Then Set RelateControl_ToggleVsList = ItemControl: Exit Function
End If ' 1. If TypeName(ItemControl) = "Label"
Next ItemControl
End Function
Problem:
I get a null property when Setting the result:
Set RelateControl_ToggleVsList = ItemControl 'This is nullSet
Debugging process:
Question:
How can I set a Control as a result of this function?
EDIT:
Per request I add the whole Debugging in order to see where it is being called form
Calling Code
Private Sub ToggleButtons_Active()
Dim ItemControl As Control
Dim ItemTextBox As Variant
Dim TxtControl As String
For Each ItemControl In Me.Controls
If TypeName(ItemControl) = "ToggleButton" Then ' 1. If TypeName(ItemControl) = "ToggleButton"
TxtControl = CStr(RelateControl_ToggleVsList(ItemControl).Value)
If InStr(TextBox_Notes.Value, TxtControl) > 0 And TxtControl <> "" Then ItemControl.Value = True
End If ' 1. If TypeName(ItemControl) = "ToggleButton"
Next ItemControl
End Sub
Your error is occurring on your line which says
TxtControl = CStr(RelateControl_ToggleVsList(ItemControl).Value)
because the returned Control's Value property is currently Null which can't be cast to a String.
I recommend that you change TxtControl to be a Variant type, then say
TxtControl = RelateControl_ToggleVsList(ItemControl).Value
If IsNull(TxtControl) Then
TxtControl = ""
Else
TxtControl = CStr(TxtControl)
End If
Or you could define a Control object and then use it:
Dim MyControl As Control
MyControl = RelateControl_ToggleVsList(ItemControl)
If IsNull(MyControl.Value) Then
TxtControl = ""
Else
TxtControl = CStr(MyControl.Value)
End If
Solution:
The object itself is calling a property which does not belong to it (as intended).
ListBox Properties does not show "Value" as its text, the real thing should have called it as:
TxtControl = CStr(RelateControl_ToggleVsList(ItemControl).List(0))CStr(RelateControl_ToggleVsList(ItemControl).List(0))
Furthermore:
Thanks to the solutions provided and the debugging process, I could notice even the object is Set in the function, it is shown as the "value" property of it while debugging.

Use VBA to assign all checkboxes to class module

I'm having a problem assigning VBA generated ActiveX checkboxes to a class module. When a user clicks a button, the goal of what I am trying to achieve is: 1st - delete all the checkboxes on the excel sheet; 2nd - auto generate a bunch of checkboxes; 3rd - assign a class module to these new checkboxes so when the user subsequently clicks one of them, the class module runs.
I've borrowed heavily from previous posts Make vba code work for all boxes
The problem I've having is that the 3rd routine (to assign a class module to the new checkboxes) doesn't work when run subsequently to the first 2 routines. It runs fine if run standalone after the checkboxes have been created. From the best I can tell, it appears VBA isn't "releasing" the checkboxes after they have been created to allow the class module to be assigned.
The below code is the simplified code that demonstrates this problem. In this code, I use a button on "Sheet1" to run Sub RunMyCheckBoxes(). When button 1 is clicked, the class module did not get assigned to the newly generated checkboxes. I use button 2 on "Sheet1" to run Sub RunAfter(). If button 2 is clicked after button 1 has been clicked, the checkboxes will be assigned to the class module. I can't figure out why the class module won't be assigned if just the first button is clicked. Help please.
Module1:
Public mcolEvents As Collection
Sub RunMyCheckboxes()
Dim i As Double
Call DeleteAllCheckboxesOnSheet("Sheet1")
For i = 1 To 10
Call InsertCheckBoxes("Sheet1", i, 1, "CB" & i & "1")
Call InsertCheckBoxes("Sheet1", i, 2, "CB" & i & "2")
Next
Call SetCBAction("Sheet1")
End Sub
Sub DeleteAllCheckboxesOnSheet(SheetName As String)
Dim obj As OLEObject
For Each obj In Sheets(SheetName).OLEObjects
If TypeOf obj.Object Is MSForms.CheckBox Then
obj.Delete
End If
Next
End Sub
Sub InsertCheckBoxes(SheetName As String, CellRow As Double, CellColumn As Double, CBName As String)
Dim CellLeft As Double
Dim CellWidth As Double
Dim CellTop As Double
Dim CellHeight As Double
Dim CellHCenter As Double
Dim CellVCenter As Double
CellLeft = Sheets(SheetName).Cells(CellRow, CellColumn).Left
CellWidth = Sheets(SheetName).Cells(CellRow, CellColumn).Width
CellTop = Sheets(SheetName).Cells(CellRow, CellColumn).Top
CellHeight = Sheets(SheetName).Cells(CellRow, CellColumn).Height
CellHCenter = CellLeft + CellWidth / 2
CellVCenter = CellTop + CellHeight / 2
With Sheets(SheetName).OLEObjects.Add(classtype:="Forms.CheckBox.1", Link:=False, DisplayAsIcon:=False, Left:=CellHCenter - 8, Top:=CellVCenter - 8, Width:=16, Height:=16)
.Name = CBName
.Object.Caption = ""
.Object.BackStyle = 0
.ShapeRange.Fill.Transparency = 1#
End With
End Sub
Sub SetCBAction(SheetName)
Dim cCBEvents As clsActiveXEvents
Dim o As OLEObject
Set mcolEvents = New Collection
For Each o In Sheets(SheetName).OLEObjects
If TypeName(o.Object) = "CheckBox" Then
Set cCBEvents = New clsActiveXEvents
Set cCBEvents.mCheckBoxes = o.Object
mcolEvents.Add cCBEvents
End If
Next
End Sub
Sub RunAfter()
Call SetCBAction("Sheet1")
End Sub
Class Module (clsActiveXEvents):
Option Explicit
Public WithEvents mCheckBoxes As MSForms.CheckBox
Private Sub mCheckBoxes_click()
MsgBox "test"
End Sub
UPDATE:
On further research, there is a solution posted in the bottom answer here:
Creating events for checkbox at runtime Excel VBA
Apparently you need to force Excel VBA to run on time now:
Application.OnTime Now ""
Edited lines of code that works to resolve this issue:
Sub RunMyCheckboxes()
Dim i As Double
Call DeleteAllCheckboxesOnSheet("Sheet1")
For i = 1 To 10
Call InsertCheckBoxes("Sheet1", i, 1, "CB" & i & "1")
Call InsertCheckBoxes("Sheet1", i, 2, "CB" & i & "2")
Next
Application.OnTime Now, "SetCBAction" '''This is the line that changed
End Sub
And, with this new formatting:
Sub SetCBAction() ''''no longer passing sheet name with new format
Dim cCBEvents As clsActiveXEvents
Dim o As OLEObject
Set mcolEvents = New Collection
For Each o In Sheets("Sheet1").OLEObjects '''''No longer passing sheet name with new format
If TypeName(o.Object) = "CheckBox" Then
Set cCBEvents = New clsActiveXEvents
Set cCBEvents.mCheckBoxes = o.Object
mcolEvents.Add cCBEvents
End If
Next
End Sub
If OLE objects suit your needs then I'm glad you've found a solution.
Are you aware, though, that Excel's Checkbox object could make this task considerably simpler ... and faster? Its simplicity lies in the fact that you can easily iterate the Checkboxes collection and that you can access its .OnAction property. It is also easy to identify the 'sender' by exploiting the Evaluate function. It has some formatting functions if you need to tailor its appearance.
If you're after something quick and easy then the sample below will give you an idea of how your entire task could be codified:
Public Sub RunMe()
Const BOX_SIZE As Integer = 16
Dim ws As Worksheet
Dim cell As Range
Dim cbox As CheckBox
Dim i As Integer, j As Integer
Dim boxLeft As Double, boxTop As Double
Set ws = ThisWorkbook.Worksheets("Sheet1")
'Delete checkboxes
For Each cbox In ws.CheckBoxes
cbox.Delete
Next
'Add checkboxes
For i = 1 To 10
For j = 1 To 2
Set cell = ws.Cells(i, j)
With cell
boxLeft = .Width / 2 - BOX_SIZE / 2 + .Left
boxTop = .Height / 2 - BOX_SIZE / 2 + .Top
End With
Set cbox = ws.CheckBoxes.Add(boxLeft, boxTop, BOX_SIZE, BOX_SIZE)
With cbox
.Name = "CB" & i & j
.Caption = ""
.OnAction = "CheckBox_Clicked"
End With
Next
Next
End Sub
Sub CheckBox_Clicked()
Dim sender As CheckBox
Set sender = Evaluate(Application.Caller)
MsgBox sender.Name & " now " & IIf(sender.Value = 1, "Checked", "Unchecked")
End Sub

Resources