How can I throttle functions in VBA? - vba

I'm making a drop down inside of Excel using a ComboBox and VBA. I have been able to get it to make requests to a remote api, add the returned data to a hidden worksheet, and update the drop down options based on the result of the api.
What I'm looking to do is throttle the api requests. At the moment, it seems that Excel will not fire off the sub if it is already processing an api request. This is not ideal, because often people will type more than one character in rapid succession. I'd like to add a timer to each sub call, and if there hasn't been a new call to the sub function within ~250ms, send the api request. If another call is made during the 250ms, I want to cancel the execution of that sub.
Initially I tried creating a global "process_id" where the sub would add 1 to the current global, set its local id to that value, wait for x time, check if its local id === the global id, and if not exit the sub. However it now seems that the second sub never runs while the timer is waiting for x time, so the first sub still runs, just x seconds later (and the second sub never runs at all).
How do I go about throttling sub functions in Excel VBA so that the same sub function can be run while the first is waiting?

As stated; you require an asynchronous timer with millisecond precision
Hopefully the following should work for you:
While the timer is set, no further timer should be set and when the timer fires the event, the timer stops itself so multiple calls during the 'DelayTimeSeconds' period should only result in one call to the API
' Adapted from https://groups.google.com/forum/?hl=en#!topic/microsoft.public.excel.programming/Ce--7sympZM
' These procedures require that you are using Office 2000 or later, because of the AddressOf function
Public Declare Function SetTimer Lib "user32" ( _
ByVal HWnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Public Declare Function KillTimer Lib "user32" ( _
ByVal HWnd As Long, ByVal nIDEvent As Long) As Long
Private AllowFireTime As Single
Private TimerID As Long
Private TimerSeconds As Single
Private TimerSet As Boolean
Const DelayTimeSeconds As Single = 0.75
Sub TestFireDelay()
CallAPIProc
End Sub
Private Function CallAPIProc()
Dim NowTime As Single: NowTime = Timer
If AllowFireTime > NowTime Then
If TimerSet = False Then StartTimer AllowFireTime - NowTime
Else
AllowFireTime = NowTime + DelayTimeSeconds
Call TestCall
' Code for API Call
End If
End Function
Function StartTimer(Optional TimerSeconds = 1) ' how often to "pop" the timer.
TimerID = SetTimer(0&, 0&, TimerSeconds * 1000&, AddressOf TimerProc)
TimerSet = True
End Function
Function EndTimer()
On Error Resume Next
KillTimer 0&, TimerID
TimerSet = False
End Function
Function TimerProc(ByVal HWnd As Long, ByVal uMsg As Long, ByVal nIDEvent As Long, ByVal dwTimer As Long)
' The procedure is called by Windows. Put your timer-related code here.
'
Call EndTimer
Call CallAPIProc
End Function
Function TestCall()
Debug.Print Format(Now, "hh:mm:ss") & "." & Strings.Right(Strings.Format(Timer, "#0.00"), 2)
End Function

I think what you're really looking for is "don't call the API until I've stopped typing" (i.e. when no further key press has occurred within x msec of the previous one).
If that's what you want then this (with hat tip to #Tragamor's answer) should do it. This is much closer to what you might do in js using window.setTimeout for example.
In a regular code module:
Option Explicit
Public Declare Function SetTimer Lib "user32" ( _
ByVal HWnd As Long, ByVal nIDEvent As Long, _
ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Public Declare Function KillTimer Lib "user32" ( _
ByVal HWnd As Long, ByVal nIDEvent As Long) As Long
Private TimerID As Long
'this function called from the control's Change event
Function CallApi()
Const DelayMsec As Long = 500
If TimerID > 0 Then KillTimer 0&, TimerID 'kill any existing timer
TimerID = SetTimer(0&, 0&, DelayMsec, AddressOf CallApi2) 'set a timer
End Function
'this function called from the timer
Sub CallApi2()
If TimerID > 0 Then KillTimer 0&, TimerID
Debug.Print "Calling API with '" & Sheet1.TextBox1.Text & "'"
End Sub

Sticking with your method of using global variables, keep one to track the Timer function. Unfortunately you did not provide your code, so I can only make assumptions on how you should implement this.
Private myTimer As Single '// Global Variable, outside of any routine
Sub foo()
If Timer - myTimer < 0.25 Then Exit Sub
callMyAPI
myTimer = Timer
End Sub
If you prefer not to exit the sub, wrap the API call in an If statement.
Private myTimer As Single
Sub foo()
' Non-API related code
If Timer - myTimer > 0.25 Then
callMyAPI
myTimer = Timer
End If
'Non-API related code
End Sub
With each API call, you will reset the myTimer variable.

Related

How to delay a macro that runs after sending email?

I have the below code for Outlook 365 which will run a macro after sending an email.
How do I modify this to delay the macro 10 seconds after clicking send, and how do I limit this code to my exchange account email which is the default email account?
Option Explicit
Private Sub Application_ItemSend(ByVal Item As Object, Cancel As Boolean)
With Item
Call BatchResendEmailsMacro
End With
End Sub
Outlook doesn't have a timer function but you can use Appointment or Task Reminders to trigger macros. Set up an Application_Reminder macro that will do something when a reminder fires. To limit it to running when specific reminders fire, use an If statement to look for words in the subject or a specific category.
If you want the macro to fire a specified time after you restart Outlook, use an Application_Startup macro to create the appointment. Read more about that in the Running Outlook Macros on a Schedule article.
Also you may consider using Windows API functions such as SetTimer and KillTimer. Outlook VBA - Run a code every half an hour page provides a sample code (for example, that is for Windows x64):
Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongLong, ByVal nIDEvent As LongLong, ByVal uElapse As LongLong, ByVal lpTimerfunc As LongLong) As LongLong
Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongLong, ByVal nIDEvent As LongLong) As LongLong
Public TimerID As LongLong 'Need a timer ID to eventually turn off the timer. If the timer ID <> 0 then the timer is running
Public Sub TriggerTimer(ByVal hwnd As Long, ByVal uMsg As Long, ByVal idevent As Long, ByVal Systime As Long)
MsgBox "The TriggerTimer function has been automatically called!"
End Sub
Public Sub DeactivateTimer()
Dim lSuccess As LongLong
lSuccess = KillTimer(0, TimerID)
If lSuccess = 0 Then
MsgBox "The timer failed to deactivate."
Else
TimerID = 0
End If
End Sub
Public Sub ActivateTimer(ByVal nMinutes As Long)
nMinutes = nMinutes * 1000 * 60
'The SetTimer call accepts milliseconds, so convert to minutes
If TimerID <> 0 Then Call DeactivateTimer
'Check to see if timer is running before call to SetTimer
TimerID = SetTimer(0, 0, nMinutes, AddressOf TriggerTimer)
If TimerID = 0 Then
MsgBox "The timer failed to activate."
End If
End Sub
You could trigger the code with the ItemAdd event on the Sent Items folder.
Option Explicit
' In the ThisOutlookSession module
Private WithEvents sentItems As Items
Private Sub Application_Startup()
Dim sentItemsFolder As Folder
' default Sent Items folder
Set sentItemsFolder = Session.GetDefaultFolder(olFolderSentMail)
Set sentItems = sentItemsFolder.Items
End Sub
Private Sub sentItems_ItemAdd(ByVal item As Object)
Dim waitTime As Long
Dim waitDiff As Long
Dim delay As Date
Dim waitStart As Date
waitTime = 10 ' in seconds
Debug.Print vbCr & "Wait start: " & Now
waitStart = Now
delay = DateAdd("s", waitTime, waitStart)
Debug.Print "Wait until: " & delay
Do Until Now >= delay
DoEvents
Loop
Debug.Print "Wait end..: " & Now
waitDiff = DateDiff("s", waitStart, Now)
Debug.Print waitDiff & " seconds delay."
Debug.Print "Call BatchResendEmailsMacro"
'Call BatchResendEmailsMacro
Debug.Print "Done."
End Sub
Private Sub test()
sentItems_ItemAdd ActiveInspector.currentItem
End Sub

