TextBox.SelStart gives different value for mouse and keyboard - ms-access

I have a TextBox on a Form. I take the cursor position using .SelStart property in that TextBox. It works as required when I click within TextBox. I then use that position to insert certain symbols on cursor position by pressing buttons on the same form which print their captions.
However, if I type some characters with the keyboard in TextBox, the Selstart returns 0. Even though I type several characters and cursor visibly is at the end of text, SelStart remains 0. Now, if I print buttons on other form the new characters get printed always at the start of TextBox which is not what I want. I want the captions to be printed always at cursor location even when I type with keyboard.
This behavior is very puzzling. Can someone help out on this?
Private LastPosition as Long 'declared in form module
Private sub t_LostFocus() 'to obtain last position in `TextBox`
LastPosition = Me!t.SelStart
End Sub
Private Sub Insert()
Dim Text As String
If LastPosition = 0 AND IsNull(Me!t.value) Then
Me!t.Value = " " + Me.ActiveControl.Caption
LastPosition = LastPosition + Len(Me.ActiveControl.Caption) + 1
ElseIf LastPosition >=0 AND Not IsNull(Me!t.Value) Then
Text = Me!t.Value
Me!t.Value = Left(Text, LastPosition) & " " & Me.ActiveControl.Caption & Mid (Text, LastPosition + 1)
LastPosition = LastPosition + Len(Me.ActiveControl.Caption)+1
Else
Me!t.Value = Me!t.Value + " " + Me.ActiveControl.Caption
End If
End Sub
Private Sub button1_Click()
Call Insert
End Sub

