How to get VBA Macro to run continuously in the background? - vba

I want to monitor a value and get an email notifications when certain conditions are met. I have a macro like so:
Do While True
Worksheet.Calculate
If Value > 10 Then
SendEmail
End If
Sleep 60*CLng(1000)
Loop
However, when I run this, it clogs up the entire program and will turn unresponsive if I try to do anything.
Is there anyway to accomplish this but have it run in the background or at least not crash the program?
What I was doing before was using VBScript to open a not-visible spreadsheet and the VBScript ran continuously in the background monitoring the condition and worked fine, but my client really wants a GUI and for it to be in the program itself.
Any thoughts?

Use the Application.OnTime method to schedule code that will run in one minute.
Your code will look something like this (Untested):
Sub CreateNewSchedule()
Application.OnTime EarliestTime:=DateAdd("n", 1, Now), Procedure:="macro_name", Schedule:=True
End Sub
Sub macro_name()
If Value > 10 Then
SendEmail
Else
CreateNewSchedule
End If
End Sub
You might want to store the time of the next schedule in a global variable so that the Workbook_BeforeClose event can cancel the next schedule. Otherwise Excel will re-open the workbook.
Public nextScheduledTime As Date
Sub CreateNewSchedule()
nextScheduledTime = DateAdd("n", 1, Now)
Application.OnTime EarliestTime:=nextScheduledTime , Procedure:="macro_name", Schedule:=True
End Sub
Sub macro_name()
If Value > 10 Then
SendEmail
Else
CreateNewSchedule
End If
End Sub
Private Sub Workbook_BeforeClose(Cancel As Boolean)
On Error Resume Next
Application.OnTime EarliestTime:=nextScheduledTime, Procedure:="macro_name", Schedule:=False
End Sub
You can then continue to use Excel between the scheduled times.

I think you need to specifically process the application event stack with a DoEvents call. This allows user interactions with the spreadsheet to occur, where normally the macro would take precedence. You code would look something like:
Do While True
If Value > 10 Then
SendEmail
End If
Sleep 60*CLng(1000)
DoEvents
Loop
You could also construct a GUI with HTA if you wanted to remain with VBScript.

Related

Excel 2016 crash on API timer initiated Workbook.close

I have a workbook that crashes Excel on close when the close is triggered by an API timer.
Edit: The workbook in link below has nothing but the timer and exit modules and it has the same behavior so it's not an issue with the content.
https://www.dropbox.com/s/x0xdwgj5h34ctdk/Book1.xlsm?dl=0
A few seconds after the workbook closes Excel crashes. I have tried turning the timer off, unhiding all sheets, unloading forms...all the objects are set to Nothing.
All I am doing is Workbooks(ThisWorkbook.Name).Close SaveChanges:=True??!!
The same sub is called from a UserForm with no problem. The workbook closes without Excel crashing.
How to fix this?
Sub ApplicationExit()
' Call UnloadAllForms
' DoEvents
' Sleep 1000
' Call StopCloseTimer
'DoEvents
'If Application.Workbooks.Count = 1 Then
' Workbooks(ThisWorkbook.Name).Save
' Application.Quit
'Else
DoEvents
Workbooks(ThisWorkbook.Name).Close SaveChanges:=True
'End If
End Sub
The code triggered by the timer is;
Sub TimerCalled()
If CloseTimerValue = "" Then Call Reset_CloseTimerValue
DoEvents
If basTimers.CloseTimerValue <= Now() And Not Unlocked Then Call ApplicationExit
On Error Resume Next 'In case sheet is protected
ThisWorkbook.Sheets("JobIndex").Range("CloseCount").Value = Format(Now() - CloseTimerValue, "hh:m:s")
End Sub
The userform says;
CloseUp:
i = MsgBox("Close Project Register?", vbYesNo, MsgBoxTitle)
Select Case i
Case vbYes
Call ApplicationExit
Case vbNo
Workbooks(ThisWorkbook.Name).Save
End Select
Timer in excel vba is done by using the Application.OnTime command or windows timer, as you are using.
Notice that, to schedule the call for a function (using the upper-mentioned command), it's used APPLICATION, which means it tells EXCEL to execute the call, not your workbook. So, in your case, the workbook is closed, BUT the to-be-called procedure is still scheduled to go off at some time in the future. The error that you get is that the excel doesn't find the procedure and then throws an error.
The reason it crashes in your case is that you are using windows timer to do it. When you close your workbook, your instance of the lib32 is lost and when time comes, Windows can't reach in the memory and then crashes the whole application.
In both cases, it seems that your scheduled procedure is still running.
I suggest that you look into that and consider using application.ontime.