Absolutely cannot run any macro after SetTimer function is used

So I need to use the SetTimer API in my Excel VB project, but after I execute the interval timer, the program crashes as soon as you attempt to run another macro. Even when simply clicking the macro button in Developer tab. The code:
Public Declare Function SetTimer Lib "user32" ( _
ByVal HWnd As Long, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As Long) As Long
Public Declare Function KillTimer Lib "user32" ( _
ByVal HWnd As Long, _
ByVal nIDEvent As Long) As Long
Public timId As Long, lala As Long, i As Integer
Public Sub CallTm()
timId = SetTimer(0, 0, 100, AddressOf Test)
End Sub
Public Sub AnotherSub()
MsgBox "This is not gonna be shown"
End Sub
Public Sub Test()
Cells(1, 1).Value = i
i = i + 1
End Sub
It seems it's not a problem with KillTimer. Simply setting the interval with SetTimer is like a switch for making sure no more macros can be run (or it will simply crash if you attempt that). I remember seeing Error 50290 if that's any more help.
Why is it so and how can it be fixed?
By the way, I'm making a snake game in Excel for a school project.
It seems like after the Timer is set, nothing can happen since the timer takes up all the thread? or smth like that and it can't be "interrupted".
Really, how is this API supposed to be used? It seems like a fatal error which makes it completely useless...
You're corrupting the stack, because your Test procedure does not match the signature of TimerProc. You should read and understand the documentation for API calls before simply making a blind stab at using them.
You can find the documentation for SetTimer at MSDN as well, just like all other WinAPI documentation.
the prototype of timerPorc is this
VOID CALLBACK TimerProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ UINT_PTR idEvent,
_In_ DWORD dwTime
);
that can be translated to vb as follow
sub Test(byval hWnd as long, byval uMsg as long,byval idIvent as long, byval dwTime as long)
'your code here
end sub

