Suspend VBA macro execution until calculations are finished - vba

I need to suspend macro execution until all calculations are finished.
I tried using loops with and without DoEvents checking CalculationState but the loop never ends.
Do Until Application.CalculationState = xlDone
DoEvents
Loop

Have you looked at handling the application's AfterCalculate() event (see https://learn.microsoft.com/en-us/office/vba/api/excel.application.aftercalculate)?
You need to create a class which contains the Application object in order to access the handler. You can then call whatever routine you wish from there. If you had more than one routine to call depending on what is being calculated you could set, say, an enum to point to the right procedure. I've called this class cApp and skeleton code for that would be:
Option Explicit
Public Enum ProcAferCalcCode
None
DeliveryProc
TimeProc
End Enum
Private WithEvents mApp As Application
Private mProcAfterCalcCode As ProcAferCalcCode
Public Property Let ProcAfterCalc(RHS As ProcAferCalcCode)
mProcAfterCalcCode = RHS
End Property
Private Sub Class_Initialize()
mProcAfterCalcCode = None
Set mApp = Application
End Sub
Private Sub mApp_AfterCalculate()
Select Case mProcAfterCalcCode
Case DeliveryProc
SetDeliveryOptions
Case TimeProc
SetTime
End Select
End Sub
In this example, I have a one-row table that looks like this:
When the user enters a quantity, and the 'Price' cell calculates (A * B), a routine is called that populates the validation list in the 'Delivery' column. When a delivery option is selected, and the 'Cost' cell calculates (A * D), a routine is called that retrieves delivery times. It's a trite example, but should give you an idea on how to code it.
Code in a module looks like this:
Option Explicit
Private mApp As cApp
Public Sub RunMe()
Debug.Print "RunMe() called..."
If MsgBox("Ready to enter qty?", vbYesNo) = vbYes Then
Debug.Print "Some user action confirmed."
Set mApp = New cApp
mApp.ProcAfterCalc = DeliveryProc
End If
Debug.Print "RunMe() ended."
Debug.Print "** No procedure is running **" & vbNewLine
End Sub
Public Sub SetDeliveryOptions()
Dim cell As Range
Dim del As String
Debug.Print "SetDeliveryOptions() called..."
mApp.ProcAfterCalc = None
Set cell = Sheet1.ListObjects("Table1").ListColumns("Price").DataBodyRange
Debug.Print "Price is " & cell.Value2
'Mimic some task.
Select Case cell.Value2
Case 0
del = vbNullString
Case Is < 5
del = "$5 - Standard"
Case Is < 10
del = "$5 - Standard, $6 - Express"
Case Else
del = "$5 - Standard, $6 - Express, $7 - Next Day"
End Select
With cell.Offset(, 1)
.Value = Empty
With .Validation
.Delete
.Add xlValidateList, xlValidAlertStop, xlBetween, del
.IgnoreBlank = True
.InCellDropdown = True
.InputTitle = ""
.ErrorTitle = ""
.InputMessage = ""
.ErrorMessage = ""
.ShowInput = True
.ShowError = True
End With
End With
mApp.ProcAfterCalc = TimeProc
Debug.Print "SetDeliveryOptions() ended."
Debug.Print "** No procedure is running **" & vbNewLine
End Sub
Public Sub SetTime()
Dim cell As Range
Dim d As Long
Debug.Print "SetTime() called..."
mApp.ProcAfterCalc = None
Set cell = Sheet1.ListObjects("Table1").ListColumns("Delivery").DataBodyRange
Debug.Print "Delivery Option is " & cell.Value
'Mimic some other task.
Select Case cell.Value2
Case Is = "$5 - Standard"
d = Int((10 - 5 + 1) * Rnd + 5)
Case Is = "$6 - Express"
d = Int((5 - 2 + 1) * Rnd + 2)
Case Is = "$7 - Next Day"
d = 1
Case Else
d = 0
End Select
cell.Offset(, 1) = d
Debug.Print "SetTime() ended."
Debug.Print "** No procedure is running **" & vbNewLine
End Sub
The immediate window outputs the following:
RunMe() called...
Some user action confirmed.
RunMe() ended.
** No procedure is running **
SetDeliveryOptions() called...
Price is 25
SetDeliveryOptions() ended.
** No procedure is running **
SetTime() called...
Delivery Option is $7 - Next Day
SetTime() ended.
** No procedure is running **

