How to navigate in a ListBox by VBA code? - ms-access

I want to insert some buttons for navigation in a access form. The user should be able to use this buttons to navigate up and down in a ListBox. The ListBox is not multiselectable.
This is my Code for the previous Button:
Private Sub btnPrev_Click()
myListBox.SetFocus
myListBox.ListIndex = MyLib.max(0, myListBox.ListIndex - 1)
btnPrev.SetFocus
End Sub
Problems with the code:
I have to set the focus to the ListBox. If not I get the error 7777 (Wrong usage of property ListIndex)
The ListBox must be active, cause SetFocus will not work if inactive.
The ListBox must not be locked
What is best practise, to navigate with vba in a ListBox?

You should not use the .ListIndex property to move around.
Use .ItemsSelected and .Selected instead:
Private Sub btnPrev_Click()
With myListBox
If .ItemsSelected.Count = 0 Then Exit Sub 'Can't move to previous if nothing is selected
Dim currentPosition As Long
currentPosition = .ItemsSelected(0) 'Current position = position of first selected item
If currentPosition = 0 Then Exit Sub 'Can't move lower than 0
.Selected(currentPosition) = False 'Deselect current item
.Selected(currentPosition-1) = True 'Select previous item
End With
End Sub
Deselecting the current item can be omitted when the listbox is not multi-select, since selecting an item deselects all others when that's the case.

Related

Use VBA to enable a text box when a check box is checked

I am new to VBA and I'm trying to create a form in Access where a text box is enabled or disabled based on whether a check box is checked or unchecked.
So if the 'Survey Requested?' box is checked the 'Date Survey Requested' box is enabled for the user to enter a date.
I have the following code:
Private Sub CheckSurveyRequested_AfterUpdate()
If CheckSurveyRequested = True Then
DateSurveyReq.Enabled = True
Else
DateSurveyReq.Enabled = False
End If
End Sub
But this comes up with a '424 Object Required' error when I run line 5.
Anyone have a suggestion as to what I'm doing wrong here?
You should definitely use the AfterUpdate event---tying the behavior of your textbox to the click event means that a user using their keyboard to navigate the form won't get the same behavior.
Also, you should replicate this behavior when the form loads: If the default value for the checkbox is False, then your textbox should be disabled when the form loads.
Also, as #ErikA notes, you can do this with one readable line.
So I would recommend something like this:
Option Explicit
Private Sub chkSurveyRequested_AfterUpdate()
Me.txtDateSurveyReq.Enabled = Me.chkSurveyRequested.value
End Sub
Private Sub Form_Load()
Me.txtDateSurveyReq.Enabled = Me.chkSurveyRequested.value
End Sub
In order to not repeat yourself, you could move this code into a separate sub:
Option Explicit
Private Sub Form_Load()
' assign the function below to the AfterUpdate event of the checkbox.
Me.chkSurveyRequested.AfterUpdate = "=UpdateControls()"
' now execute the function directly
UpdateControls
End Sub
Private Function UpdateControls()
Me.txtDateSurveyReq.Enabled = Me.chkSurveyRequested.value
End Function
I would suggest the following -
Private Sub CheckSurveyRequested_AfterUpdate()
DateSurveyReq.Enabled = CheckSurveyRequested
End Sub

How to return the name of a form's highest parent

I have subform and sometimes it is opend with a parent form, and sometimes it is opened with a parent and a grandparent forms.
How can I find the name of the highest current parent of the subform?
I recommend using events and not relying on a string of object hierarchies. So subform decides it needs to be closed and raises a CloseRequested event. Then whatever form opened subform can act on that. That action could either be to attempt to close itself (if it succeeds then great, it was the parent) or pass it along the chain.
This example below doesn't use events but will work to close the parent-est form when a button is clicked on the subform.
'command button on your subform
Private Sub Command0_Click()
Dim frm As Form
Set frm = FindHighestAncestor(Me)
DoCmd.Close acForm, frm.Name
End Sub
Public Function FindHighestAncestor(frm As Form)
If IsHighestLevelForm(frm) Then
Set FindHighestAncestor = frm
Else
If TypeOf frm.Parent Is Form Then
Set FindHighestAncestor = FindHighestAncestor(frm.Parent)
Else
Set FindHighestAncestor = frm
End If
End If
End Function
Public Function IsHighestLevelForm(frm As Form) As Boolean
Dim f As Form
For Each f In Application.Forms
If f.Name = frm.Name Then
IsHighestLevelForm = True
Exit Function
End If
Next
IsHighestLevelForm = False
End Function
If that is your only two scenarios you can do two things. Check if Mainform is open or check the Parent property of Subform1.