Closing Excel after inactivity, even in edit mode

I'm not sure this is possible I didn't really find anything when I was searching for an answer online. I have a macro in place where it will close after 5 minutes of inactivity. Works like a charm except when the user is editing a cell the timer does not start, therefore it will not close because of this. Is there a way for excel to have a timer for how long the user is in edit mode then it will take them out of it. Once out of edit mode the macro will start the 5 minutes of inactivity. Any help is greatly appreciated!
HERE IS THE WORKBOOK MODULE
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Me.Saved = True
End Sub
Private Sub Workbook_Open()
start_Countdown
End Sub
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
stop_Countdown
start_Countdown
End Sub
HERE IS THE STANDARD MODULE
Option Explicit
Public Close_Time As Date
Sub start_Countdown()
Close_Time = Now() + TimeValue("00:05:00")
Application.OnTime Close_Time, "close_WB"
End Sub
Sub stop_Countdown()
Application.OnTime Close_Time, "close_WB", , False
End Sub
Sub close_wb()
ThisWorkbook.Close True
End Sub
You will need some kind of process monitor/task terminator outside of VBA to do this because VBA is "locked out" while the user is in input mode.
You can try this:
Public Declare Function SetTimer Lib "user32" ( _
ByVal HWnd As Long, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As Long) As Long
Public Declare Function KillTimer Lib "user32" ( _
ByVal HWnd As Long, _
ByVal nIDEvent As Long) As Long
Public TimerID As Long
Public TimerSeconds As Single
Sub StartTimer()
TimerSeconds = 5*60 ' how often to "pop" the timer (5 minutes in the example).
TimerID = SetTimer(0&, 0&, TimerSeconds * 1000&, AddressOf TimerProc)
End Sub
Sub EndTimer()
On Error Resume Next
KillTimer 0&, TimerID
End Sub
Sub TimerProc(ByVal HWnd As Long, ByVal uMsg As Long, _
ByVal nIDEvent As Long, ByVal dwTimer As Long)
' call here whatever you want to call
End Sub
This always work even in edit mode, dont forget to call StartTimer to start and EndTimer before quitting...