What state is it in?
List item xlCalculating 1 Calculations in process.
List item xlDone 0 Calculations complete.
List item xlPending 2 Changes that trigger calculation have been made, but a recalculation has not yet been performed.
It might help you determine what is happening on your worksheets.

Related

Get focus on unbound textbox when form returns no records

I'm a little stumped.
I've got an MS Access front end application for an SQL Server back end. I have an orders form with a list box that, when selected and a "Notes" button is clicked will open another form of notes. This is a continuous form and has a data source (linked table - a view) from the back end database.
When the notes button is clicked in the main orders form, it passes a filter and an OpenArgs string to the Notes form in this code:
Private Sub cmdItemNotes_Click()
Dim i As Integer
Dim ordLine As Boolean
Dim line As Integer
Dim args As String
If Me.lstOrders.ItemsSelected.count = 1 Then
ordLine = False
With Me.lstOrders
For i = 0 To .ListCount - 1
If .selected(i) Then
If .Column(16, i) = "Orders" Then
ordLine = True
line = .Column(0, i)
End If
End If
Next i
End With
If ordLine Then
args = "txtLineID|" & line & "|txtCurrentUser|" & DLookup("[User]", "tblUsers", "[Current] = -1") & "|txtSortNum|" & _
Nz(DMax("[SortNum]", "dbo_vwInvoiceItemNotesAll", "[LineID] = " & line), 0) + 1 & "|"
DoCmd.OpenForm "frmInvoiceItemNotes", , , "LineID = " & line, , , args
Else
'Potting order notes
End If
Else: MsgBox "Please select one item for notes."
End If
Here is my On Load code for the Notes form:
Private Sub Form_Load()
Dim numPipes As Integer
Dim ArgStr As String
Dim ctl As control
Dim ctlNam As String
Dim val As String
Dim i As Integer
ArgStr = Me.OpenArgs
numPipes = Len(ArgStr) - Len(Replace(ArgStr, "|", ""))
For i = 1 To (numPipes / 2)
ctlNam = Left(ArgStr, InStr(ArgStr, "|") - 1)
Set ctl = Me.Controls(ctlNam)
ArgStr = Right(ArgStr, Len(ArgStr) - (Len(ctlNam) + 1))
val = Left(ArgStr, InStr(ArgStr, "|") - 1)
ctl.Value = val
ArgStr = Right(ArgStr, Len(ArgStr) - (Len(val) + 1))
Next i
End Sub
This code executes fine. The form gets filtered to only see the records (notes) for the line selected back in the orders form.
Because this is editing a table in the back end, I use stored procedures in a pass through query to update the table, not bound controls. The bound controls in the continuous form are for displaying current records only. So... I have an unbound textbox (txtNewNote) in the footer of the form to type a new note, edit an existing note, or post a reply to an existing note.
As stated above, the form filters on load. Everything works great when records show. But when it filters to no records, the txtNewNote textbox behaves quite differently. For instance, I have a combo box to mention other users. Here is the code after update for the combo box:
Private Sub cmbMention_AfterUpdate()
Dim ment As String
If Me.txtNewNote = Mid(Me.txtNewNote.DefaultValue, 2, Len(Me.txtNewNote.DefaultValue) - 2) Then
Me.txtNewNote.Value = ""
End If
If Not IsNull(Me.cmbMention) Then
ment = " #" & Me.cmbMention & " "
If Not InStr(Me.txtNewNote, ment) > 0 Then
Me.txtNewNote = Me.txtNewNote & ment
End If
End If
With Me.txtNewNote
.SetFocus
.SelStart = Len(Nz(Me.txtNewNote, ""))
End With
End Sub
The problem occurs with the line
.SelStart = Len(Nz(Me.txtNewNote, ""))
When there are records to display, it works. When there are no records to display, it throws the Run-time error 2185 "You can't reference a property or method for a control unless the control has the focus."
Ironically, if I omit this line and make the .SetFocus the last line of code in the sub, the control is in focus with the entire text highlighted.
Why would an unbound textbox behave this way just because the filter does not show records?
Thanks!

