Get the index of a usercontrol in a control array at runtime - user-controls

So I've got a usercontrol.
In the form, I have a control array of these usercontrols. Each instance of the control has an index set in the designer.
I want, at runtime, to get the index of a specific usercontrol (this is in the context of a For Each loop). However, "index" is not a member of the UserControl class. How do I make it so I can get the index at runtime?
Example of what I am trying to do:
for each UserControl in UserControls
OtherArray(UserControl.index) = UserControl.value
next UserControl

The Index property is a member of the Control and Extender objects, but NOT the user controls.
You can get the index by typing the variable correctly:
Dim UserControl As MyUserControlType
Dim UserControl2 As Control
For Each UserControl In UserControls
Set UserControl2 = UserControl
OtherArray(UserControl2.index) = UserControl.value
Next UserControl
You still need a variable of your user control type to access the Value property.
The former method no longer works.
As the user control can access its Index through its Extender object, you can add an additional property that the caller can use:
Public Property Get MyIndex() As Long
MyIndex = Extender.Index
End Property
And this to access it:
Dim MyUserControlInstance As MyUserControl
Dim OtherArray() As String
ReDim OtherArray(0 To 3)
For Each MyUserControlInstance In MyUserControlArray
OtherArray(MyUserControlInstance.MyIndex) = MyUserControlInstance.Value
Next MyUserControlInstance

Most controls in VB6 have a Tag property. (It's been a while, so I don't remember if User Controls have this property as well.)
If they do, you could set the Tag property to the same value as the array index, in the form designer.
If User Controls don't have a Tag property, you could loop through the array at program startup and set the Tag property of one of the controls inside each User Control. For example, pick some Label or TextBox control to hold the "Tag" property for the entire Uset Control.

Try using a for loop and access them by index:
For I = UserControls.LBound To UserControls.UBound
'Use I as the index here
Next
Note that if the array is not contiguous (Some indexes in the middle are not loaded) you will need to detect the error and skip to the next item.

I suggest a small improvement on the code above:
If the UserControl is not in an array, Extender.Index causes an error.
MyIndex returns -1 if the Usercontrol is not in an array.
Public Property Get MyIndex() As Long
On Error GoTo NotAnArrayNoIndexError
MyIndex = Extender.Index
On Error GoTo 0
Exit Property
NotAnArrayNoIndexError:
On Error GoTo 0
MyIndex = -1
End Property

What about the "name" Property. In the ControlsCollection every Control has a name.
dim i as long
dim found as boolean
for i = lBound(OtherArray) to uBound(OtherArray)
for each UserControl in UserControls
if OtherArray(i).name = UserControl.name then
found = true
exit for
end if
next UserControl
if found then exit for
next UserControl

Related

How to use String value as a dot operation in VBA? it is possible?

Sub getTrailInfo()
Dim attr As Variant
Dim attrTB As String
attrNames = Split("trailName,trailType,aSideSite,zSideSite,status", ",")
Dim trailForm As New formTrailInfo
For Each attr In attrNames
attrTB = attr + "TB"
trailForm.attrTB = attr
Next attr
trailForm.Show
End Sub
When I run the above code it gives a compilor error: Method or Data not found at line trailForm.attrTB = attr
I have required variables in attrNames String array. I need to put values of these variables in corosponding textboxes in a userForm. The name of Text Box in this userForm is attrNameTB. For example Text box for trailName is trailNameTB.
You cannot use VBA like that.
When you start your code, the compiler will first compile the code and check that you want to access a property named attrTB of your form. This doesn't exist and you will get the error you mentioned.
The compiler cannot wait until your variable attrTB has an actual value and then guess that you don't want a property with that name, but use the content of that variable as property name.
However, every form has a collection of all it's controls (button, edit boxes, combo boxes, labels...), and you can access the members of a collection either by index or by name. As you have the name of the control in attrTB, you could simply write
trailForm.Controls(attrTB).Text = attr

How can I get a reference to a TextBox itself instead of to its default value in Excel vba?