Trying to write a simply program in VB that will check the system for a timevalue(now) and if its midnight then it increments a number

I'm interested in creating a slide with powerpoint that will just display a new number in a Shape if theres a new day or time (midnight). I know java programming but haven't done programming in over 6 years now. I've never really used VB.
Dim CurrentTime = TimeValue(now)
Sub If CurrentTime.Now = TimeValue(24:00:00)
Then updateNum;
i = i+1
'Then I would like to display 'i' in a shape on powerpoint.
End Sub
Was thinking about doing a continous loop since the file will always be open and will never close.
Or Should I use a timer to countdown the seconds of the day then increment the number?
Unlike Excel, PowerPoint doesn't have OnTimer which would be helpful here.
Just making a loop will result in 100% processor consumption. You probably don't want that.
Calling Sleep() on each iteration will preserve processor time, but make the application unresponsible. That you probably don't want either.
So you should really set up a timer. If writing a VSTO addin is okay with you, then just use the Timer class, otherwise make one yourself in VBA:
Option Explicit
Private Declare Function SetTimer Lib "user32.dll" (ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib "user32.dll" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Private hTimer As Long
Private PrevDate As Date
Public Sub StartTimer()
If hTimer = 0 Then
hTimer = SetTimer(0, 0, 1000, AddressOf TimerProc)
End If
End Sub
Public Sub StopTimer()
KillTimer 0, hTimer
hTimer = 0
End Sub
Private Sub TimerProc(ByVal hwnd As Long, ByVal uMsg As Long, ByVal nIDEvent As Long, ByVal dwTime As Long)
Dim CurDate As Date
CurDate = Date
If CurDate > PrevDate Then
PrevDate = CurDate
'Put your display code here
End If
End Sub
You can include this in a module in your presentation. It will fire on every slide change during a slide show:
Sub OnSlideShowPageChange(ByVal SSW As SlideShowWindow)
MsgBox (SSW.View.Slide.SlideIndex)
End Sub
Obviously, replace the MsgBox statement with code to update your text with the current date/time.
This works in PPT 2010 and should work as far back as Office 97, but isn't documented/supported, so MS might remove it whenever the whim strikes them. I don't know whether it works in PPT on the Mac.

Outlook VBA - Run a code every half an hour

I want run a specific code in outlook(VBA) every half an hour.
Also the outlook user should not get disturbed when the code runs. It should run in back-end only.
There is an event called Application_Reminder. It runs when a at each occurrence of reminder in outlook. But this still involves user interaction. I want a complete back end procedure.
http://www.outlookcode.com/threads.aspx?forumid=2&messageid=7964
Place the following code in the ThisOutlookSession module (Tools->Macros->VB Editor):
Private Sub Application_Quit()
If TimerID <> 0 Then Call DeactivateTimer 'Turn off timer upon quitting **VERY IMPORTANT**
End Sub
Private Sub Application_Startup()
MsgBox "Activating the Timer."
Call ActivateTimer(1) 'Set timer to go off every 1 minute
End Sub
Place the following code in an new VBA module
Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerfunc As Long) As Long
Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Public TimerID As Long 'Need a timer ID to eventually turn off the timer. If the timer ID <> 0 then the timer is running
Public Sub ActivateTimer(ByVal nMinutes As Long)
nMinutes = nMinutes * 1000 * 60 'The SetTimer call accepts milliseconds, so convert to minutes
If TimerID <> 0 Then Call DeactivateTimer 'Check to see if timer is running before call to SetTimer
TimerID = SetTimer(0, 0, nMinutes, AddressOf TriggerTimer)
If TimerID = 0 Then
MsgBox "The timer failed to activate."
End If
End Sub
Public Sub DeactivateTimer()
Dim lSuccess As Long
lSuccess = KillTimer(0, TimerID)
If lSuccess = 0 Then
MsgBox "The timer failed to deactivate."
Else
TimerID = 0
End If
End Sub
Public Sub TriggerTimer(ByVal hwnd As Long, ByVal uMsg As Long, ByVal idevent As Long, ByVal Systime As Long)
MsgBox "The TriggerTimer function has been automatically called!"
End Sub
Key points:
1) This timer function does not require that a particular window is open; it works in the background
2) If you don't deactivate the timer when the application closes it will likely crash
3) The example shows the timer being activated at startup, but it can just as easily be called by a different event
4) If you don't see the msgbox indicating that the timer was activated upon startup, your macro security is set too high
5) To have the timer deactivate after one iteration of the time interval add: If TimerID <> 0 Then Call DeactivateTimer after the msgbox statement in sub TriggerTimer
Someone else suggested
"one point to note, if you don't check if TimerID is the same as idevent in the TriggerTimer, you get every so often, and not the time you asked for."
Public Sub TriggerTimer(ByVal hwnd As Long, ByVal uMsg As Long, ByVal idevent As Long, ByVal Systime As Long)
'keeps calling every X Minutes unless deactivated
If idevent = TimerID Then
MsgBox "The TriggerTimer function has been automatically called!"
End If
End Sub
For Win64, I needed to change it to this:
Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongLong, ByVal nIDEvent As LongLong, ByVal uElapse As LongLong, ByVal lpTimerfunc As LongLong) As LongLong
Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongLong, ByVal nIDEvent As LongLong) As LongLong
Public TimerID As LongLong 'Need a timer ID to eventually turn off the timer. If the timer ID <> 0 then the timer is running
Public Sub TriggerTimer(ByVal hwnd As Long, ByVal uMsg As Long, ByVal idevent As Long, ByVal Systime As Long)
MsgBox "The TriggerTimer function has been automatically called!"
End Sub
Public Sub DeactivateTimer()
Dim lSuccess As LongLong
lSuccess = KillTimer(0, TimerID)
If lSuccess = 0 Then
MsgBox "The timer failed to deactivate."
Else
TimerID = 0
End If
End Sub
Public Sub ActivateTimer(ByVal nMinutes As Long)
nMinutes = nMinutes * 1000 * 60 'The SetTimer call accepts milliseconds, so convert to minutes
If TimerID <> 0 Then Call DeactivateTimer 'Check to see if timer is running before call to SetTimer
TimerID = SetTimer(0, 0, nMinutes, AddressOf TriggerTimer)
If TimerID = 0 Then
MsgBox "The timer failed to activate."
End If
End Sub
Correct for upper answer for 64-bit:
Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongLong, ByVal nIDEvent As LongLong, ByVal uElapse As LongLong, ByVal lpTimerfunc As LongLong) As LongLong
Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongLong, ByVal nIDEvent As LongLong) As LongLong
Public TimerID As LongLong 'Need a timer ID to eventually turn off the timer. If the timer ID <> 0 then the timer is running
Public Sub TriggerTimer(ByVal hwnd As Long, ByVal uMsg As Long, ByVal idevent As Long, ByVal Systime As Long)
MsgBox "The TriggerTimer function has been automatically called!"
End Sub
Public Sub DeactivateTimer()
Dim lSuccess As LongLong '<~ Corrected here
lSuccess = KillTimer(0, TimerID)
If lSuccess = 0 Then
MsgBox "The timer failed to deactivate."
Else
TimerID = 0
End If
End Sub
Public Sub ActivateTimer(ByVal nMinutes As Long)
nMinutes = nMinutes * 1000 * 60 'The SetTimer call accepts milliseconds, so convert to minutes
If TimerID <> 0 Then Call DeactivateTimer 'Check to see if timer is running before call to SetTimer
TimerID = SetTimer(0, 0, nMinutes, AddressOf TriggerTimer)
If TimerID = 0 Then
MsgBox "The timer failed to activate."
End If
End Sub