Passing Values in VBA

In the code I am posting, I am using a check box called "ACDS Test" and whenever it is checked it creates a sheet, then when it becomes unchecked it calls the upper function and deletes the sheet.
I am trying to add a message box that essentially works like a fail safe to ensure they want to delete the page. If they say they do not want to delete the page then I want the checkbox to stay checked.
For some reason I am getting this error message when I try to pass the value to make sure the checkbox stays checked and I cannot figure out why.
The error comes up on the line:
Sub ACDSTest_Click(CorrectValue As Integer)
And the specific error is: "Compile error: Procedure Declaration does not match description of event or procedure having the same name".
Any help is much appreciated! IF any more clarification is needed please feel free to ask!
Sub DeleteWorksheet(NameSheet As String)
Dim Ans As Long
Dim t As String
Dim CorrectValue As Integer
Dim i As Long, k As Long
k = Sheets.Count
Ans = MsgBox("Would you like to take this test off of the form?", vbYesNo)
Select Case Ans
Case vbYes
'Code reads through each page and finds one with corresponding name to string t
'Once it finds the correct page, it deletes it
For i = k To 1 Step -1
t = Sheets(i).Name
If t = NameSheet Then
Sheets(i).Delete
End If
Next i
CorrectValue = 0
Case vbNo
CorrectValue = 1
End Select
End Sub
Sub ACDSTest_Click(CorrectValue As Integer)
Dim NameSheet As String
Dim NameValue As String
NameSheet = "ACDS"
NameValue = "ACDS Test"
If ACDSTest.Value = True Then
CreateWorksheet (NameSheet), (NameValue)
Worksheets("Sheet1").Activate
Else
DeleteWorksheet (NameSheet)
If CorrectValue = 1 Then
ActiveSheet.Shapes("ACDS Test").ControlFormat.Value = 1
End If
End If
End Sub
The issue here is that the CorrectValue variable as you define it in DeleteWorksheet does not exist in the context of the
variable does not exist in context of the ACDSTest_Click subroutine. This is because variables defined within subroutines or functions are local to those functions. To correct this I would convert DeleteWorksheet to a function such as the below.
Further, the event that fires Private Sub ACDSTest_Click() cannot handle passing a value to that function, so changing it to Sub ACDSTest_Click(CorrectValue As Integer) causes an error.
Function DeleteWorksheet(ByVal SheetName As String) As Boolean
On Error GoTo SheetDNE
SheetName = Sheets(SheetName).Name 'Check if sheet exists w/o other objects
On Error GoTo 0
Select Case MsgBox("Would you like to take this test off of the form?", vbYesNo)
Case vbYes
Application.DisplayAlerts = False
Sheets(SheetName).Delete
Application.DisplayAlerts = True
DeleteWorksheet = True
Case Else: DeleteWorksheet = False
End Select
Exit Function 'Exit The Function w/o error
SheetDNE: 'Sheet Does Not Exist
MsgBox "The indicated sheet, " & SheetName & ", does not exist", vbOKOnly
End Function
And
Private Sub ACDSTest_Click()
Dim NameSheet As String
Dim NameValue As String
NameSheet = "ACDS"
NameValue = "ACDS Test"
If ACDSTest.Value = True Then
CreateWorksheet (NameSheet), (NameValue)
Worksheets("Sheet1").Activate
Else
If Not DeleteWorksheet(NameSheet) Then _
ActiveSheet.Shapes("ACDS Test").ControlFormat.Value = 1
End If
End Sub

Include loop counter in object name [VBA]