Windows 10 Pro 64, Office 365 Excel vba
ClassCounter is a class that operates on a Long value it stores internally and then displays that value in a TextBox in an active UserForm. I want to be able to assign the TextBox to the ClassCounter object dynamically, so that I can use the same class to instantiate a number of objects, each of which references its own TextBox in the same UserForm.
It has the following private members declared at the class level:
Private mctrLinked As ClassCounter
Private mnCount As Long
Private mtbxDisplay As TextBox
The Initialize subroutine makes the connection between displayTextBox (the textbox used to display the value) and the object. linkCounter provides the opportunity to link to another object of the same class and fire off the same operation on it in a daisychain fashion.
Public Sub Initialize(ByRef displayTextBox As msforms.TextBox, Optional linkCounter As ClassCounter = Nothing)
Const kstrMethodName As String = "Initialize"
Set mtbxDisplay = displayTextBox
Set mctrLinked = linkCounter
Clear
End Sub ' Initialize
The class is instantiated in another class, as follows:
Private mobjAllBlankCounter As New ClassCounter
Private mobjAllEnteredCounter As New ClassCounter
Private mobjAllFoundCounter As New ClassCounter
Private mobjAllIssuesCounter As New ClassCounter
...
And the connection between the TextBox and the ClassCounter object is established by calling the Initialize subroutine in this way:
Public Sub InitializeAllCounters()
With mufMCP
mobjAllBlankCounter.Initialize .tbxBlankCountAll
mobjAllEnteredCounter.Initialize .tbxEnteredCountAll
mobjAllFoundCounter.Initialize .tbxFoundCountAll
mobjAllIssuesCounter.Initialize .tbxIssuesCountAll
End With
End Sub ' InitializeAllCounters
where mufMCP is the UserForm in which the TextBoxes are defined.
Ultimately, the Increment function (and others like it) will operate on the stored variable and then display it in the referenced TextBox as follows:
Public Sub Increment()
Const kstrMethodName As String = "Increment"
If (mtbxDisplay Is Nothing) Then
Err.Raise gknErrNoControlForCounter, mkstrModuleName & "." & kstrMethodName, "Attempt to Increment Counter with no associated control."
Else
mnCount = mnCount + 1
mtbxDisplay.Text = CStr(mnCount)
If (Not (mctrLinked Is Nothing)) Then
mctrLinked.Increment
End If
End If
End Sub ' Increment
The problem I'm having is in the Initialize subroutine where I attempt to assign the value of the TextBox argument to the local variable. Instead of assigning a reference to the TextBox itself, the right side of the assignment is evaluating to the TextBox's default value, which is its Text property. As a result, I get a type mismatch error.
How can I get it to evaluate to a reference to the TextBox itself? I've spent a couple of days searching for the answer and found several sources that said that using ByRef displayTextBox As msforms.TextBox to define the parameter would do the trick, but I'm still getting the control's default value.
As FaneDuru writes, TextBox and MsForms.TextBox are two different object types. A TextBox is a textbox (Form Control, not Active X Control) placed on a sheet. A MsForms.TextBox is a textbox places on a user form.
Bad thing (1): The name "Form Control" related to sheet controls is misleading, it is not the same as a control placed on a user form.
Bad thing (2): As the Form Control Textbox is no longer available from the Developer menu, it is not easy to proof. If you are interested: You can still create them using VBA.
Dim tb1 As TextBox
Set tb1 = ActiveSheet.TextBoxes.Add(255, 243, 73.5, 22.5)
Dim tb2 As msforms.TextBox
UserForm1.Show False
Set tb2 = UserForm1.TextBox1
Checking both objects in the Locals window of the VBA editor, type for both is displayed as "Textbox/Textbox". However, when you look at the properties of the objects, you see that they are different.
So declare all your (userform) controls with the suffix msforms to be sure that you are dealing with the right object types.

VBA - Class Module - access to property by index

