Show progress on UserForm without percentage update - vba

I want to show progress while my program is doing everything it does, to know that it is working and not failed. Problem is, I don't use any index, For...Next, While, or anything I can use to call my progress-bar update.
The program:
opens 2 books
copies a bunch of rows from one to another and
refreshes all the data/graphics in many other sheets.
As I don't have a loop through all the rows (which was my main reference point for progress-bar), I'm not sure how I can show the progress.
Sorry if I can't be more specific but I'm not sure if I can share any of the code. If you need more information I could try to post some in here.
My progress-bar code is:
Public Sub UpdateProgressBar(PctDone As Single)
With ProgressForm
' Update the Caption property of the Frame control.
.FrameProgress.Caption = Format(PctDone, "0%")
' Widen the Label control.
.LabelProgress.Width = PctDone * (.FrameProgress.Width - 10)
End With
' The DoEvents allows the UserForm to update.
DoEvents
End Sub
Public Sub UpdateProgressText(txt)
ProgressText.Caption = txt
End Sub

Related

Detect if combobox has been modified in the last Xs [duplicate]

Good morning!
I have a "fancy" search function in Microsoft Access where the list of possible options shrinks as you type in the search field. Unfortunately the computer and server can't keep up with these rapid requeries of the data.
Currently the command to requery with the field in the 'onchange' function of the search box. I'd like to add a delay so it only runs the requery when the search box has not changed for a second. Thus if someone types in a 8 letter word, it isn't running 8 requeries.
The current idea I have for it, which I know there must be something better, is..
"On change, set search box value to X and wait 1 second. After 1 second, if X = search box value, run the requery. An issue is that it would be rapidly rewriting the X value and have a 'wait' command floating for each letter.
Hopefully there's a way to write an event trigger of "When field X has changed, but not changed for the past second."
Thank you!
As requested, here is my current code
'Create a string (text) variable
Dim vSearchString As String
'Populate the string variable with the text entered in the Text Box SearchFor
vSearchString = SearchFor.Text
'Pass the value contained in the string variable to the hidden text box SrchText,
'that is used as the sear4ch criteria for the Query QRY_SearchAll
SrchText = vSearchString
'Requery the List Box to show the latest results for the text entered in Text Box SearchFor
Me.SearchResults.Requery
Me.SearchResults2.Requery
'Tests for a trailing space and exits the sub routine at this point
'so as to preserve the trailing space, which would be lost if focus was shifted from Text Box SearchFor
If Len(Me.SrchText) <> 0 And InStr(Len(SrchText), SrchText, " ", vbTextCompare) Then
'Set the focus on the first item in the list box
Me.SearchResults = Me.SearchResults.ItemData(1)
Me.SearchResults.SetFocus
'Requery the form to refresh the content of any unbound text box that might be feeding off the record source of the List Box
DoCmd.Requery
'Returns the cursor to the the end of the text in Text Box SearchFor,
'and restores trailing space lost when focus is shifted to the list box
Me.SearchFor = vSearchString
Me.SearchFor.SetFocus
Me.SearchFor.SelStart = Me.SearchFor.SelLength
Exit Sub
End If
'Set the focus on the first item in the list box
' Me.SearchResults = Me.SearchResults.ItemData(1)
Me.SearchResults.SetFocus
'Requery the form to refresh the content of any unbound text box that might be feeding off the record source of the List Box
DoCmd.Requery
'Returns the cursor to the the end of the text in Text Box SearchFor
Me.SearchFor.SetFocus
If Not IsNull(Len(Me.SearchFor)) Then
Me.SearchFor.SelStart = Len(Me.SearchFor)
End If
Obviously this is not MY code, it's from somewhere on the interweb. It works fantastic for databases stored locally, but everything is moving to our Sharepoint server which is running on a 386 in a moldy basement powered by a narcoleptic gerbil.
You can simply use the Timer of the current form. No need for a separate form or anything.
Private Sub DoSearch()
' Your current code
' but you should look into removing as many "Requery" from there as possible!
End Sub
Private Sub SearchFor_Change()
' Wait for x Milliseconds until the search is started.
' Each new change restarts the timer interval.
' Use 1000 (1 s) for slow typists or a really slow server
' 200 ms feels right for a normal typist
Me.TimerInterval = 200
End Sub
Private Sub Form_Timer()
' Disable timer (will be enabled by the next SearchFor_Change)
Me.TimerInterval = 0
' Now run the search
DoSearch
End Sub
Note: you may need to move some of the cursor-handling code from DoSearch() to SearchFor_Change(), specifically:
Me.SearchFor.SelStart = Len(Me.SearchFor)
Assign a shortcut key like (Ctrl+ J) to the logic in on change event and call it on demand once you have finished typing search keyword.
Remove on change event.
Create other procedure which has the logic of on change event and assign a shortcut key
Press shortcut to get search suggestion
Other approach
Add below validation to Change event which will check for length of string and will trigger only if length of string is >=8
Private Sub txtSearch_Change()
If Len(Nz(txtSearch.Text, 0)) >= 8 Then
End If
End Sub
I'm going a little outside my comfort area, since I hardly use MS Access forms, but why are you bothering the Server/Database so much? In my experience, each query costs the same amount of time, whether it returns 1 record or 100,000 records.
So even before the user types anything, why don't you just do a single query to return a sorted list. After that, it takes almost no time to use VBA to process the results and find everything in the list that starts with whatever the user types in (it's sorted after all).
Except for the initial load, users who are local to the database or on the other side of the world will experience the same snappy response from your interface.
----------
Like I said, I haven't messed with Access Forms a lot, so this is more of a strict VBA solution. Maybe there is a better way to do it without going outside the Access Forms box that someone could enlighten us with.
You should basically just call LoadItemList when you load the form, or whenever you need to.
Public dbConn As ADODB.Connection
Private ItemList As Variant
Private RecordCount As Long
Sub LoadItemList()
Dim SQL As String
Dim RS As New ADODB.Recordset
SQL = "SELECT T.Name FROM Table T"
Set RS = dbConn.Execute(SQL)
If Not RS.EOF Then
ItemList = RS.GetRows
RecordCount = UBound(ItemList, 2) - LBound(ItemList, 2) + 1
End If
End Sub
Then replace DoCmd.Requery with AddItemtoCombobox SearchResults, SearchFor.Text
Sub AddItemtoCombobox(Control As ComboBox, Filter As String)
Dim Index As Long
Control.Clear
If Not IsEmpty(ItemList) Then
For Index = 0 To RecordCount - 1
If ItemList(Index) Like Filter Then Control.AddItem ItemList(Index)
Next
End If
End Sub
Again, maybe there is a better way that is built into Access...
The technical term that you're looking for is debounce.
What you can do is on your on change event, keep track of the current search string
in terms of pseudocode.
sub onChange()
Form.timerinterval = 0
setSearchString
form.timerinterval = delay
So in terms of the explanation, if your on change is called, disable the timer. Update your search string, then reset the timer to fire after a certain amount of time. The form should be a hidden form that contains the code that you want to execute