Basically I wrote a code, which is to be used in userform. The thing is that userform is created by other macro (amount of checkboxes differs, depends how many words string strNamn contains, that is why userform must be created by macro).
I would like to, somehow, include loop counter in the line:
If UserForm1.CheckBox0.Value = True Then
to make it like this:
If UserForm1.CheckBox(i).Value = True Then
But it obviously doesn't work like this :(
Any suggestion how to declare checkbox to include the counter in the line?
Code in UserForm1 to execute macro looks like:
Private Sub cmd_1_Click()
Call clicker
End Sub
Macro code:
Sub clicker()
Dim strNamnOK As String
Dim strNamn As String
Dim strNamnA() As String
strNamn = "one, two, three, four"
strNamnA = Split(strNamn, ", ")
Dim intAmount As Integer
intAmount = UBound(strNamnA)
strNamnOK = ""
For i = 0 To intAmount
If UserForm1.CheckBox0.Value = True Then
strNamnOK = strNamnOK & " " & strNamnA(i)
End If
Next
strNamnOK = Left(strNamnOK, 12)
MsgBox strNamnOK
End Sub

Every 5 seconds record the value in 2 cells in another worksheet VBA

Problem:
I have searched extensively for this and cannot seem to get it to work. I have a timer running when the "StartBtn" is pressed:
Dim StopTimer As Boolean
Dim SchdTime As Date
Dim Etime As Date
Dim currentcost As Integer
Const OneSec As Date = 1 / 86400#
Private Sub ResetBtn_Click()
StopTimer = True
Etime = 0
[TextBox21].Value = "00:00:00"
End Sub
Private Sub StartBtn_Click()
StopTimer = False
SchdTime = Now()
[TextBox21].Value = Format(Etime, "hh:mm:ss")
Application.OnTime SchdTime + OneSec, "Sheet1.NextTick"
End Sub
Private Sub StopBtn_Click()
StopTimer = True
Beep
End Sub
Sub NextTick()
If StopTimer Then
'Don't reschedule update
Else
[TextBox21].Value = Format(Etime, "hh:mm:ss")
SchdTime = SchdTime + OneSec
Application.OnTime SchdTime, "Sheet1.NextTick"
Etime = Etime + OneSec
End If
End Sub
Then in another cell (say, C16) I have a manually entered value which is the hourly cost rate. I have a third cell that is calculating total cost by C16*current timer value.
What I want to do is record every 5 seconds after the "StartBtn" is clicked the current time and current calculated cost in another sheet. This is what I have started:
Sub increment()
Dim x As String
Dim n As Integer
Dim Recordnext As Date
n = 0
Record = [TextBox21].Value
Recordnext = [TextBox21].Value + OneSec
Range("B13").Value = Recordnext
Do Until IsEmpty(B4)
If [TextBox21].Value = Recordnext Then ActiveCell.Copy
Application.Goto(ActiveWorkbook.Sheets("Sheet2").Range("A1").Offset(1, 0))
ActiveSheet.Paste
Application.CutCopyMode = False
n = n + 1
Recordnext = [TextBox21].Value + 5 * (OneSec)
Exit Do
End If
ActiveCell.Offset(1, 0).Select
Loop
End Sub
But it doesnt work. Any help would be appreciated.
I have tried to simplify your timer method down to what is actually needed.
Sheet1 code sheet
Option Explicit
Private Sub ResetBtn_Click()
bRun_Timer = False
'use the following if you want to remove the last time cycle
TextBox21.Value = Format(0, "hh:mm:ss")
End Sub
Private Sub StartBtn_Click()
bRun_Timer = True
dTime_Start = Now
TextBox21.Value = Format(Now - dTime_Start, "hh:mm:ss")
Range("D16").ClearContents
Call next_Increment
End Sub
Module1 code sheet
Option Explicit
Public bRun_Timer As Boolean
Public Const iSecs As Integer = 3 'three seconds
Public dTime_Start As Date
Sub next_Increment()
With Worksheets("Sheet1")
.TextBox21.Value = Format(Now - dTime_Start, "hh:mm:ss")
.Range("D16") = Sheet1.Range("C16") / 3600 * _
Second(TimeValue(Sheet1.TextBox21.Value)) '# of secs × rate/sec
Worksheets("Sheet2").Cells(Rows.Count, 1).End(xlUp).Resize(1, 2).Offset(1, 0) = _
Array(.TextBox21.Value, .Range("D16").Value)
End With
If bRun_Timer Then _
Application.OnTime Now + TimeSerial(0, 0, iSecs), "next_Increment"
End Sub
Note that the operation of transferring the data to Sheet2 is a direct value transfer¹ with no .GoTo, ActiveCell or Select.
It was not entirely clear to me what you were trying to do with the value transfer. I have stacked them one after another on Sheet1.
        