is there way how to make class working similar to Arrays?
Let's say, I have Class (e.g. Workers) where main property is array of the Workers, nothing else.
Then I'm filling the class as follows
Dim wks as new Workers
wks.add("Worker1")
wks.add("Worker2")
wks.add("Worker3")
Then in Workers Class module:
Private Workers as Variant
Public Function add(ByVal val As Variant) As Long
ReDim Preserve Workers(LBound(Workers) To UBound(Workers) + 1)
Workers(UBound(Workers)) = val
add = UBound(Workers) - LBound(Workers) +1
End Function
Workers representation -> {"Worker1", "Worker2", "Worker3"}
Then I want to access Worker by its index. I know, how to access it by e.g wks.getWorker(1) but what I want to do, is to access it directly by wks(1) which should return "Worker 1". Example above looks, that usual Array or Collection can be used, but I have many internal methods done, only what I'm missing is to access Workers property to read/write directly by its index number.
Is it possible?
Edit
After transfer to Collections, Class looks like:
Option Explicit
Private Workers As Collection
Private Sub Class_Initialize()
Set Workers = New Collection
End Sub
Public Function add(ByVal val As Variant) As Long
Workers.add val
End Function
Public Property Get Item(Index As Integer) As Variant
Item = Workers(Index)
End Property
Public Property Set Item(Index As Integer, Value As Variant)
Workers.Remove Index
Workers.add Value, Before:=Index
End Property
with hidden attributes Attribute Item.VB_UserMemId = 0 at Getter and Setter.
Getting works fine:
Dim wks As New Workers
wks.add "Worker1"
wks.add "Worker2"
wks.add "Worker3"
Debug.Print wks(2) ' <-- OK here
'wks(2) = "Second Worker" ' <-- By debugging this go to Getter not Setter and after Getter is done, it allerts with Runtime error '424': Object required
Set wks(2) = "Second Worker" ' <-- This alert immediately Compile error: Object required on "Second Worker" string
Debug.Print wks(2)
Prints "Worker2" into console, thanks for this, but still I'm not able to set a new value to the required Index of the Workers Collection.
You could use a default member in VBA. Though you can't make the default memeber directly through VBA editor, but you can use any text editor.
Export your class from VBA editor, i.e. File->Export File
Open your exported class in Notepad (or any text editor)
Add this attribute line on your method or property you want to make it default. Attribute Item.VB_UserMemId = 0
You can for example make getWorker default member as.
Public Function GetWorker(Index As Integer) As Worker
Attribute Item.VB_UserMemId = 0
GetWorker = Workers(Index)
End Function
you can then use it like.
Set wk = wks(1)
Here is some detail about default members
http://www.cpearson.com/excel/DefaultMember.aspx
Edits
An example to make Getter/Setter as default member
Public Property Get Item(Index as Integer) as Worker
Attribute Item.VB_UserMemId = 0
Set Item = Workers(Index)
End Property
Public Property Set Item(Index as Integer, Value as Worker)
Attribute Item.VB_UserMemId = 0
Set Workers(Index) = Value
End Property

Set CheckBox Control dim as a UserForm CheckBox

I am attempting to create a class module that builds on top of a checkbox control. Within the Class Module I want to point at the checkbox in the userform. Although, when I try to fill the CheckBox object with one of the checkboxes in the userform I get a type-mismatch since calling on the checkbox gives back it's state instead of the entire object. Is there a way to get the entire object?
I have tried
set myCheckBox = makeMyCheckBox(Me.CheckBox1)
and
set myCheckBox = makeMyCheckBox(Me.CheckBox1.Object)
where makeMyCheckBox is a function that takes in a CheckBox object and creates a new MyCheckBox object.
'Within my userform's code
Dim myCheckBoxes(1 to 2) As MyCheckBox 'MyCheckBox is my class module
Private Sub UserForm_Initialize()
set myCheckBoxes(1) = makeMyCheckBox(me.CheckBox1)'<--Error Type Mismatch
End Sub
Private Function makeMyCheckBox(c As CheckBox) As MyCheckBox
Dim myChck As MyCheckBox
Set myChck = New MyCheckBox
myChck.init c 'takes in a CheckBox and fills its internal CheckBox object
Set makeMyCheckBox= myChck
End Function
I expect Me.CheckBox1 to be a CheckBox object.
Me.CheckBox1 outputs the checkbox's state when I look in debug (true/false)
I get--
Run-time error '13':
Type Mismatch
I get a type-mismatch since calling on the checkbox gives back it's state instead of the entire object. Is there a way to get the entire object?
Wrong assumption, you're getting the "entire object", but the object you're getting isn't implementing the interface you're expecting, hence the type mismatch.
You need to qualify your MSForms types explicitly with the MSForms library, like this:
Private Function makeMyCheckBox(ByVal c As MSForms.CheckBox) As MyCheckBox
Otherwise the unqualified CheckBox identifier / type name is referring to Excel.CheckBox, because the host application's object model (the Excel library) always has a higher priority than the referenced MSForms library, in the project references dialog:
This is excruciatingly hard to discover in a vanilla VBE. With Rubberduck you just place the caret on CheckBox and it tells you where it's coming from:
Without any add-ins, you kind of have to guess what the actual type is, because Shift+F2 (which normally takes you to the definition in the Object Browser) is useless for this - all you get is a message saying "the identifier under the cursor is not recognized".
Disclaimer: I manage the Rubberduck open-source project.