Cannot run the macro

I have encountered a problem in the macro below
Sub RefreshAction()
Range("b7").Select
Application.Run "RefreshCurrentSelection"
Application.OnTime (Now() + TimeValue("00:00:05")), "thisworkbook.Action"
End Sub
The cell refreshes when I run the macro the first time but I get the error message immediately after
Message: Cannot run the macro "C\Desktop\XYZ.xlsm'!thisworkbook.Action'. The macro may not be available in this workbook or all macros may be disabled.
I have already gone through "Trust Center->Trust Center Settings->Macro Settings->Enable all macros and it didn't work.
The "Trust access to VBA project object model" box is also clicked.
First of all, here is a snapshot of the error you get when you attempt to run OnTime from a worksheet (not a module) as I will explain. I was getting this error too and trying to figure out why.
It looks like a security error, but in this case it isn't exactly a normal security error.
To run code on a timer you have to add it to a VBA module.
Go to the VisualBasic editor and right click the VBAProject (book).
In Excel it looks like the following:
Once the module is added you add your timer code there.
Since you want to call RefreshAction every 5 seconds you would do something like the following:
Sub StartProcess()
Debug.Print Now()
Application.OnTime Now() + TimeValue("00:00:05"), "RefreshAction", Schedule = True
End Sub
Sub RefreshAction()
Application.EnableEvents = True
Debug.Print Now() + TimeValue("00:00:05")
Application.OnTime Now() + TimeValue("00:00:05"), "RefreshAction", Schedule = True
End Sub
I'll let you add the code that you want it to do each time in the RefreshAction subroutine.
Here's what it will look like in the Module. Make sure yours shows that it is in a module as it does in the image:
Also, I found it to be quite flaky. If you have anything even slightly wrong in the OnTime call it will fail silently. Copy my code (I tested it) and try it first. Once it runs, just add your code to the RefreshAction sub.
StartProcess()
Run the StartProcess to start the thing going.
Additionally Odd Thing
After I added that Module, I still had my code in the Worksheet and I went back and attempted to run it to see the error again and the odd thing is that once the code is in the Module you won't get the error from the Worksheet any more. It's probably referencing the code in the Module now.
See the absolute reference for more details : CPearson OnTime
First issue, you need to store the time that you'll input in your OnTime method to be able to stop it. (Here I declared a Public TimeToRun As Date)
Second Point To use the OnTime method continuously, you need to reset the timer at the end of your timed procedure (here RefreshAllStaticData).
So your whole code should look like this :
Public TimeToRun As Date 'so that TimeToRun can be used in both the functions
Sub RefreshAction()
Range("b7").Select
Application.Run "RefreshCurrentSelection"
DoEvents
'Store the next date of execution in TimeToRun
TimeToRun = Now() + TimeValue("00:00:05")
'Launch the next OnTime
Application.OnTime TimeToRun, "RefreshAllStaticData"
End Sub
Sub RefreshAllStaticData()
'--++-- Place your code here, as it is now --++--
'----Call RefreshAction to reset the OnTime method
'---------to another 5 seconds and keep "looping"
RefreshAction
End Sub
Sub Kill_OnTime()
'Launch this to stop the OnTime method
Application.OnTime _
earliesttime:=TimeToRun, _
procedure:="RefreshAllStaticData", _
schedule:=False
End Sub
A different but related cause of this error can be the string-length limitation of the OnTime method's Procedure parameter's argument. See my post at: Getting around the Max String size in a vba function?