Using PreviousControl property with nested subforms and controls

I am creating a custom floating number pad so that tablet users can more easily tap numerical data into a form. I'm using toggle buttons (the idea is to eventually highlight them briefly). The following code for the number pad works on my main form's controls. When I click on the 1 button, then a 1 is put into the active control of the main form
Private Sub Form_Activate()
FocusForm = Application.Screen.PreviousControl.Parent.Name
FocusControl = Application.Screen.PreviousControl.Name
End Sub
Private Sub Toggle1_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
'Add a 1 to the currently selected control
Dim refControl As Control
If FocusForm = "" Then End
Set refControl = Forms(FocusForm).Controls(FocusControl)
If (IsNull(refControl)) Then
refControl = "1"
Else
refControl.Text = refControl.Text & "1"
End If
refControl.SetFocus
End Sub
However, if I navigate into the subform then the Screen.PreviousControl properties return the main form's name and the subform's name, but I can't then seem to be able to refer to the subform control's name as well. Bascially I'm looking for a line that does the following
FocusForm = Application.Screen.PreviousControl.Parent.Name
FocusSubform = Application.Screen.PreviousControl.Name
FocusSubformControl = ?????
Similarly, my subform has its own nested subform and I would like to do the same with this
FocusForm = Application.Screen.PreviousControl.Parent.Name
FocusSubform = Application.Screen.PreviousControl.Name
FocusSubform2 = ?????
FocusSubform2Control = ?????
The in-built floating number pad on the tablets is very fiddly to use, hence my coding of this custom one, however if someone knows where I might find code for a customisable number pad that writes directly into whatever window is active, this would similarly be much appreciated!
If called from a subform,Screen.PreviousControlreturns the name of the mainforms subfom-control (as mainform needs to be active to get the subform control (nested in mainform) active), not the subform (but they can have same name, just change controls name and you won't get subforms name returned, but subforms control name!).
Now how to get previous control of the subform? By its.ActiveControlproperty, as form was active before, its active control must be the previous control. (if you want to get previous control, when you stay on the same subform, you need to store it in a module variable)
As there may be multiple subforms inside of subforms, we just loop while the control is a subform and set it to the next subform.ActiveControltill finished.
Why bother with names, if you can use references? This enables using multiple instances of a form.
Just store the reference to the control in Form_Activate and use it inMouseUp:
'On top of forms code module
Option Explicit
Private PreviousFormControl As Access.Control
Private Sub Form_Activate()
On Error Resume Next ' ignore error if no previous control availible
Set PreviousFormControl = Application.Screen.PreviousControl ' this stores the reference not the name!
If Err.Number <> 0 Then
'Handle error here, e.g. disable controls, show msg, ...
End If
On Error GoTo 0 'Reset error handler (not needed only to remind you not just to ignore all errors!
Do While TypeOf PreviousFormControl Is SubForm ' Loop till control is not subform
Set PreviousFormControl = PreviousFormControl.Form.ActiveControl ' if subform, previous control of it is its current ActiveControl
Loop
End Sub
Private Sub Toggle1_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
'Add a 1 to the currently selected control
PreviousFormControl.Text = PreviousFormControl.Text & "1" ' concat "1" to controls text, no need to handle empty text as it can only be an vbNullString(""), even if Null, then Null & "1" => "" & "1" => "1"
PreviousFormControl.SetFocus
End Sub
however if someone knows where I might find code for a customisable number pad that writes directly into whatever window is active
Of course you can use any Virtual Keyboard running on windows that fit your needs. But then you may have to handle things like bring keyboard to front on wanted position.
numpad emulator was the first open source app I found on quick search, but looks goods on short test. Of course there may be better solutions out there, just search and test.

MS Access: Action "onchange" event after a delay?

Good morning!
I have a "fancy" search function in Microsoft Access where the list of possible options shrinks as you type in the search field. Unfortunately the computer and server can't keep up with these rapid requeries of the data.
Currently the command to requery with the field in the 'onchange' function of the search box. I'd like to add a delay so it only runs the requery when the search box has not changed for a second. Thus if someone types in a 8 letter word, it isn't running 8 requeries.
The current idea I have for it, which I know there must be something better, is..
"On change, set search box value to X and wait 1 second. After 1 second, if X = search box value, run the requery. An issue is that it would be rapidly rewriting the X value and have a 'wait' command floating for each letter.
Hopefully there's a way to write an event trigger of "When field X has changed, but not changed for the past second."
Thank you!
As requested, here is my current code
'Create a string (text) variable
Dim vSearchString As String
'Populate the string variable with the text entered in the Text Box SearchFor
vSearchString = SearchFor.Text
'Pass the value contained in the string variable to the hidden text box SrchText,
'that is used as the sear4ch criteria for the Query QRY_SearchAll
SrchText = vSearchString
'Requery the List Box to show the latest results for the text entered in Text Box SearchFor
Me.SearchResults.Requery
Me.SearchResults2.Requery
'Tests for a trailing space and exits the sub routine at this point
'so as to preserve the trailing space, which would be lost if focus was shifted from Text Box SearchFor
If Len(Me.SrchText) <> 0 And InStr(Len(SrchText), SrchText, " ", vbTextCompare) Then
'Set the focus on the first item in the list box
Me.SearchResults = Me.SearchResults.ItemData(1)
Me.SearchResults.SetFocus
'Requery the form to refresh the content of any unbound text box that might be feeding off the record source of the List Box
DoCmd.Requery
'Returns the cursor to the the end of the text in Text Box SearchFor,
'and restores trailing space lost when focus is shifted to the list box
Me.SearchFor = vSearchString
Me.SearchFor.SetFocus
Me.SearchFor.SelStart = Me.SearchFor.SelLength
Exit Sub
End If
'Set the focus on the first item in the list box
' Me.SearchResults = Me.SearchResults.ItemData(1)
Me.SearchResults.SetFocus
'Requery the form to refresh the content of any unbound text box that might be feeding off the record source of the List Box
DoCmd.Requery
'Returns the cursor to the the end of the text in Text Box SearchFor
Me.SearchFor.SetFocus
If Not IsNull(Len(Me.SearchFor)) Then
Me.SearchFor.SelStart = Len(Me.SearchFor)
End If
Obviously this is not MY code, it's from somewhere on the interweb. It works fantastic for databases stored locally, but everything is moving to our Sharepoint server which is running on a 386 in a moldy basement powered by a narcoleptic gerbil.
You can simply use the Timer of the current form. No need for a separate form or anything.
Private Sub DoSearch()
' Your current code
' but you should look into removing as many "Requery" from there as possible!
End Sub
Private Sub SearchFor_Change()
' Wait for x Milliseconds until the search is started.
' Each new change restarts the timer interval.
' Use 1000 (1 s) for slow typists or a really slow server
' 200 ms feels right for a normal typist
Me.TimerInterval = 200
End Sub
Private Sub Form_Timer()
' Disable timer (will be enabled by the next SearchFor_Change)
Me.TimerInterval = 0
' Now run the search
DoSearch
End Sub
Note: you may need to move some of the cursor-handling code from DoSearch() to SearchFor_Change(), specifically:
Me.SearchFor.SelStart = Len(Me.SearchFor)
Assign a shortcut key like (Ctrl+ J) to the logic in on change event and call it on demand once you have finished typing search keyword.
Remove on change event.
Create other procedure which has the logic of on change event and assign a shortcut key
Press shortcut to get search suggestion
Other approach
Add below validation to Change event which will check for length of string and will trigger only if length of string is >=8
Private Sub txtSearch_Change()
If Len(Nz(txtSearch.Text, 0)) >= 8 Then
End If
End Sub
I'm going a little outside my comfort area, since I hardly use MS Access forms, but why are you bothering the Server/Database so much? In my experience, each query costs the same amount of time, whether it returns 1 record or 100,000 records.
So even before the user types anything, why don't you just do a single query to return a sorted list. After that, it takes almost no time to use VBA to process the results and find everything in the list that starts with whatever the user types in (it's sorted after all).
Except for the initial load, users who are local to the database or on the other side of the world will experience the same snappy response from your interface.
----------
Like I said, I haven't messed with Access Forms a lot, so this is more of a strict VBA solution. Maybe there is a better way to do it without going outside the Access Forms box that someone could enlighten us with.
You should basically just call LoadItemList when you load the form, or whenever you need to.
Public dbConn As ADODB.Connection
Private ItemList As Variant
Private RecordCount As Long
Sub LoadItemList()
Dim SQL As String
Dim RS As New ADODB.Recordset
SQL = "SELECT T.Name FROM Table T"
Set RS = dbConn.Execute(SQL)
If Not RS.EOF Then
ItemList = RS.GetRows
RecordCount = UBound(ItemList, 2) - LBound(ItemList, 2) + 1
End If
End Sub
Then replace DoCmd.Requery with AddItemtoCombobox SearchResults, SearchFor.Text
Sub AddItemtoCombobox(Control As ComboBox, Filter As String)
Dim Index As Long
Control.Clear
If Not IsEmpty(ItemList) Then
For Index = 0 To RecordCount - 1
If ItemList(Index) Like Filter Then Control.AddItem ItemList(Index)
Next
End If
End Sub
Again, maybe there is a better way that is built into Access...
The technical term that you're looking for is debounce.
What you can do is on your on change event, keep track of the current search string
in terms of pseudocode.
sub onChange()
Form.timerinterval = 0
setSearchString
form.timerinterval = delay
So in terms of the explanation, if your on change is called, disable the timer. Update your search string, then reset the timer to fire after a certain amount of time. The form should be a hidden form that contains the code that you want to execute

