I'm at a loss why this is not working and other questions and answers here has not lead me to an answer.
I have a simple project with 2 listboxes. One is named lList and the other is named lListH.
The below works:
function MyFunction (byval lList as listbox, byval lListH as listbox)
lList.Items.Add("Visible list")
lListH.Items.Add("Hidden List")
end Function
Whereas the below is what I'd like to use, but it doesn't work:
function MyFunction (byval lList as listbox)
Dim sControlName = lList.Name & "H"
Debug.Print(sControlName) 'Outputs lListH as expected
Dim lListH As ListBox = CType(Me.Controls(sControlName), ListBox)
lList.Items.Add("Visible list")
lListH.Items.Add("Hidden List") 'error on this line: System.NullReferenceException, 0x80004003
end Function
The most likely explanation is that the ListBox is not directly on the form but, rather, is in some other container, e.g. a Panel. That means that it is in the Controls collection of that container, not that of the form. You can also call the Find method of the form's Controls collection to search it and its children. Note that that will return a Control array, because there could be multiple controls with the same name in different containers.
Related
In MS Access 2016, let's say I have 2 forms: frmMain and frmBaby.
I have embedded frmBaby as a subform on frmMain. I have embedded on frmBaby a control (let's say it's a textbox, but it could be any control) named tbxInput.
On frmMain, since frmBaby is a "control" on frmMain, I have given that control the traditional name of subfrmBaby.
Now, in VBA, an event on subfrmBaby passes the tbxInput control ByRef (as Me.tbxInput) to a function that is meant to return the .Left property of the parent of the control passed ByRef. That is, I need the function to determine the .Left property for the location of subfrmBaby on frmMain. (The function is more complicated than this, but for the sake of keeping this question let's just say the function is returning the .Left property value because the .Left value is what I need to perform the function.)
Let's say the function is: Public Function fncLocation(ByRef whtControl As Variant) as Long
(I use Variant so that null values can be passed.)
Here is the code that I expected to return the .Left value of the parent (i.e., subfrmBaby) of whtControl: lngLeft = whtControl.Parent.Left
However, that gives me an error of: "Application or object-defined error"
When I use the immediate window to check things out I find that whtControl.Parent.Name is "frmBaby" and not "subfrmBaby" which makes it problematic to reference the subform on frmMain since I cannot figure out how to get the actual name given to the control on frmMain from the object passed to the function and so I cannot reference the subform by name either.
Questions:
How can I get the .Left value for the parent of the control passed to this function?
How can I get the actual name assigned to the subform control on frmMain? In this case, I need the name of "subfrmBaby" rather than "frmBaby."
Thanks in advance for ideas.
You can do this by iterating the controls on the main form, assuming whtControl is the form object of the subform (if it's a textbox, it's whtControl.Parent.Parent and If c.Form Is whtControl.Parent Then)
Dim mainForm As Form
Set mainForm = whtControl.Parent
Dim c As Access.Control
Dim subformControl As Access.Control
For Each c In mainForm.Controls
If TypeOf c Is SubForm Then
If c.Form Is whtControl Then
Set subformControl = c
Exit For
End If
End If
Next
If Not subformControl Is Nothing Then
Debug.Print subformControl.Left
End If
Note that iterating controls comes at a performance penalty, but this code should still take milliseconds, not seconds. Also, since we test reference equality, it works even if the same subform is present multiple times on the parent form.
I just had this issue, and I think I solved it! Thanks to Eric A's answer above to get me started. I tweaked it and built on it for my use. In my case, I needed to save the "full" address of a control to build and facilitate a control log (used to log both user actions for auditing and to allow for users to "undo" an action). I have several duplicated subforms in several sub-form controls, and a few sub-sub forms (each displaying differently filtered and sorted data), so I couldn't rely on simply knowing the subform's name, I also needed the subform control name. This also leverages others' work (as noted in the code notes with some tweaks to allow easier re-use for us. I've posted it here, hopefully it will help someone else. I know I've used SO a lot.
How we use it:
On a form, after logging an action, we record the control's ID info, which calls a function to get the toppost form (this is used in conjunction with afterUpdate event so we refresh the main form and subform). We also use the HWND to validate some other items elsewhere, and to grab a form if we don't have the initial form reference. If you use this and modify it, please point back to here and give comments.
Specific Function Code to get Control "address" and get control from address
' Posted on StackOverflow 2022 February 18 in response to Question:
' https://stackoverflow.com/q/66425195/16107370
' Link to specific answer: https://stackoverflow.com/a/71176443/16107370
' Use is granted for reuse, modification, and sharing with others
' so long as reference to the original source is maintained and you
' help lift others up as others have done those who helped with this concept
' and code.
Private Function GetControlAddress(ByRef ControlTarget As Object, _
ByRef ParentForm As Access.Form) As String
' Used in concert with building a form ID, this allows reflection back to the specific
' subform control and containing subform.
Dim ControlSeek As Access.Control
If TypeOf ControlTarget Is Form Then
' You need to dig through the whole list to get the specific controls for proper reflection down.
For Each ControlSeek In ParentForm.Controls
If ControlSeek Is ControlTarget Then
GetControlAddress = ParentForm.Name & FormIDHWNDSep & ParentForm.Hwnd & FormIDHWNDSep & ControlTarget.Name & FormIDFormSep
Exit For
ElseIf TypeOf ControlSeek Is SubForm Then
If ControlSeek.Form Is ControlTarget Then
GetControlAddress = ParentForm.Name & FormIDHWNDSep & ParentForm.Hwnd & FormIDHWNDSep & ControlSeek.Name & FormIDFormSep
End If
End If
Next ControlSeek
Else
' If you're not looking for a form, then you can skip the slow step of running through all controls.
GetControlAddress = ParentForm.Name & FormIDHWNDSep & ParentForm.Hwnd & FormIDHWNDSep & ControlTarget.Name & FormIDFormSep
End If
End Function
Public Function GetControlByAddress(ByRef StartingForm As Access.Form, ByRef strControlAddress As String) As Access.Control
' Given a control address and a starting form, this will return that control's form.
Dim ControlTarget As Access.Control
Dim TargetForm As Access.Form ' This is a reference to the hosting control
'Dim ControlSeek As
Dim FormIDArr() As String
Dim FormInfo() As String
Dim ControlDepth As Long
Dim CurrentDepth As Long
If strControlAddress = vbNullString Then GoTo Exit_Here
FormIDArr = Split(strControlAddress, FormIDFormSep)
' Because there's always a trailing closing mark (easier to handle buidling address), we skip the last array
' value, as it's always (or supposed to be...) empty.
ControlDepth = UBound(FormIDArr) - LBound(FormIDArr)
' Split out the form's Specific Information to use the details.
FormInfo = Split(FormIDArr(CurrentDepth), FormIDHWNDSep)
' The specific control is located in the 3rd element, zero referenced, so 2.
Set ControlTarget = StartingForm.Controls(FormInfo(2))
' If ControlDepth is 1 (control is on passed form) you can skip the hard and slow work of digging.
If ControlDepth > 1 Then
For CurrentDepth = 1 To ControlDepth - 1
' Note: you start at 1 because you already did the first one above.
' Split out the form's Specific Information to use the details.
FormInfo = Split(FormIDArr(CurrentDepth), FormIDHWNDSep)
Set TargetForm = ControlTarget.Form
Set ControlTarget = TargetForm.Controls(FormInfo(2))
Next CurrentDepth
End If
Exit_Here:
Set GetControlByAddress = ControlTarget
End Function
Required Helper Functions
Note, I use a property for the separators as there is some user locale handling (no included), and it also ensures that if we do change the separator it remains consistent. In this example, I simply set them to a character which is unlikely to be used in a form name. You will need to ensure your forms don't use the separator characters.
Public Function hasParent(ByRef p_form As Form) As Boolean
' Borrowed concept from https://nolongerset.com/get-top-form-by-control/
' and modified for our uses.
On Error Resume Next
hasParent = (Not p_form.Parent Is Nothing)
Err.Clear ' The last line of this will cause an error. Clear it so it goes away.
End Function
Private Function GetFormObjectByCtl(ByRef ctl As Object, _
ByRef ReturnTopForm As Boolean, Optional ByRef strControlAddress As String) As Form
strControlAddress = GetControlAddress(ctl, ctl.Parent) & strControlAddress
If TypeOf ctl.Parent Is Form Then
If ReturnTopForm Then
If hasParent(ctl.Parent) Then
'Recursively call the function if this is a subform
' and we need the top form
Set GetFormObjectByCtl = GetFormObjectByCtl( _
ctl.Parent, ReturnTopForm, strControlAddress)
Exit Function
End If
End If
Set GetFormObjectByCtl = ctl.Parent
Else
'Recursively call the function until we reach the form
Set GetFormObjectByCtl = GetFormObjectByCtl( _
ctl.Parent, ReturnTopForm, strControlAddress)
End If
End Function
Public Function GetFormByCtl(ctl As Object, Optional ByRef strControlAddress As String) As Form
Set GetFormByCtl = GetFormObjectByCtl(ctl, False, strControlAddress)
End Function
Public Function GetTopFormByCtl(ctl As Object, Optional ByRef strControlAddress As String) As Form
Set GetTopFormByCtl = GetFormObjectByCtl(ctl, True, strControlAddress)
End Function
Public Property Get FormIDHWNDSep() As String
FormIDHWNDSep = "|"
End Property
Public Property Get FormIDFormSep() As String
FormIDFormSep = ";"
End Property
Interesting. I don't think you can.
As you have seen, the parent of whtControl is its form, frmBaby.
The parent of that one is frmMain. The subform control is not part of the object chain when "going up", only when going down.
If you always use the naming scheme as in the question, you could do something like this (air code):
strSubform = whtControl.Parent.Name
strSubformCtrl = "sub" & strSubform
Set ctlSubform = whtControl.Parent.Parent(strSubformCtrl)
I am trying to categorize/group my form controls in order to be able to apply changes to several of them in one go. For instance, I may want to enable/disable them.
Should I add the form controls to collections, dictionaries or should I create classes?
Ideally I would like to create categories and sub-categories. I would define properties for the categories and the sub-categories. The properties of the categories would be passed to the "child" sub-categories.
For instance, if the font of CategoryA is "arial" the font of subcategories A1, A2 ect would also be "arial".
I would only "store" the objects in the sub-categories. If you have ideas how I could such architecture please make some suggestions.
At this stage I have created a dictionary. I am quite sure that I cannot create the categories/sub-categories I would like to with dictionaries and collections but it's still a first step in the right direction (bulk changes).
The problem I am facing with dictionaries is that the properties/methods specific to the controls do not display in the IntelliSense. How can I make them available?
Public dct As New Dictionary(Of Object, Integer)
Dim ctlr As Control
Dim i As Integer
i = 1
For Each ctlr In Controls
dct.Add(ctlr.Name, i)
i = i + 1
Next
For Each Item In dct
'Enabled is not available
Item.Enabled = False
Next
I would dump the dictionary and use a List(Of T). Add the actual object to the list and the properties should be available. In any case here is the dictionary code. Comments and explanation in-line.
Public dct As New Dictionary(Of String, Integer)
'I changed Object to String because that is what you are
'adding to the dictionary. I don't see where the .Value
'is ever used so I suggest changint to List(Of Button) or List(Of Control)
Private Sub OpCode2()
Dim ctlr As Control
Dim i As Integer
i = 1
'This saves the name of the control to the key of the dictionary
'This is just a string unrelated to a control as far as the dictionary knows
For Each ctlr In Controls
dct.Add(ctlr.Name, i)
i = i + 1
Next
For Each Item As KeyValuePair(Of String, Integer) In dct
'The .Find method returns a control using the control name
Dim ctrl As Control = Controls.Find(Item.Key, True).FirstOrDefault()
'The properties inherited from Control will be available in intellisense
'If you need a property of a particular type of control
'you will need to cast ctrl to the type you need
ctrl.Enabled = False
Next
End Sub
The following code uses List(Of T) and is much shorter and easier to read.
Public lst As New List(Of Control)
Private Sub OpCode2()
Dim ctlr As Control
For Each ctlr In Controls
lst.Add(ctlr)
Next
For Each Item In lst
'The list actually contains a reference to the control
'so properties of Control are available
Item.Enabled = False
Next
End Sub
I am trying to access a ComboBox on a UserForm from a sub. Therefore I'm trying to pass a Combobox object into it.
However, I don't seem to be able to create a Combobox Object in order to pass it in. They are always empty when entering the sub. This is what I've been trying:
Dim ctl As ComboBox
Set ctl = Me.cb_FcnName 'cb_FcnName is the name of the Combobox I'm trying to access
Call ColumnEntries2Combobox(ctl)
And this is my Sub:
Private Sub ColumnEntries2Combobox(ByRef Combo As ComboBox)
Combo.AddItem = Worksheets(WorksheetName).Cells(currRow, 2)
End Sub
For some reason I can't seem to find any documentation on how to create the necessary combobox object to pass into the sub...
Thanks in advance for any kind of help!
AddItem is a method, not a property. For a method we supply arguments after a space, compared to setting a property equal to something.
So change
Combo.AddItem = Worksheets(WorksheetName).Cells(currRow, 2)
to
Combo.AddItem Worksheets(WorksheetName).Cells(currRow, 2)
This is a common error, so a simple demonstration is:
object.Property = value
object.Method arg1, arg2
I'm facing a deadend When trying to call this sub :
Public Sub backblue(ByVal frm As Form, ByVal boxname As String)
For i = 1 To 3
CType(frm.Controls(boxname & i.ToString()), TextBox).BackColor = Color.LightBlue
Next
End Sub
with button click event :
Private Sub Button1_click and bla bla....
backblue(Me, "txb1_")
End Sub
Can anybody show me a suggestion to fix the code.
It throws "Object Referrence not set to an instance bla bla" error
For information the textbox names are :
txb1_1 , txb1_2 , txb1_3
(these are some of the many textboxes in the form that i want its bakcolor changed)
and these three textboxes are already created through designer, not from execution.
i did check the textboxes names and there's nothing wrong.
the form class is also public.
if they are the only textboxs on said form you can just loop through
For Each box as Textbox In frm.Controls
box.BackColor = Color.LightBlue
Next
This error will occur if you do not declare the Form class to be public.
Also, make sure the textbox names are really correct, although this will probably cause a different error.
If you create the textboxes during execution, make sure they are initialized with New and added to the form's Controls collection.
Try this....
Public Sub backblue(ByVal frm As Form, ByVal prefix As String)
For i = 1 To 3
Dim bxName as String = prefix & i.ToString()
Dim bx as TextBox = CType(frm.Controls(bxName), TextBox)
If bx Is Nothing Then
MsgBox("Unable to find text box " +bxName)
Dim mtch() As Control = frm.Controls.Find(bxName, true)
If mtch.Length> 0 then
bx = mtch(0)
Else
Continue For
End if
End If
Bx.BackColor = Color.LightBlue
Next
End Sub
Although, a better solution would be to either create the textboxes inside a control and pass that control to BackBlue or to create an collection that has the controls and pass that in. Which brings up what is most likely yor problem your control is contained in a sub component and thus is not in the main form control collection
Alternative, you could use either the tag of the control or create a component control that implements IExtenderProvider and add it to the form --all of the above would effectively allow you to define the controls and/how they should be handled at designtime.
It may really seem that the names generated by this loop may not be the names of the original textboxes. My suggestion is before setting this Color property verify that the names generated by this loop are indeed the actual names. Maybe output this in a messagebox:
MessageBox.Show(boxname & i.ToString()) for each loop before you set the property
Have the following code that ends up with a .Count variable. I would like to take that integer value and output it to a sentence in a custom UserForm. How do I do this within the Visual Studio 2012 designer. This is really stumping me. Thanks!
Public Shared Property mailItem As Object
Public Shared Property BodyMatchResults As MatchCollection
Public Shared Property SubjectMatchResults As MatchCollection
Public Sub Application_ItemSend(ByVal Item As Object, _
ByRef Cancel As Boolean) Handles Application.ItemSend
Dim mailItem As Outlook.MailItem = TryCast(Item, Outlook.MailItem)
If mailItem IsNot Nothing Then
Dim attachments = mailItem.Attachments
For Each attachment As Outlook.Attachment In attachments
AttachmentQuery(attachment, mailItem, Cancel)
Next attachment
End If
Dim BodyMatchResults As MatchCollection
Dim SubjectMatchResults As MatchCollection
Dim RegexObj As New Regex("\b(?!000)(?!666)(?!9)[0-9]{3}[ .-]?(?!00)[0-9]{2}[ .-]?(?!0000)[0-9]{4}\b")
BodyMatchResults = RegexObj.Matches(mailItem.Body)
SubjectMatchResults = RegexObj.Matches(mailItem.Subject)
If BodyMatchResults.Count > 0 Or SubjectMatchResults.Count > 0 Then
Cancel = True
MessageBox.Show((BodyMatchResults.Count + SubjectMatchResults.Count))
' Access individual matches using AllMatchResults.Item[]
Else
Cancel = False
End If
The UserForm is pretty basic with three buttons and a warning text above it. I would like to have in that text "There were either "BodyMatchResults.count" or "subjectmatchresults.count" in your email.
Well you obviously beging with your .count variable (which for arguments sake I'll assume is of type Integer). Hopefully you also have your Custom UserForm which we'll assume you have called UserForm.
In that UserForm add the following code:
Private _count As Integer
Public Sub New (ByVal count AS Integer)
InitializeCompponent()
_count = count
End Sub
You can then use the _count variable to dipaly the informationyou want displayed.
Now when you call your UserForm you can pass your .count variable to it so that it can be used there like this:
dim frm as New UserForm(NumberOfCountsIWantToDisplay)
This basic principle will work for most situations.
Edit
Clearly I'm not reading things properly. Your question had specifically asked about passing a Global variable. You should simply be able to refer to a publically defined global variable from anywhere within your application (so long as that variable has application scope). However global variables can be more trouble that they are worth and if in reality you simply want to pass one value from form a to form b then I would use the approach that I had originally outlined having misread the subject title, for which I apologise.