I have a bit of an oddball problem. I use a routine to check focus; and ... it stopped working. I have no idea how or why.
The routine basically checks to see if the active control is the one you're checking, and if so, returns true (so we can handle the cases where it's not).
However...it recently started returning false all the time (we didn't change anything, we only noticed when some field auditing started returning weird values). Even when the control is focused, and if there's no other controls on the form, or only one form open, and the form clearly has focus.
Does anyone have any ideas how or why this might be? It's confounding me. As you can see, I've got a test field, where we're running an init in it...and the values clearly match, name, values, every field compared, and it still doesn't return true.
What am I doing wrong?
Edit: forgot to add the code.
The whole thing as-is:
' I call it from here:
' Inside form, any control, say `PurchaseCostBox`
Private Sub PurchaseCostBox_AfterUpdate()
' Check if the focus is had
If VerifyFocus(Me.PurchaseCostBox) Then
' Save more field info.
Debug.Print Me.PurchaseCostBox.SelStart
Debug.Print Me.PurchaseCostBox.SelLen
Debug.Print Me.PurchaseCostBox.Value
Else
' Do limited stuff
Debug.Print Me.PurchaseCostBox.Value
End if
End Sub
Public Function VerifyFocus(ByRef ctlWithFocus As Control) As Boolean
Dim FrmParent As Form
Dim ctlCurrentFocus As Control
On Error Resume Next
' Determine parent form for control
' Verify focus of parent form
Set FrmParent = Screen.ActiveForm
' Verify focus of control on form
Set ctlCurrentFocus = FrmParent.ActiveControl
If Not ctlCurrentFocus Is ctlWithFocus Then
ctlWithFocus.SetFocus
DoEvents
End If
' Even adding the below line does not return true:
ctlWithFocus.SetFocus
' Return true if the control currently has the focus
VerifyFocus = FrmParent.ActiveControl Is ctlWithFocus
' Discard any errors
Err.Clear
End Function
I've also had it try this:
Public Function VerifyFocus(ByRef ctlWithFocus As Control) As Boolean
On Error Resume Next
' Return true if the control currently has the focus
VerifyFocus = Screen.ActiveControl Is ctlWithFocus
' Discard any errors
Err.Clear
End Function
Neither work any more...and I'm floundering.
Well, this turned out to be something utterly unexpected, and totally unrelated to the focus.
Turns out, one of the ways I call this is by getting a control's parent, by using Control.Properties.Parent.Form. While this DOES return the correct form, it also makes the above VerifyFocus routine never return true, ever (even when it's not being used). I don't know why. I really, at this point, don't care. But I'm going to leave it here for others to find.
Refactoring my GetTopForm routines allowed me to get the focus.
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 not sure if this is possible, but I would like to be able to restrict anyone from changing/deleting the current text in a textbox, but allow them to add to it? This way there is no way to accidentally delete data, but if something was wrong, the user can add information/clarification.
I tried using the KeyDown and KeyPress events to do this (something like below):
Private Sub TxtIssueDescription_KeyDown(KeyCode As Integer, Shift As Integer)
If Len(Me.TxtIssueDescription.OldValue) > Len(Me.TxtIssueDescription.Value) Then
Me.TxtIssueDescription.Value = Me.TxtIssueDescription.OldValue
End If
End Sub
But since it doesn't fire after every character that is typed, it doesn't work (ie .OldValue = "Hello" and .Value = "Good Bye" - not right!)
I am really looking for a creative idea to this because I don't think using events will work.
In the On Get Focus event for the Control, check if the Control is Empty.
In pseudocode:
If Control Is Empty Then Exit
Else Set Enabled = No and Locked = Yes
That should do it. If you want to do this for several Controls, just make the above a Procedure that accepts the current Control as a parameter and call the procedure from within the Event Handler. If you make it a Function, then you can just put the function call right in the Property Sheet like this "=LockControl(Me!CurrentControl)". I prefer to use an Event Handler.
Here is how I check for Empty Controls:
Public Function IsEmptyControl(ctl As Variant) As Boolean
' ****************************************************************************
' PURPOSE: Determines if control is empty, that is, either all blanks or null
'
' AUTHOR: Paul Strauss
' Paul#PStrauss.net
'
' DATE: December 31, 2000
'
' NOTES: Returns True if control is empty
'
' CHANGE LOG:
'
' ****************************************************************************
On Error GoTo Err_IsEmptyControl
IsEmptyControl = IsNull(ctl) Or (Len(Trim(ctl)) = 0)
Exit_IsEmptyControl:
Exit Function
Err_IsEmptyControl:
Select Case Err.Number
Case Else
Call ErrHandler(Object:=CodeProject.Name, procedure:="IsEmptyControl", ErrNum:=Err.Number, ErrDesc:=Err.Description)
Resume Exit_IsEmptyControl
End Select
End Function
I have a Userform that has a Checkbox. I am able to check the value of it, but it is always False whether it is checked or not.
Update
This is how the UserForm is being called (This is in another UserForm ):
Private Sub AddOutgoingbtn_Click()
With New AddIncomingForm
.TopBottom.Value = False
.Show
.Repaint
End With
End Sub
End Update
I created a sub to look for a change in the value like:
Sub TopBottom2_Change()
With AddOutgoingForm
If .TopBottom2.Value = True Then TopBottom = True
If .TopBottom2.Value = False Then TopBottom = False
End With
End Sub
But no matter what I do the .TopBottom2.Value is always False.
I've put a breakpoint on the With line so that I know it is hitting this Sub, then I step through it each time. I open the UserForm and check the box, step through and the value is False, then I uncheck the box and step through. The value is still False.
I am not setting the value in any other way with VBA. I am checking the value in the UserForms code, not in any other place.
I have an If in a Calculation Module that is looking at this value for when it is true or falses, but it is always false.
Here are all the Properties of the Checkbox:
With AddOutgoingForm
That's referring to the form's default instance, which may or may not be the instance that's currently being displayed.
You have two options:
At the call site, instead of doing this (or something similar):
With New AddOutgoingForm
.Show
'...
End With
Do this:
AddOutgoingForm.Show
'...
That way you'll be working with the default instance and the checkbox value-check should work.
...but IMO that's a very very bad idea, because then your form contains code that will only ever work when you're showing the default instance.
Leave the call site alone, and NEVER refer to the default instance of a UserForm inside that form's code-behind. In other words change With AddOutgoingForm for With Me.
The Me keyword refers to the current instance - and that is what you want. Doing this will make the form work regardless of what the call site does.
Alternatively, just drop the With block altogether: With Me wouldn't be doing anything useful here.
I will assume the checkbox is indeed on your user form so just use me or you could use
AddOutgoingForm.TopBottom2.Value
But let me ask you, where is TopBottom and what is it a Boolean variable or another Checkbox? Also on the same form? The Subs are also all in the form? You have to be calling the form from somewhere so be careful between the worksheet, workbook, module variables. Is TopBottom a Global variable to the whole project (in a module called Global_Variables with Public in front of it perhaps?) You may not have access to TopBottom from inside of your form if you are not passing anything in or out.
Private Sub TopBottom2_Change()
If (Me.TopBottom2.Value) = True Then
Me.TopBottom.Value = True
Else
Me.TopBottom.Value = False
End If
End Sub
Cheers,
-WWC
Objective: Redirect focus from one command button to another using the first's GotFocus procedure.
Context: I have a form-independent procedure in a generic module that, on most forms, sets focus to the NewRecord button after saving the previous record. But on one form, I would like to redirect (based on certain conditions) focus back to the SignRecord button so the user can "sign" a second part of the same record (I may need this for other uses in the future). The target control is enabled and visible and can otherwise be focused and the original control can be focused when the redirect doesn't occur. Reference [2] below implies that this should be possible, though I'm not changing visibility of my controls.
Issue: When the conditions are met to redirect focus in the GotFocus procedure, it redirects as desired but the original (test) SetFocus call throws a "Run-time error '2110', Can't move focus to the control CommandNew".
What I've tried:
Exit Sub after my downstream SetFocus calls.
Call CommandSign.SetFocus in the hopes that it would make it happen outside the previous SetFocus process.
In a module,
Public Sub test()
Forms("TargetForm").CommandNew.SetFocus 'This gets the error '2110'
End Sub
In the 'TargetForm',
Private Sub CommandNew_GotFocus()
If IsNull(textDateTime) Then Exit Sub 'Works as expected
'I can see these two parts work. The framSign value changes
'and CommandSign gets focus
If checPPC And IsNull(textSigID_PPC) And framSign = 2 Then
framSign = 1
CommandSign.SetFocus
ElseIf checDAS And IsNull(textSigID_DAS) And framSign = 1 Then
framSign = 2
CommandSign.SetFocus
End If
End Sub
References:
[1]: SelectNextControl() a bad idea in a GotFocus event?
[2]: http://www.access-programmers.co.uk/forums/showthread.php?t=100071
I think your problem is that the call to Forms("TargetForm").CommandNew.SetFocus doesn't quite seem to, in fact, finish setting the focus to CommandNew until after Private Sub CommandNew_GotFocus() has finished executing. Because you've called another SetFocus before the first SetFocus could finish, there is a conflict that Access seems to be unable to cope with.
Whether or not that is the case, one thing is clear: the way you have your execution plan set up right now is unfortunately not going to work. You might try adding either a global variable or a public variable to each form that determines whether or not you should set your focus to CommandSign after you set the focus to CommandNew.
Ex. TargetForm:
Public boolSetCommandSignFocusInstead As Boolean
Private Sub CommandNew_GotFocus()
If IsNull(textDateTime) Then Exit Sub 'Works as expected
'I can see these two parts work. The framSign value changes
'and CommandSign gets focus
If checPPC And IsNull(textSigID_PPC) And framSign = 2 Then
framSign = 1
boolSetCommandSignFocusInstead = True
ElseIf checDAS And IsNull(textSigID_DAS) And framSign = 1 Then
framSign = 2
boolSetCommandSignFocusInstead = True
Else
boolSetCommandSignFocusInstead = False
End If
End Sub
Module:
Public Sub test()
Forms("TargetForm").CommandNew.SetFocus
If Forms("TargetForm").boolSetCommandSignFocusInstead Then
Forms("TargetForm").CommandSign.SetFocus
End If
End Sub
I am working in vb.net and I want to exit a validation and return the focus to the textbox with the error.
it raises the error but as soon as I click ok, it continues with the rest of the program. Any idea's? thank you.
Should I also mention that this is in a class.
'number of tickets property
Public Property NumberOfTickets() As String
Get
Return NumTickets
End Get
Set(value As String)
If (String.IsNullOrEmpty(value)) Then
MessageBox.Show("Please Enter Number of Tickets")
Exit Property
Else
NumTickets = value
End If
End Set
End Property
You could use Validating event of your Textbox, and in that, say:
e.Cancel = True
This will prevent user from leaving the TextBox. However, this would also prevent user from closing the form, and performing any other action. Restricting users like that is considered bad practice by some.
Please consider using ErrorProvider instead and just notify user about the errors.
Always do your best to let them finish what they were doing.