I am trying to open a form and move to last record.
I am using the following
DoCmd.RunCommand acCmdRecordsGoToLast
The form open and go to last record, but it is hiding the other records(I must use the scroll bar). This might confuse users that there are no other records.
Is it possible to go to last record and have visible the 10 last records?
It's your lucky day - this is surprisingly non-trivial, and I have written a function for this purpose some time ago.
'---------------------------------------------------------------------------------------
' Procedure : FormGotoEnd
' Author : Andre
' Purpose : Go to the last record of a continuous form, but don't scroll that record to the top
' (as DoCmd.RunCommand acCmdRecordsGoToLast would do).
' Instead scroll up so that the last record is visible at the bottom of the form.
' Parameters: F = the form, can be a subform
' AddtlEmptyRowsBottom = if you want to have room for more than one empty row, for data entry forms
'
' Call this sub e.g. in Form_Load() or in Form_Current of the parent form, like this:
' Call FormGotoEnd(Me)
' or Call FormGotoEnd(Me!SubformControl.Form, 3)
'---------------------------------------------------------------------------------------
'
Public Sub FormGotoEnd(F As Form, Optional AddtlEmptyRowsBottom As Long = 0)
Dim DetailSectionHeight As Long
Dim nVisible As Long
Dim nRecords As Long
On Error GoTo FormGotoEnd_Error
' Calculate height of full details section: Window height minus header+footer
DetailSectionHeight = F.InsideHeight
' Ignore errors if form has no header or footer
On Error Resume Next
If F.Section(acHeader).Visible Then
DetailSectionHeight = DetailSectionHeight - F.Section(acHeader).Height
End If
If F.Section(acFooter).Visible Then
DetailSectionHeight = DetailSectionHeight - F.Section(acFooter).Height
End If
On Error GoTo FormGotoEnd_Error
' Number of visible records in details section
nVisible = CLng(DetailSectionHeight / F.Section(acDetail).Height)
' Nothing to do if the form has no records
If F.RecordsetClone.RecordCount > 0 Then
' For complex record source and/or many records, Access may not know .RecordCount yet
' -> calculate via .MoveLast
F.RecordsetClone.MoveLast
nRecords = F.RecordsetClone.RecordCount
' Nothing to do if all records are visible
If nRecords > nVisible Then
' Move to last record. Use .Bookmark so the subform doesn't need to get focus
F.Bookmark = F.RecordsetClone.Bookmark
' This is the important part!
' Add 2 to AddtlEmptyRowsBottom, in order to see the empty data-entry record plus one empty line
F.SelTop = nRecords - nVisible + 2 + AddtlEmptyRowsBottom
' Make sure the last record is selected
F.Bookmark = F.RecordsetClone.Bookmark
End If
End If
FormGotoEnd_Exit:
On Error GoTo 0
Exit Sub
FormGotoEnd_Error:
MsgBox "Error " & Err.Number & " (" & Err.Description & ") in FormGotoEnd", vbExclamation
Resume FormGotoEnd_Exit
End Sub
Related
MS Access arbitrary version.
I have a continuous form which displays a part of a recordset, based on a query that can be sorted in many ways. Let’s say the form displays n records of the recordset (where n>1). The first row in the form isn’t necessary displaying the first record of the recordset. One of the records (let’s say the record on row x of n) is selected.
After a Form.Requery, the first record of the recordset will always be on the first row of the form. That first record is also selected.
That’s not good. I want to restore the form after a Form.Requery so the selected record is still on the x:th row in the form, provided that this is always possible. Which records that is displayed above and beneath the x:th row is obviously depended on the recordset’s underlying query.
In my case, this could be achieved if there was a way to calculate how many rows there is between the selected row and the first row in the form, before Form.Requery.
That calculation would (independently of which part of the recordset that is displayed) result in 0 if the first row of the form is selected, 1 if the second row of the form is selected and so on.
How do I do that?
Or is there is another way to achieve the same thing?
I use this function - see explanations in comments.
'---------------------------------------------------------------------
' Requery a continuous form, keeping the selected record and scroll position as it is before
' With ideas by Stephen Lebans - http://www.lebans.com/SelectRow.htm
'---------------------------------------------------------------------
Public Sub FormRequeryPosition(F As Form)
On Error GoTo Err_FormRequeryPosition
Dim OrigSelTop As Long
Dim RowsFromTop As Long
Dim OrigCurrentSectionTop As Long
' Must cache the current props because Requery will reset them
OrigSelTop = F.SelTop
OrigCurrentSectionTop = F.CurrentSectionTop
' Turn off screen redraw
F.Painting = False
' Requery the Form
F.Requery
' Calculate how many rows, if any, the selected row was from the top prior to the Requery
' Check if Header is visible or not
If F.Section(acHeader).Visible = True Then
RowsFromTop = (OrigCurrentSectionTop - F.Section(acHeader).Height) / F.Section(acDetail).Height
Else
RowsFromTop = OrigCurrentSectionTop / F.Section(acDetail).Height
End If
' Setting the SelTop property forces this row to appear at the top of the Form.
' We will subtract the number of rows required, if any, so that the original
' current row remains at the original position prior to the Requery.
' First set the current record to the last record.
' This is required due to the method that the Access GUI manages the ScrollBar.
' Prevent error on empty form
If F.RecordsetClone.RecordCount > 0 Then
' With complex record source or many records, Access may not know .RecordCount yet -> use MoveLast to get it
F.RecordsetClone.MoveLast
F.SelTop = F.RecordsetClone.RecordCount
F.SelTop = OrigSelTop - RowsFromTop
DoEvents
F.Painting = True
' Now setfocus back to the original row prior to the Requery
' In unknown conditions this raises a runtime error 3001 "Invalid argument"
On Error Resume Next
F.RecordsetClone.AbsolutePosition = F.CurrentRecord + RowsFromTop - 1
If Err.Number = 0 Then
F.Bookmark = F.RecordsetClone.Bookmark
End If
End If
Exit_FormRequeryPosition:
F.Painting = True
Exit Sub
Err_FormRequeryPosition:
MsgBox "Error " & Err.Number & ": " & Err.Description & vbCrLf & "Line: " & Erl
Resume Exit_FormRequeryPosition
End Sub
if you have in the form any unique column then you can do something like this:
Dim uniqueId : uniqueId = me.uniqueField_Value
' uniqueId hold the current selected unique field value
Form.Requery
With Me.RecordsetClone
' if uniqueId is numeric use next line
.FindFirst "[uniqueField] = " uniqueId
' if uniqueId is string uncomment next line and comment previous line
' .FindFirst "[uniqueField] = " & """" & uniqueId & """"
If Not .NoMatch Then
Me.Bookmark = .Bookmark
End If
End With
I have a word file (Word 2016) with approx. 750 fields. Through a VBA macro I update each field separately (.Fields(i).Update) in order to be able to make a "poor men's" progress bar showing the user the status of the update (how many fields have been updated and how many fields there are in total):
'Select Storyrange (first section)
Dim rngRange as Range
rngRange = ThisDocument.StoryRanges(wdMainTextStory)
Dim intFields as Integer
intFields = rngRange.Fields.Count
'Show Form
UserForm1.Show vbModeless
'Update each field individually
For i = 1 To intFields
'Update Field
rngRange.Fields(i).Update
'Update User Form
UserForm1.Label1.Caption = i & "/" & intFields
Next i
The problem is, that the user form does not get updated in real time. The first few counts work (approx. until i = 20), then there user form doesn't update (approx. until i = 150), after that the update works again. I already tried:
use Repaint and DoEvents
[snip]
'Update User Form
UserForm1.Label1.Caption = i & "/" & intFields
Repaint
DoEvents
Application.ScreenRefresh 'just to be save, since - in my view - ScreenRefresh only effects the Word window, not the user form
[snip]
use a separate sub
[snip]
'Update User Form
updateUserform i, intFields
[snip]
Dim updateUserform(i as Integer, intFields as Integer)
UserForm1.Label1.Caption = i & "/" & intFields
Repaint
DoEvents
End Sub
So my question is: are there any other option to force a user form update?
Please Note: I also posted two other questions within this context:
Update multiple fields with Excel links very slow
How to show the progress of the “Fields.Update”-Method in VBA (Word)
Seems in some case it is a Windows/Office glitch. Restarting Office/Windows solved the problem in some (but not all) cases when I encountered the problem.
This question is a "part 3" of a project I'm working on. After adding multiple labels and textboxes to an Excel userform during runtime using vba and retrieving data from multiple textboxes created during runtime in an Excel userform using vba, I'm now trying to use all that data to select the names in the dropdown boxes to assign work to.
The issue I'm having is, I have the code set up to loop through MyArray(i) from LBound to UBound that gives us the names of the employees, as it does this, it is also looping through an array created by splitting MultFLNAmt that was retrieved from the UserForm so we can determine how many FLNs each employee will receive, then it also loops through to find the name of the current employee selected to assign to. Once all of this is done and everyone has the correct amount of FLNs assigned, it will click the Submit button in the application to finish the assignment.
' Shows and hides the multiple option UserForm
MultipleOptionForm.Show
MultipleOptionForm.Hide
' Creates an array from a comma-delimited
' list of numbers stored in a variable
MFA = Split(MultFLNAmt, ",")
' Activates the application we will be assigning work from
WShell.AppActivate "Non-Keyable Document Management System"
' Table cell node where the dropdown is located
tdNode = 64
a = 1
' Loop through each of the names within the array
For c = LBound(MyArray) + 1 To UBound(MyArray) - 1
' Loop through the array to see how many FLNs each person receives
For b = 1 To MFA(a)
' Loop through to locate the current name of the employee
i = 0
For Each objOption In objIE.Document.GetElementsByTagName("table")(0).GetElementsByTagName("td")(tdNode).GetElementsByClassName("txt_input1")(0).Options
Q(i) = objOption.Text & "-" & objOption.Value
strWQ = Q(i)
' Remove "Selected User" from the list of options
If i = 0 Then
If strWQ = "--Select User---" Then strWQ = ""
Else
' If an option matches the current name selected,
' select that option, then increase the node location
' for the next dropdown box
If InStr(strWQ, MyArray(c)) Then
objOption.Selected = True
objIE.Document.GetElementsByTagName("table")(0).GetElementsByTagName("td")(tdNode).GetElementsByClassName("txt_input1")(0).OnChange
tdNode = tdNode + 23
Else
objOption.Selected = False
End If
End If
Next
i = i + 1
Next
Next
objIE.Document.all.Item("btn_submit1").Click
While the code is working for the most part, where it's failing is, if MFA(a) is 2 or more, only the first dropdown is selected. I put the code in debugging mode and I'm not seeing why 2 or more are not being selected. Any ideas?
After a lot of research, I finally figured out how to get my project to work.
' This line allows for growth/shrinkage of the list of employees
MultipleOptionForm.Height = (UBound(MyArray) - 1) * 20
' This line shows the form
MultipleOptionForm.Show
' This line hides the form after being updated
MultipleOptionForm.Hide
' Creates an array from a comma-delimited
' list of numbers stored in a variable
MFA = Split(MultFLNAmt, ",")
' Activates the application we will be assigning work from
WShell.AppActivate "Non-Keyable Document Management System"
' Table cell node where the dropdown is located
tdNode = 64
' MFA index
a = 1
' Loop through each of the names within the array
For c = LBound(MyArray) + 1 To UBound(MyArray) - 1
' Loop through the array to see how many FLNs each person receives
For b = 1 To MFA(a)
' Starts loop at first drop down
On Error Resume Next
For Each objOption In objIE.Document.GetElementsByTagName("table")(0).GetElementsByTagName("td")(tdNode).GetElementsByClassName("txt_input1")(0).Options
' Stores options within drop down
strWQ = objOption.Text & "-" & objOption.Value
If IsEmpty(strWQ) Then
Exit Sub
End If
' Remove "Selected User" from the list of options
If strWQ = "--Select User---" Then
strWQ = ""
Else
' If there's a match between the drop down for the list
' and the list of assigned FLNs, begin assigning
If InStr(strWQ, MyArray(c)) Then
objOption.Selected = True
objIE.Document.GetElementsByTagName("table")(0).GetElementsByTagName("td")(tdNode).GetElementsByClassName("txt_input1")(0).OnChange
tdNode = tdNode + 23
Exit For
Else
objOption.Selected = False
End If
End If
Next
On Error GoTo 0
Next
Next
Does anyone know how to make a userform function in the same way as the Message Box 'ok' button? I'll explain.
I'm detecting errors in a column in a spreadsheet. When an error is found, a message box pops up as follows:
MsgBox "Please enter valid data"
When I select "OK" it goes to the next error in the column. This is great, except of course a message box is modal, which freezes the application. I want the user to be able to edit the data and then move to the next error. So, I designed a userform, which can be non-modal. Great, except I want the macro to advance to the next error. It will do that IF the user corrects the error. If they do not, it just stays at that error cell.
I know WHY this happens. My userform 'Next' button just calls the macro which finds the first error. But what I want to know is if there is a way around this.
Error checking starts at row 19 because that is where user input data starts.
I'm including a link to the spreadsheet here. Module 1 'NextValidationError' works great and proceeds to the next error. Module 14 just hangs at the error until it is resolved. I'd like it to be able to skip.
https://www.dropbox.com/s/yqko5kj19pnauc9/Transparency%20Data%20Input%20Sheet%20for%20Indirect%20Spend%20V7%2009212016%20v2%200.xlsm?dl=0
Can anyone give me advice on how to make module 14 proceed as module 1?
Something like this:
Dim r_start As Long
Sub CheckNames()
Dim r As Long
'Dim emptyRow As Boolean
If r_start = 0 Then r_start = 19
With ActiveSheet
For r = r_start To 5000
'Checks entire row for data. User may skip rows when entering data.
If WorksheetFunction.CountA(.Range(.Cells(r, 1), .Cells(r, 33))) > 0 Then
If ((.Cells(r, 2) = "") <> (.Cells(r, 3) = "")) Or _
((.Cells(r, 2) = "") = (.Cells(r, 4) = "")) Then
MsgBox "Please fill in First and Last Name or HCO in Row " & r & "."
End If
End If
Next
End With
End Sub
Unless I'm mis-reading your code you can combine your two checks with Or.
You will need some method to reset r_start when the user is done checking (if the form stays open after that).
EDIT: here's a very basic example.
UserForm1 has two buttons - "Next" and "Close"
Code for "next" is just:
Private Sub CommandButton1_Click()
ShowErrors
End Sub
In a regular module:
Dim r_start As Long
'this kicks off the checking process
Sub StartChecking()
r_start = 0
UserForm1.Show vbModeless
ShowErrors
End Sub
'a simple example validation...
Sub ShowErrors()
Dim c As Range, r As Long
If r_start = 0 Then r_start = 9
For r = r_start To 200
With ActiveSheet.Rows(r)
If Not IsNumeric(.Cells(1).Value) Then
UserForm1.lblMsg.Caption = "Cell " & .Cells(1).Address() & " is not numeric!"
r_start = r + 1
Exit Sub
End If
End With
Next r
r_start = 0
UserForm1.lblMsg.Caption = "No more errors"
End Sub
I have 2 pivot tables with powerpivot connection, and I need to propagate selected values from one slicer (connected to table1) to another slicer (connected to table2):
Dim sl As Variant
sl = ActiveWorkbook.SlicerCaches("Slicer_V").VisibleSlicerItemsList
ActiveWorkbook.SlicerCaches("Slicer_V1").VisibleSlicerItemsList = sl
Works perfectly but calls recalculation of table2 every time.
I just want to avoid redundant calculations by adding this check before my code:
if(Slicer_2.SelectedValues = Slicer_1.SelectedValues) then: exit sub
plz advice how to comapare them
Here's code I use to work with slicers:
Sub SetSlicer(ByVal SlicerName As String, ByVal value As String)
If value = "clear" Then
'Clearing takes a while to refresh, so only do if slicer is filtered
If Right(ThisWorkbook.SlicerCaches(SlicerName).VisibleSlicerItemsList(1), 5) <> "[All]" Then
ThisWorkbook.SlicerCaches(SlicerName).ClearManualFilter
End If
Else
'Setting a value takes a while on pivots, so only do if slicer is a different value.
'Note that if more than one value set in slicer, this code could fail to reset a slicer that needs reset. I'll take the chance...
If ThisWorkbook.SlicerCaches(SlicerName).VisibleSlicerItemsList(1) <> value Then
On Error Resume Next
ThisWorkbook.SlicerCaches(SlicerName).VisibleSlicerItemsList = Array(value)
If Err.Number = 5 Then
MsgBox "Error. Confirm that " & SlicerName & " exists.", vbCritical + vbOKOnly
End If
On Error GoTo 0
End If
End If
End Sub
Function GetSlicer(SlicerName As String, Optional item As Integer = 1) As Variant
On Error Resume Next
GetSlicer = ThisWorkbook.SlicerCaches(SlicerName).VisibleSlicerItemsList(item)
On Error GoTo 0
End Function