MS Access VBA to set scrollbar position of a subform that is a continuous form

I have a form with a subform. The subform is a continuous form so I can use conditional formatting. Using controls in a Tab Control, the values of the currently selected record on the subform are changed. So, I requery the subform to update the subform continuous form to show the updated data.
I can get the correct record re-selected in the subform, but the position of that record in the list jumps to the top of the subform's list instead of maintaining the position it was in prior to the update.
I have tried playing with the CurrentSectionTop values of the subform, but I am not able to correctly maintain the user's selection position within the subform after requerying the subform.
Is there some way to get the current position of the subform's continuous form's scrollbar position and then set that position in code after the continuous form is requeried? (Stephen Lebans' code for doing this (see: http://www.lebans.com/SelectRow.htm) does not work for me because I'm using Access 2013 and his code won't convert to Access 2013).
Here's a sample of what the subform continuous form display might look like to begin with while Record 7 is the current record selected:
{start of continuous form view}
[ ] Record 3 in continuous form view
[ ] Record 4 in continuous form view
[ ] Record 5 in continuous form view
[ ] Record 6 in continuous form view
[>] Record 7 in continuous form view
[ ] Record 8 in continuous form view
[ ] Record 9 in continuous form view
{end of continuous form view}
{tab control displays down here below the continuous form subform}
After the subform is requeried, here is what the subform continuous form display looks like, but I want the display to look the same as above; the display should not put Record 7 as the top record in the continuous form view since it was the 5th record down in the view originally so I want it to be the 5th record down after the requery:
{start of continuous form view}
[>] Record 7 in continuous form view
[ ] Record 8 in continuous form view
[ ] Record 9 in continuous form view
[ ] Record 10 in continuous form view
[ ] Record 11 in continuous form view
[ ] Record 12 in continuous form view
[ ] Record 13 in continuous form view
{end of continuous form view}
{tab control displays down here below the continuous form subform}
I couldn't get Wayne G Dunn's solution working, but I came up with this alternative. It's not wonderful, but it worked for me.
The basic idea is that each record in the continuous form has a position (ie top record showing on the screen is in position 1, regardless which actual record it is). You have a table that relates those positions, to the Form.currentSectionTop property of each record in the form, so you can figure out what position the current record is in. Then it's relatively straightforward to return to that position after the requery.
Create the table with the positions - this needs to run on startup or somewhere - might need to be more frequent if the user can resize or anything might change the number of records that could be shown in the continuous form.
Public Sub Setup_Positions()
Dim sql As String
Dim Position As Long
Dim currentSectionTop As Long
Dim lastSectionTop As Long
sql = "DELETE FROM tblRecordPosition"
currentdb.execute sql
DoCmd.GoToRecord , , acFirst
Position = 1
Call Set_NoUpdate
With Forms("frmMain").Controls("frmContinuousSubForm").Form
currentSectionTop = .currentSectionTop
Do While currentSectionTop <> lastSectionTop
'record previous sectiontop
lastSectionTop = currentSectionTop
'write it into the table
sql = "INSERT INTO tblRecordPosition (Position, CurrentSectionTop) " & _
"SELECT " & Position & ", " & _
currentSectionTop
CurrentDb.Execute sql
'update to next position and record the 'last' one, move to next record. When we've run out of visible ones, the last and current will be the same.
Position = Position + 1
DoCmd.GoToRecord , , acNext
'get new current sectiontop
currentSectionTop = .currentSectionTop
Loop
End With
Call Set_NoUpdateOff
End Sub
Set up global variables and a couple of functions to maintain them. The 'NoUpdateRequired' variable is optional - I use it to prevent unnecessary stuff running all the time.
Public NoUpdateRequired As Boolean
Public Position As Long
Public Sub Set_NoUpdate()
NoUpdateRequired = True
End Sub
Public Sub Set_NoUpdateOff()
NoUpdateRequired = False
End Sub
Create this function to convert between the property you can measure, and the actual position:
Public Function Get_Position(Optional InputCurrentSectionTop As Long) As Long
Dim currentSectionTop As Long
Dim Position As Long
If InputCurrentSectionTop > 0 Then
currentSectionTop = InputCurrentSectionTop
Else
currentSectionTop = Forms("frmMain").Controls("frmContinuousSubForm").Form.currentSectionTop
End If
Position = Nz(ELookup("Position", "tblRecordPosition", "CurrentSectionTop = " & currentSectionTop), 0)
Get_Position = Position
End Function
In the current event of the continuous form, you need this:
Private Sub Form_Current()
If NoUpdateRequired = False Then
Position = Get_Position
End If
End Sub
And finally, in the bit where you want your refresh to happen, you need this:
Public Sub Refresh_ContinuousSubForm()
'All this problem goes away if you can use Refresh instead of Requery, but if you have a few things editting the underlying table, you must use requery to avoid 'another user has changed the data' errors.
'However, this then causes the form to jump
'back to the first record instead of keeping the current record selected. To get around this, the following has been employed:
'the .seltop property allows you to select the top selected record (in most cases, only one record is selected). This is recorded before the refresh, and
'the form set back to that after the refresh. However, this puts the selected record at the top of the screen - confusing when you're working down a list.
'The .currentSectionTop property measures the number of twips from the selected record to the top of the screen - and correlates to which position in the list
'of 25 records in the bottom pane. tblRecordPosition converts between the twips to the actual position (recorded when the database is opened).
'The key to all this is that going back to the right record using .seltop only puts the record at the top of the screen IF the record wasn't already visible on the screen.
'But GoToRecord, if used when you're already at the top of the screen, will push the records down the screen as you move backward (upward) through them.
'So we go to the right record, and it will probably be at the top of the screen because of the requery. Then we push them down the screen back to the original position
'using GoToRecord, but now we're on the wrong record. Then we return to the right record using .seltop, and because it's already on the screen, it won't move position.
Dim startSeltop As Long
Dim newSectionTop As Long
Dim newPosition As Long
Dim startPosition As Long
Dim recordsToMove As Long
'Also global variable Position (long) which is recorded in the form's current event
Call Set_NoUpdate
startPosition = Position
With Forms("frmMain").Controls("frmContinuousSubForm").Form
.Painting = False 'stops the screen flickering between
startSeltop = .SelTop 'records which record we're on. Position represents where that was showing on the screen.
.Requery 'does the requery
.SelTop = startSeltop 'sets us back to the correct record
newSectionTop = .currentSectionTop 'measures in twips which position it's in (usually 1)
newPosition = Get_Position(newSectionTop) 'converts that to the position
recordsToMove = startPosition - newPosition 'calculates how many records to move - moving records using GoToRecord moves the position as well
If recordsToMove > 0 Then
DoCmd.GoToRecord , , acPrevious, recordsToMove 'moves back enough records to push our record to the right place on the screen
End If
.SelTop = startSeltop 'now sets back to the correct record
.Painting = True 'turns the screen painting back on
End With
Call Set_NoUpdateOff
End Sub
The following code is a subset of the code found on Stephen Lebans' website: http://www.lebans.com/SelectRow.htm . That link has a link to a zipped version of an Access database with all the code to handle multiple scenarios, however the database is an older version and needs to be converted. Mr Leban's code does far more than what is included here, but I am only using this code to solve one specific issue.
(A) Create a Class Module named 'clsSetRow' and paste in the following code:
Option Compare Database
Option Explicit
Private mSelTop As Long
Private mCurrentSectionTop As Long
Public Property Get SelTop() As Long
SelTop = mSelTop
End Property
Public Property Let SelTop(x As Long)
mSelTop = x
End Property
Public Property Get CurrentSectionTop() As Long
CurrentSectionTop = mCurrentSectionTop
End Property
Public Property Let CurrentSectionTop(x As Long)
mCurrentSectionTop = x
End Property
(B) In your module for your form, include the following at the top:
Private SR As clsSetRow
Dim lCurRec As Long
(C) Add the following Event Handlers and code:
Private Sub Form_Load()
Set SR = New clsSetRow
End Sub
Private Sub Form_Current()
' This event can be called during the Form Load event prior to the init of
' our class so we must test for this.
If Not SR Is Nothing Then
SR.SelTop = Me.SelTop
SR.CurrentSectionTop = Me.CurrentSectionTop
End If
End Sub
Private Sub Form_AfterInsert() ' OR JUST USE THE BEFOREINSERT
lCurRec = Me.CurrentRecord
'Debug.Print "After Insert, Current: " & Me.CurrentRecord
End Sub
Private Sub Form_BeforeInsert(Cancel As Integer) ' OR JUST USE THE AFTERINSERT
lCurRec = Me.CurrentRecord
'Debug.Print "Before Insert, Current: " & Me.CurrentRecord
End Sub
(D) Wherever you want to reposition (i.e. after a REQUERY), add the following line of code:
DoCmd.GoToRecord acDataForm, Me.Name, acGoTo, lCurRec
(E) To test this, just add a command button that will 'Requery and then GoToRecord'.
NOTE: Simply scrolling up or down using the scrollbar will NOT save the row of where you are! You need to establish a 'current record' for this to reposition.
Good Luck! And thank you Stephen Lebans for the code!

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