VBA class module: get property from an object using another property

All,
I am setting-up a class module structure in VBA to add plans that have multiple milestones, but I'm quite new to it. I did the following:
A class module called 'Plan' that contains a 'name' property (string) and a 'Milestones' property (class Milestones).
This milestones class module is a collection of objects of a class module called 'Milestone'.
The 'Milestone' class has a 'name' property and a 'value' property.
So in my module I am now specifying the milestones for a specific plan:
Plan.Milestones.Add "MilestoneA", Cells(i, 5)
Plan.Milestones.Add "MilestoneB", Cells(i, 7)
...
Until now everything is fine. Now for MilestoneC I would like to know the value of MilestoneA. How do I get the value for the Milestone with name 'MilestoneA'.
I know the below code would give me the answer, but I don't want to hardcode 'item(1)' (I want to use the name):
Plan.Milestones.Item(1).Value
In the clsMilestones class:
Private prvt_Milestones As New Collection
Property Get Item(Index As Variant) As clsMilestone
Set Item = prvt_Milestones(Index)
End Property
Sub Add(param_Name As String, param_Value As String)
Dim new_milestone As clsMilestone
Set new_milestone = New clsMilestone
new_milestone.Name = param_Name
new_milestone.Value = param_Value
prvt_Milestones.Add new_milestone
End Sub
Your Milestones class is a collection class. By convention, collection classes have an Item property that is the class' default member. You can't easily specify a class' default member in VBA, but it's not impossible.
Export the code file, open it in Notepad. Locate your Public Property Get Item member and add a VB_UserMemId attribute - while you're there you can add a VB_Description attribute, too:
Public Property Get Item(ByVal Index As Variant) As Milestone
Attribute Item.VB_UserMemId = 0
Attribute Item.VB_Description = "Gets the item at the specified index, or with the specified name."
Set Item = prvt_Milestones(Index)
End Property
The UserMemId = 0 is what makes the property the class' default member - note that only one member in the class can have that value.
Don't save and close just yet.
You'll want to make your collection class work with a For Each loop too, and for that to work you'll need a NewEnum property that returns an IUnknown, with a number of attributes and flags:
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_Description = "Gets an enumerator that iterates through the collection."
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
Set NewEnum = prvt_Milestones.[_NewEnum]
End Property
Note that your internal encapsulated Collection has a hidden member with a name that begins with an underscore - that's illegal in VBA, so to invoke it you need to surround it with square brackets.
Now this code is legal:
Dim ms As Milestone
For Each ms In Plan.Milestones
Debug.Print ms.Name, ms.Value ', ms.DateDue, ...
Next
Save the file, close it, and re-import it into your project.
Since you're populating the collection using a string key (at least that's what your Add method seems to be doing), then the client code can use either the index or the key to retrieve an item.
And now that Item is the class' default member, this is now legal:
Set milestoneA = Plan.Milestones("Milestone A").Value
Note that your Add method needs to specify a value for the Key argument when adding to the internal collection - if you want the items keyed by Name, use the Name as a key:
Public Sub Add(ByVal Name As String, ByVal Value As Variant)
Dim new_milestone As Milestone
Set new_milestone = New Milestone
new_milestone.Name = Name
new_milestone.Value = Value
prvt_Milestones.Add new_milestone, Name
End Sub
Use a dictionary of Milestone classes in the plan class and set the key to be the "Milestone_x" and the item to be a milestone class
Then you can say Plan.Milestones("Milestone99")
Add a property to the Milestones class that returns the milestone based on the name:
Property Get SelectByName(strMilestoneName as string) as clsMilestone
Dim vIndex
'Add code here to find the index of the milestone in question
vIndex = ????????
Set SelectByName = prvt_Milestones(Index)
End Property
OR
Edit the Item Property to Allow selection by either Index or Name:
Property Get Item(Index As Variant) As clsMilestone
If isNumeric(Index) then
Set Item = prvt_Milestones(Index)
Else
'Find Item based on Name
Dim vIndex
vIndex = ?????
Set Item = prvt_Milestones(vIndex)
End If
End Property

Resources