Charts aren't rendered in MS Access forms

I created a form that contains 5 charts. When opening, the cursor changes 5 times, so obviously the data is queried, but the charts aren't rendered and left blank. However, when moving the form outside of the screen and back in, parts of the chart are shown, so it seems that is just a repaint missing.
I tried to execute me.chart1.repaint on several events, but I haven't found the appropriate event and I am not sure if me.chart1.repaint is actually necessary.
Private Sub chart1_Updated(Code as integer)
me!chart1.Visible = True
End Sub
Don't worry if visible already true!
I am using this code as a workaround:
Private Sub chart1_Updated(Code As Integer)
DoCmd.Minimize
[Forms]![myform].SetFocus
DoCmd.Restore
End Sub
Sometimes the form is doing too many things while rendering the graphs, and the actual painting does not have time to finish before processing moves on the next object. You can force Access to "catch a breath" like this:
Private Sub Detail_Paint()
DoEvents
End Sub
Since the Detail section of the form is doing the actual rendering, it has a chance to finish each object before moving on to the next.

VBA Listbox becomes unresponsive after first use

I have a VBA (Excel 2010) system which involves selecting an item from a listbox and then displaying it in another form. Here is a very simplified version of what happens.
' Part of frmForm1 code module
sub lstListbox_Click
dim MyEvent as string
dim i as integer
i=me.lstListbox.listindex
MyEvent=me.lstlistbox.list(i)
' Now show the item in the second form
Load frmForm2
me.hide
ThisWorkbook.LoadDataIntoForm2 (frmForm2, MyEvent)
frmForm2.show
unload frmForm2
me.show
end sub
The listbox accepts the click, and first the event (the event handler is giver above). Key parts of the event handler are:
Load the second form (to display the detail data)
Pass the second form as a UserForm parameter to a procedure (LoadDataIntoForm2)
Hide the host form (frmForm1) and show the second form (frmForm2)
When the second form processes an Exit click, the code looks like this:
' Part of frmForm2 code module
sub cmdExit_Click
me.hide
end sub
The first time round it works fine - but when I return to frmForm1 (in the tail end of the lstListBox_Click procedure), even though the rest of the form is operative, the listbox remains stubbornly unresponsive.
I've managed to abstract this down to a little demo system if that would help - the same behavior is seen there. (It's regular .xls file, but that seems not to be easily acceptable as an upload)
Has anyone seen this before? And does anyone have any ideas how I might get this to work the way I want it to?
Thanks,
Tony
The default for the .Show method is to make the form modal. Explicitly set it to modeless:
Sub lstListbox_Click
...
Me.Show vbModeless
End Sub