You would benefit by adding Option Explicit² to the top of all your code sheets. This requires variable declaration and if you misplace a public variable, you will quickly know.
¹ See How to avoid using Select in Excel VBA macros for more methods on getting away from relying on select and activate to accomplish your goals.
² Setting Require Variable Declaration within the VBE's Tools ► Options ► Editor property page will put the Option Explicit statement at the top of each newly created code sheet. This will avoid silly coding mistakes like misspellings as well as influencing you to use the correct variable type in the variable declaration. Variables created on-the-fly without declaration are all of the variant/object type. Using Option Explicit is widely considered 'best practice'.

"Compile error: Block If Without End If"

First, I have used my eyes and Ctrl+F and another person's eyes to check for End If, I do not see why I get this error message:
Compile error: Block If Without End If
I have tried to indent macro correctly, hope it is clear to read.
This is the entire function since I am not sure where the error message is. The compiler selects the End Sub at the very end of this function.
Sub settings()
' Goal of this function: to get user-desired settings and request permission to clear sheet
' could write these settings to text file and create profiles so that user can skip entering all this every time?
Dim cases As Variant
Dim title As String
title = "K-Map Program"
cases = InputBox("Enter number of inputs.", title)
If Not IsNumeric(cases) Then
Call notnum
End If
clearsheet = MsgBox("Permission to clear Sheet1?", vbYesNo + vbQuestion, title) ' could upgrade by giving user choice of which sheet to clear
If clearsheet = vbYes Then
MsgBox ("Clearing sheet1.")
Sheet1.Cells.Clear
ElseIf clearsheet = vbNo Then
MsgBox ("Sheet1 has not been cleared, program is ending.")
Exit Sub
Else
Call errormessage
End If
numforswitch = MsgBox("Do you want to label the inputs as numbers (1, 2, 3...)?", vbYesNo + vbQuestion, title)
' deposit the switches in columns in 0, 1 format
If numforswitch = vbYes Then
ifshift = MsgBox("Shift=0?", vbYesNo + vbQuestion, title)
If ifshift = vbYes Then
For counter = 1 To cases
Cells(1, counter).Value2 = counter ' will print 1, 2, 3... in the columns
Next
ElseIf shift = vbNo Then
shift = InputBox("What's shift?")
For counter = 1 To cases
Cells(1, counter).Value2 = counter + shift ' will print x, x+1...
Next
ElseIf numforswitch = vbNo Then
MsgBox ("Using letters for switches.")
' alphabet array
Const ALPHABET As String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Dim i As Integer
For i = 1 To VBA.Len(ALPHABET)
Cells(1, i).Value = VBA.Mid(ALPHABET, i, 1)
Next i
Else
Call errormessage
End If
End Sub
Your missing end-if is in this block:
If ifshift = vbYes Then
For counter = 1 To cases
Cells(1, counter).Value2 = counter ' will print 1, 2, 3... in the columns
Next
ElseIf shift = vbNo Then
shift = InputBox("What's shift?")
For counter = 1 To cases
Cells(1, counter).Value2 = counter + shift ' will print x, x+1...
Next
You should cleanup/format your code so you can easily spot issues like this. If you don't make your code look nice and orderly, you'll waste a lot of time with problems like this.