Macro to start at a particular time

I have found sources that say to use
Application.OnTime TimeValue("18:00:00"), "MyMacro"
But I can't seem to get it to work. This is what I have entered
Sub TimeStamp()
'
' TimeStamp Macro
'
' Keyboard Shortcut: Ctrl+Shift+T
'
Application.OnTime TimeValue("13:25:00"), "TimeStamp"
' Following refreshes the data
Application.CalculateFullRebuild
Then the rest of the code followed by End Sub
For whatever unknown reason, when I start the Macro, it does not wait until that given time. What syntax rules am I breaking here?
The macro is triggered when the macro is triggered. OnTime schedules it to be run automatically, but why should that prevent you from running it manually? Think of it like a virus scan. Many people have their computers configured to do automatic scans at certain set times, but are perfectly able to run nonscheduled scans at any time. If you want to make sure that your macro doesn't have any effect before a certain time, use an if - then statement involving Now (if it is too early exit sub) -- although it makes more sense to not run it at all when you don't want to.
Another method that I've used for years successfully is by just creating a small vb script that is scheduled to call the macro. My other answer to a very similar question show this example script
Dim xlApp
Dim xlWkb
Set xlApp = CreateObject("excel.application")
Set xlWkb = xlApp.Workbooks.Open("PATH TO YOUR FILE")
xlApp.Visible = True
xlWkb.RunAutoMacros 1 'enables macros to be run on open
xlApp.Run ("YOUR PROCEDURE")
xlApp.Workbooks("YOUR WORKBOOK NAME").Save 'Save the workbook
xlApp.Quit 'quits excel
Schedule this through Tasks Scheduler. My other answer can be found here
This is what I ended up with thanks to the help of several of you. I used:
Private Sub Workbook_Open()
Application.Wait "6:45:00"
Call TimeStamp
End Sub
and placed this in "ThisWorkbook". This made it so that when I open up my workbook, the macro is automatically started but it waits until 3:03 pm to perform the rest of the task.
Next, I needed it to refresh every 15 minutes so I used Chip's solution suggested by User: Findwindow. The code goes as follows
Public RunWhen As Double
Public Const cRunIntervalSeconds = 900 ' 15 minutes
Public Const cRunWhat = "TimeStamp" ' the name of the procedure to run
Sub StartTimer()
If Time < TimeSerial(13, 15, 0) Then
RunWhen = Now + TimeSerial(0, 0, cRunIntervalSeconds)
Application.OnTime EarliestTime:=RunWhen, Procedure:=cRunWhat, _
Schedule:=True
End If
End Sub
at the end of the TimeStamp macro, I put Call StartTimer so that every time the macro is ran, the timer will schedule a new run time for 15 minutes from the current time. The If Time < TimeSerial(13, 15, 0) Then allows for the macro to stop running at 1:15pm, the time that I wanted it to stop.

Application.ontime code isn't running