First a few facts about data entry on an Access Form. These need to be understood separately to properly explain the behavior described in the question, and especially if one is trying to alter the default behavior of the control.
The TextBox.SelStart, SelLength, and SelText are only available and valid when the control has focus. When a TextBox control receives focus again, the default is for all text to be selected, so that SelStart = 0 and SelLength = length of Text property. When using the mouse and clicking on the TextBox at a particular character position, the default behavior is bypassed and the cursor is placed at the mouse cursor, as expected.
TextBox controls have both a Text property and a Value property. The Text property represents the text string as it is displayed in the control. The displayed text can be different than the underlying value that the control represents, especially if Value is a non-text data type (e.g. an integer is stored as a number, but represented as individual text digits). The Value property returns a VBA variant which itself holds the underlying value of a particular data type.
For a bound control (i.e. the ControlSource property is populated), the Value data type will be the same as the bound source column.
For an unbound control (i.e. ControlSource is blank), the Value data type is dictated by the TextBox.Format property. If Format is blank, then the data type is text and will effectively match the Text property.
Text and Value are not always synced and this is especially true when the control has focus and is being edited. When the text is edited by the form user (i.e. not from code), Value is not updated until the control loses focus or Shift+Enter saves the form (except in cases when the Enter key behavior has been altered). Most events that will update the control will also involve clicking or otherwise moving focus outside the control, like saving the record, changing focus to another control, etc.
When the control’s Value is updated, the displayed text--accessible via the Text property--is interpreted and/or convert into the appropriate data type which is then saved to the Value property. (Sometimes the synchronization continues by reformatting the ‘Value’ back into a representation specified in the Format property. For instance, if Format = Long Date then: Text entered as “4-12-19” --> updated Value: #4/12/19 00:00# → updated Text: “Friday, April 12, 2019”.
One important last fact before I get to the point:
When TextBox.Value property is updated--even if it is also a String data type--the Text property is also refreshed and the cursor position and text selection is reset so that the entire text is selected. In other words, SelStart is set to 0 and SelLength is set to the length of Text, just like the behavior observed when the TextBox newly receives focus (as mentioned in the first point above).
Finally to the crux of all this detail:
When the keyboard is used to alter the text, this will eventually trigger an update, but usually not until the control loses focus. But when such an update occurs, it happens before the LostFocus event and the text selection is reset as described above, so that within the LostFocus event handler, SelStart == 0.
The issue is really not between keyboard and mouse, rather between the control text being altered or unaltered. If one only uses the arrow keys while in the textbox, then the cursor position and text selection are retained in the LostFocus event because a control update has not occurred. Contrariwise, if the mouse is used to alter the text (e.g. right-click Paste), this also triggers an update which will reset the selection. In fact, if one changes the text in any way and then uses the arrow keys or the mouse clicks, an update will still occur and reset the cursor position and text selection.
If the focus is moved outside the textbox and then back in using the mouse, an update may have occurred but the mouse will subsequently set the cursor position. I only mention this to be aware of stray clicks that might unknowing cause an update and still give the illusion that there is unique behavior to the mouse.
For kicks, press Shift+Enter to force an update but retain focus on the control, and observe that all of the text is automatically selected.
It is worth tracing the code by placing some "logging" statements in the various events, so that you can observe when they happen and the order.
Option Explicit
Option Compare Database
Dim LastSelStart As Integer
Dim LastSelLength As Integer
Dim UpdateSelStart As Integer
Dim UpdateSelLength As Integer
Private Sub button1_Click()
Insert
End Sub
Private Sub button2_Click()
Insert
End Sub
Private Sub Form_Load()
LastSelStart = -1
LastSelLength = 0
ResetUpdateSelValues
End Sub
Private Sub ResetUpdateSelValues()
UpdateSelStart = -1
UpdateSelLength = 0
End Sub
Private Sub t_AfterUpdate()
On Error Resume Next
UpdateSelStart = Me.t.SelStart
UpdateSelLength = Me.t.SelLength
If Err.Number <> 0 Then
UpdateSelStart = -1
End If
End Sub
Private Sub t_GotFocus()
On Error Resume Next
If LastSelStart >= 0 Then
Me.t.SelStart = LastSelStart
Me.t.SelLength = LastSelLength
End If
End Sub
Private Sub t_LostFocus()
LastSelStart = Me.t.SelStart
LastSelLength = Me.t.SelLength
If LastSelStart = 0 And UpdateSelStart > 0 Then
LastSelStart = UpdateSelStart
LastSelLength = UpdateSelLength
End If
ResetUpdateSelValues
End Sub
Private Sub Insert()
Dim caption As String
caption = Me.ActiveControl.caption
If IsNull(Me.t.Value) Then
Me.t.Value = caption
LastSelStart = Len(caption)
LastSelLength = 0
Else
Dim Text As String
Text = Me.t.Value
If LastSelStart = 0 Then
'* Don't add extra space at beginning
Text = caption & Mid(Text, LastSelLength + 1)
'Text = caption & Text
LastSelStart = Len(caption)
LastSelLength = 0
ElseIf LastSelStart > 0 Then
Text = Left(Text, LastSelStart) & " " & caption & Mid(Text, LastSelStart + LastSelLength + 1)
'Text = Left(Text, LastSelStart) & " " & caption & Mid(Text, LastSelStart + 0 + 1)
LastSelStart = LastSelStart + 1 + Len(caption)
LastSelLength = 0
Else
'If last cursor position is invalid, append characters
Text = Text & " " & caption
LastSelStart = Len(Text)
LastSelLength = 0
End If
t.Value = Text
End If
Me.t.SetFocus
End Sub

Related

MS Access Multi-control KeyPress CTRL+A Handler

I have an Access database with 10+ text controls. I'd like to have some code to handle the CTRL + A KeyPress event. Normally, when pressing CTRL + A in Access, this selects all records. My end goal is to have CTRL + A only select that control's text (like pressing CTRL + A in your browser's URL bar, it only selects THAT text) so that I can delete only that control's text. I checked this article, as I wanted something that could handle any text box (handling each textbox's KeyPress = 60+ lines of code). Is there any way I could have, say, a for-next loop?
Function HandleKeyPress(frm As Form, KeyAscii As Integer, ByVal e As KeyPressEventArgs) 'should it be a function or a sub?
For Each ctl In Me.Controls
If KeyAscii = 1 Then 'I think this is CTRL + A?
e.Handled = True 'Stop this keypress from doing anything in access
focused_text_box.SelStart = 0
focused_text_box.SelLength = Len(focused_text_box.Text)
End If
Next
End Function
Along with this, how can I pass to this sub/function the text box's name?
Note: In case you haven't noticed yet, I'm still a noob with VBA/Access.
Your current approach will not work, since it contains multiple things that just don't work that way in VBA (as June7 noted), and since form keydown events take priority over textbox keydown events
You can use the following code instead (inspired by this answer on SU):
Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer)
If KeyCode = vbKeyA And Shift = acCtrlMask Then 'Catch Ctrl+A
KeyCode = 0 'Suppress normal effect
On Error GoTo ExitSub 'ActiveControl causes a runtime error if none is active
If TypeOf Me.ActiveControl Is TextBox Then
With Me.ActiveControl
.SelStart = 0
.SelLength = Len(.Text)
End With
End If
End If
ExitSub:
End Sub
It's important to set the Form.KeyPreview property to True, either using VBA or just the property pane, to allow this function to take priority over the default behaviour.

selstart returns position 0 if text is entered in memo field (not clicked)

I have memo field and list. What I want to accomplish is if I am typing something in memo field and then just click on text record in list that the text shows up in memo positioned with the beginning where cursor was.
After research, and googling I succeed to make it. I did it with .selstart property.
But for me it seems that selstart has bug. It works only if I click somewhere in memo (Then everything works great.) But if was typing something, and then click on text in list (without previously clicking in memo field) selstart returns position 0.
This makes me huge problem.
Can anyone help? Thank you.
As you found out, the problem is that the cursor position is lost when you move away from the memo.
This is probably due to the fact that Access form controls are not "real" controls: they are real windows controls only when they have the focus. the rest of the time, they are sort of images of the control pasted onto the form.
So, what you need to do is track the cursor position (and currently selected length of text) during various interractions:
when the user moves the cursor using the keyboard (KeyUp event)
when the user clicks inside the memo (Click event, to position the cursor or select text using the mouse)
when the memo initially gets the focus (GetFocus, the first time, the whole text is selected and the cursor is at position 0)
To test this, I made a small form:
The added the following code to the form:
'----------------------------------------------------------
' Track the position of the cursor in the memo
'----------------------------------------------------------
Private currentPosition As Long
Private currentSelLen As Long
Private Sub txtMemo_Click()
RecordCursorPosition
End Sub
Private Sub txtMemo_GotFocus()
RecordCursorPosition
End Sub
Private Sub txtMemo_KeyUp(KeyCode As Integer, Shift As Integer)
RecordCursorPosition
End Sub
Private Sub RecordCursorPosition()
currentPosition = txtMemo.SelStart
currentSelLen = txtMemo.SelLength
End Sub
'----------------------------------------------------------
' Insert when the user double-click the listbox or press the button
'----------------------------------------------------------
Private Sub listSnippets_DblClick(Cancel As Integer)
InsertText
End Sub
Private Sub btInsert_Click()
InsertText
End Sub
'----------------------------------------------------------
' Do the actual insertion of text
'----------------------------------------------------------
Private Sub InsertText()
If Len(Nz(listSnippets.Value, vbNullString)) = 0 Then Exit Sub
Echo False 'Avoid flickering during update
' Update the Memo content
Dim oldstr As String
oldstr = Nz(txtMemo.Value, vbNullString)
If Len(oldstr) = 0 Then
txtMemo.Value = listSnippets.Value
Else
txtMemo.Value = Left$(oldstr, currentPosition) & _
listSnippets.Value & _
Mid$(oldstr, currentPosition + currentSelLen + 1)
End If
'We will place the cursor after the inserted text
Dim newposition As Long
newposition = currentPosition + Len(listSnippets.Value)
txtMemo.SetFocus
txtMemo.SelStart = newposition
txtMemo.SelLength = 0
currentPosition = newposition
currentSelLen = 0
Echo True
End Sub
I have made a test accdb database that you can download so you can see the details and play around with this.

ms Access runtime error '2115" when setting ListIndex to -1

I have Find Forms that are basically a large list box with some filter, sorting and action buttons and some text boxes to implement the filter.
I have been using these forms for quite a while without problem. The SELECT button is disabled unless an item from the list box is selected. I am trying to unselect any listbox entry when the filtering or sorting is changed.
To do this I set the focus to the listbox (no multi selection option) to -1. There are a number of equivalent actions and I have tried most of them. Once ListIndex is set to -1 I get the '2115' runtime error if I execute a requery action on the listbox or a refresh action on the containing form. In addition, I cannot set the focus to any of the text boxes or buttons on the form getting a variety of runtime errors that say that the field must be saved.
Anybody have an ideas about how this is supposed to function?
In the code snippet the actual code getting the error is the .requery line near the bottom after the Unselect listbox comment
Here is a code snippet of one of the ways I have had this occur:
With Me.lbxFoundItems
If strCurrentSearchText = vbNullString Then 'Nothing to search for
.Visible = False
Call ButtonSettings
Exit Sub
End If
strQry = strSql & strWhere & strOrderBy
If Nz(.RowSource, vbNullString) = vbNullString Then 'First time through
Set qry = Nothing
On Error Resume Next
Set qry = CurrentDb.CreateQueryDef(strQname, strQry)
If qry Is Nothing Then
Set qry = CurrentDb.QueryDefs(strQname)
qry.sql = strQry
End If
colGarbage.Add CurrentDb.QueryDefs(qry.Name), qry.Name
.RowSource = qry.Name
Else
CurrentDb.QueryDefs(qry.Name).sql = strQry
End If
' Unselect the listbox entry
.SetFocus
.Selected(.ListIndex + 1) = False
.Requery
Me.Refresh
Me.tbxListCount = .ListCount - 1
.Visible = True
End With

Clear Textbox on key press

Is there any way to clear the textbox on keypress like in excel.
I tried the following code but it clears the text when clicking on the textbox. I want it to clear it when a certain key is pressed.
Private Sub Text10_GotFocus()
Text10.Value = ""
End Sub
You could select the control's entire text content whenever that control gets focus. Then your keypress would replace the selected text.
If you want that to happen for every text box on every form, you can set "Behavior entering field" setting to "Select entire field". (In Access 2007, find that setting from Office Button -> Access Options -> Advanced, then look under the Editing heading of that dialog. For Access 2003, see this page.)
Not only will that setting be applied to form controls, but also to tables and queries in datasheet view. If that's not what you want, you can use VBA in your form's module to select the text for only specific controls:
Private Sub MyTextBox_GotFocus()
Me.MyTextBox.SelStart = 0
Me.MyTextBox.SelLength = Len(Me.MyTextBox)
End Sub
If you want to do that for multiple controls, you could create a general procedure:
Private Sub SelectWholeField()
Me.ActiveControl.SelStart = 0
Me.ActiveControl.SelLength = Len(Me.ActiveControl)
End Sub
Then call that procedure from the got focus event of an individual control like this:
Private Sub MyTextBox_GotFocus()
SelectWholeField
End Sub
Private Sub Field1_KeyPress(KeyAscii As Integer)
If KeyAscii = 65 Then Me.Field1 = ""
'Or: If Chr(KeyAscii) = "A" Then Me.Field1 = ""
End Sub
change the number to the ascii value of whatever key you are wanting to use to clear the field
Declare a Form Level variable
Private CanDelete as Boolean
When the TextBox receives focus, set CanDelete to True
Private Sub txtTest_GotFocus()
CanDelete = True
End Sub
On the KeyPress event, clear the text if CanDelete is True
Private Sub txtTest_KeyPress(KeyAscii As Integer)
If CanDelete Then
Me.txtTest = ""
CanDelete = False
End If
End Sub

How do I access the selected rows in Access?

I have a form which includes a data sheet. I would like to make it possible for a user to select multiple rows, click on a button and have some sql query run and perform some work on those rows.
Looking through my VBA code, I see how I can access the last selected record using the CurrentRecord property. Yet I don't see how I can know which rows were selected in a multiple selection. (I hope I'm clear...)
What's the standard way of doing this? Access VBA documentation is somewhat obscure on the net...
Thanks!
I used the technique similar to JohnFx
To trap the Selection height before it disappears I used the Exit event of the subform control in the Main form.
So in the Main form:
Private Sub MySubForm_Exit(Cancel As Integer)
With MySubForm.Form
m_SelNumRecs = .SelHeight
m_SelTopRec = .SelTop
m_CurrentRec = .CurrentRecord
End With
End Sub
Here is the code to do it, but there is a catch.
Private Sub Command1_Click()
Dim i As Long
Dim RS As Recordset
Dim F As Form
Set F = Me.sf.Form
Set RS = F.RecordsetClone
If F.SelHeight = 0 Then Exit Sub
' Move to the first selected record.
RS.Move F.SelTop - 1
For i = 1 To F.SelHeight
MsgBox RS![myfield]
RS.MoveNext
Next i
End Sub
Here's the catch:
If the code is added to a button, as soon as the user clicks that button, the selection is lost in the grid (selheight will be zero). So you need to capture that info and save it to a module level variable either with a timer or other events on the form.
Here is an article describing how to work around the catch in some detail.
http://www.mvps.org/access/forms/frm0033.htm
Catch 2: This only works with contiguous selections. They can't select mutliple non-sequential rows in the grid.
Update:
There might be a better event to trap this, but here is a working implementation using the form.timerinterval property that i have tested (at least in Access 2k3, but 2k7 should work just fine)
This code goes in the SUBFORM, use the property to get the selheight value in the master form.
Public m_save_selheight As Integer
Public Property Get save_selheight() As Integer
save_selheight = m_save_selheight
End Property
Private Sub Form_Open(Cancel As Integer)
Me.TimerInterval = 500
End Sub
Private Sub Form_Timer()
m_save_selheight = Me.selheight
End Sub
I've tried doing something like that before, but I never had any success with using a method that required the user to select multiple rows in the same style as a Windows File Dialog box (pressing Ctrl, Shift, etc.).
One method I've used is to use two list boxes. The user can double click on an item in the left list box or click a button when an item is selected, and it will move to the right list box.
Another option is to use a local table that is populated with your source data plus boolean values represented as checkboxes in a subform. After the user selects which data they want by clicking on checkboxes, the user presses a button (or some other event), at which time you go directly to the underlying table of data and query only those rows that were checked. I think this option is the best, though it requires a little bit of code to work properly.
Even in Access, I find sometimes it's easier to work with the tables and queries directly rather than trying to use the built-in tools in Access forms. Sometimes the built-in tools don't do exactly what you want.
A workaround to the selection loss when the sub form loses the focus is to save the selection in the Exit event (as already mentioned by others).
A nice addition is to restore it immediately, using timer, so that the user is still able to see the selection he made.
Note: If you want to use the selection in a button handler, the selection may not be restored already when it executes. Make sure to use the saved values from the variables or add a DoEvents at the beginning of the button handler to let the timer handler execute first.
Dim m_iOperSelLeft As Integer
Dim m_iSelTop As Integer
Dim m_iSelWidth As Integer
Dim m_iSelHeight As Integer
Private Sub MySubForm_Exit(Cancel As Integer)
m_iSelLeft = MySubForm.Form.SelLeft
m_iSelTop = MySubForm.Form.SelTop
m_iSelWidth = MySubForm.Form.SelWidth
m_iSelHeight = MySubForm.Form.SelHeight
TimerInterval = 1
End Sub
Private Sub Form_Timer()
TimerInterval = 0
MySubForm.Form.SelLeft = m_iSelLeft - 1
MySubForm.Form.SelTop = m_iSelTop
MySubForm.Form.SelWidth = m_iSelWidth
MySubForm.Form.SelHeight = m_iSelHeight
End Sub
There is another solution.
The code below will show the number of selected rows as soon as you release the mouse button.
Saving this value will do the trick.
Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
MsgBox Me.SelHeight
End Sub
Use a Global variable in the form, then refer to that in the button code.
Dim g_numSelectedRecords as long
Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
g_numSelectedRecords = Me.SelHeight
End Sub
Dim formRecords As DAO.Recordset
Dim i As Long
Set formRecords = Me.RecordsetClone
' Move to the first record in the recordset.
formRecords.MoveFirst
' Move to the first selected record.
formRecords.Move Me.SelTop - 1
For i = 1 To numSelectedRecords
formRecords.Edit
formRecords.Fields("Archived") = True
formRecords.Update
formRecords.MoveNext
Next i
Why not use an array or recordset and then every time the user clicks on a row (either contiguous or not, save that row or some identifier into the recordset. Then when they click the button on the parent form, simply iterate the recordset that was saved to do what you want. Just don't forget to clear the array or recordset after the button is clicked.?
Another workaround to keeping the selection while attempting to execute a procedure - Instead of leaving the datasheet to activate a button, just use the OnKeyDown event and define a specific keycode and shift combination to execute your code.
The code provided by JohnFx works well. I implemented it without a timer this way (MS-Access 2003):
1- Set the Form's Key Preview to Yes
2- put the code in a function
3- set the event OnKeyUp and OnMouseUp to call the function.
Option Compare Database
Option Explicit
Dim rowSelected() As String
Private Sub Form_Load()
'initialize array
ReDim rowSelected(0, 2)
End Sub
Private Sub Form_Current()
' if cursor place on a different record after a selection was made
' the selection is no longer valid
If "" <> rowSelected(0, 2) Then
If Me.Recordset.AbsolutePosition <> rowSelected(0, 2) Then
rowSelected(0, 0) = ""
rowSelected(0, 1) = ""
rowSelected(0, 2) = ""
End If
End If
End Sub
Private Sub Form_KeyUp(KeyCode As Integer, Shift As Integer)
rowsSelected
If KeyCode = vbKeyDelete And Me.SelHeight > 0 Then
removeRows
End If
End Sub
Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
rowsSelected
End Sub
Sub rowsSelected()
Dim i As Long, rs As DAO.Recordset, selH As Long, selT As Long
selH = Me.SelHeight
selT = Me.SelTop - 1
If selH = 0 Then
ReDim rowSelected(0, 2)
Exit Sub
Else
ReDim rowSelected(selH, 2)
rowSelected(0, 0) = selT
rowSelected(0, 1) = selH
rowSelected(0, 2) = Me.Recordset.AbsolutePosition ' for repositioning
Set rs = Me.RecordsetClone
rs.MoveFirst ' other key touched caused the pointer to shift
rs.Move selT
For i = 1 To selH
rowSelected(i, 0) = rs!PositionNumber
rowSelected(i, 1) = Nz(rs!CurrentMbr)
rowSelected(i, 2) = Nz(rs!FutureMbr)
rs.MoveNext
Next
Set rs = Nothing
Debug.Print selH & " rows selected starting at " & selT
End If
End Sub
Sub removeRows()
' remove rows in underlying table using collected criteria in rowSelected()
Me.Requery
' reposition cursor
End Sub
Private Sub cmdRemRows_Click()
If Val(rowSelected(0, 1)) > 0 Then
removeRows
Else
MsgBox "To remove row(s) select one or more sequential records using the record selector on the left side."
End If
End Sub