I've done a fair bit of research on here to try and get this code to run, but I'm still having issues.
I am trying to run a macro at a set time. In fact it is a number of macros as different times. This macro then copies a block of cells, into a different area on the sheet.
I will not be able to use windows scheduler so I'm trying to do it this way. I gather without scheduler I will have to run the macro from a button. Therefore, my first sub is this (note button is on a separate sheet to the data)
Sub Call_save()
Call Sheets("Dashboard").8amsave
Application.OnTime TimeValue("08:10:00"), "Call_8amsave"
Call 9amsave
Application.OnTime TimeValue("09:10:00"), "Call_9amsave"
Call 10amsave
Application.OnTime TimeValue("10:10:00"), "Call_10amsave"
End Sub
This button is designed to then run the subs at the predetermined time. The subs it calls are these:
Sub 8amsave()
Dim current_data As Variant
current_data = Worksheets("Dashboard").Range("S6:V22").Value
Worksheets("Dashboard").Range("B33:E49").Value = current_data
End Sub
Sub 9amsave()
Dim current_data As Variant
current_data = Worksheets("Dashboard").Range("S6:V22").Value
Worksheets("Dashboard").Range("B54:E70").Value = current_data
End Sub
Sub 10amsave()
Dim current_data As Variant
current_data = Worksheets("Dashboard").Range("S6:V22").Value
Worksheets("Dashboard").Range("B75:E91").Value = current_data
End Sub
Now, when I run the initial button it runs all the subs at once and copies the sells into the correct places, straight away. This isnt really an issue for me. However, at the predetermined time I get an error saying unable to run the macro... can't find it or sheet unable to run macros... or something of that ilk.
Is there anything blindingly obvious that i am missing? I have kind of been working this out as I go along so my VBA knowledge is fairly limited. A point in the right direction would be great.
Your OnTime statements also appear to be referencing procedure names which don't exist, i.e., Call_8amSave as opposed to 8amsave. Note also that procedure names cannot begin with numeric (e.g., 8amsave) and this code should not even compile, let alone execute.
Rename your procedures so that they match the calls in the OnTime statement, and are legally permissible procedure names, like: Sub Call_8amSave, etc.
Also, the OnTime property can only have one procedure at a time, so you can't set 3 future times, you have to set them sequentially. So, set the OnTime for the 9am save during the 8am procedure, etc.
Option Explicit
Sub Call_save()
Application.OnTime TimeValue("08:10:00"), "Call_8amsave"
End Sub
Sub Call_8amsave()
With Worksheets("Dashboard")
.Range("B33:E49").Value = .Range("S6:V22").Value
End With
Application.OnTime TimeValue("09:10:00"), "Call_9amsave"
End Sub
Sub Call_9amsave()
With Worksheets("Dashboard")
.Range("B54:E70").Value = .Range("S6:V22").Value
End With
Application.OnTime TimeValue("10:10:00"), "Call_10amsave"
End Sub
Sub Call_10amsave()
With Worksheets("Dashboard")
.Range("B75:E91").Value = .Range("S6:V22").Value
End With
End Sub

Allow user to kill process during wait in VBA

I have written a program to print multiple .pdfs with varying file extensions off of an Excel spreadsheet list.
The problem is that it takes anywhere from 30 seconds to a minute for the printer to receive the pdf after the line: Application.SendKeys "^p~", False is called.
To get by this I used Application.Wait (Now + TimeValue("0:01:03")) to wait for a minute (plus 3 seconds just to be safe) before closing the file.
To me there seems like there should be a better way than just causing the program to wait, so I looked around a little and found a question about this lovely gem known as Application.OnTime.
I tried a sample of one of the answers:
Sub test2()
ActiveSheet.Cells(1, 1).Value = ActiveSheet.Cells(1, 1).Value + 1
Application.OnTime Now + TimeValue("00:00:5"), "test2"
End Sub
However when I tried to stop the above code it kept going on an infinite loop and I was unable to stop it until I killed excel using the windows task manager.
I would like to be able to add in a little message box or something of the sort so that the user can click in between the wait time.
So that while the program is waiting for a minute, the user can either manually click and start the program on the next pdf, or click another button to exit if they need to stop printing early. Something like this:
Sub pdfPrinter()
'...
'Insert all the other code here
'...
Application.SendKeys "^p~", False
Application.OnTime Now + TimeValue("00:01:02"), "pdfPrinter"
continue= MsgBox("Click Retry to print again, or cancel to stop printer.", vbRetryCancel)
If continue = vbRetry Then
Call pdfPrinter
ElseIf continue = vbCancel Then
Exit Sub
End If
End Sub
Application.OnTime(unlike Application.Wait) is Asynchronous
so code after
Application.OnTime Now + TimeValue("00:01:02"), "pdfPrinter"
Runs Immediately.
If you want to print another PDF you need to call Application.OnTime with a different schedule of the 1st one.
Now, canceling Application.OnTime, when it is already started is a different story:
To do that you need to store that time that the respective function is scheduled to run and then Cancel it using the following code:
Application.OnTime Now + TimeValue("00:00:50"), "test", schedule:=False