5

I have a userform which runs a script every 100ms. The script handles images on the userform and is used to animate them, while the form continues to receive user input (mouse clicks and key presses). This continues until the userform is closed. While Application.OnTime seems to work best, it only operates consistently on time values of 1 second or more.

When I use something like

Sub StartTimer()
    Application.OnTime now + (TimeValue("00:00:01") / 10), "Timer"
End Sub

Private Sub Timer()
    TheUserForm.ScreenUpdate
    Application.OnTime now + (TimeValue("00:00:01") / 10), "Timer"
End Sub

and call StartTimer in the userform, Excel becomes very unresponsive and "Timer" is called many more times per second than it should.

Using the Sleep function causes the program to become unresponsive too, although the script is run with the right interval.

Is there a workaround for this? Thanks in advance!

Badja
  • 857
  • 1
  • 8
  • 33
Honey Lemon
  • 51
  • 1
  • 2

4 Answers4

3

OnTime can only be scheduled to run in increments of 1 second. When you attempt to schedule it at 1/10th second, you actually schedule at 0 seconds, ie it runs again immediately, consuming all resources.

Short answer, you cannot use OnTime to run an event every 1/10 second.

There are other ways, see CPearson for using a call to Windows API
Public Declare Function SetTimer Lib "user32" ...

Community
  • 1
  • 1
chris neilsen
  • 52,446
  • 10
  • 84
  • 123
  • Use `sleep()` from kernel lib: `Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)`. \*zzz...\* – peter_the_oak Aug 04 '14 at 11:22
  • @peter_the_oak OP has already stated he has tried `Sleep` without success – chris neilsen Aug 04 '14 at 11:25
  • @chrisneilsen Thanks, your solution worked perfectly. Seems like using Windows timers is the way to go! – Honey Lemon Aug 04 '14 at 12:50
  • MY apologies for times less than 1 sec it does the Sub a number of times – Harry S Dec 30 '16 at 01:05
  • It may do say 300 iterations in 10 sec but it does them in 1 second bursts where it does say 30 in the first 0.01 of a sec then waits to the end of the sec... doing nothing for .99 sec.. repeating this for each of the 10 sec... – Harry S Dec 30 '16 at 01:16
  • Sorry wrong again .. it is erratic for less 1 sec ... a column output of Timer seconds shows various changes over 1 sec intervals... Looks like backt o Shawn's Hybrid . I do feel like a donkey brain.. but am still working on a system to display a text box of hints on mouse passing over various shapes on a worksheet.. – Harry S Dec 30 '16 at 04:52
1

Try this simple hybrid method for your 'Timer' sub:

Sub Timer
  Application.OnTime now + TimeValue("00:00:01"), "Timer"
  t1 = Timer
  Do Until Timer >= t1 + 0.9
    t2 = Timer
    Do Until Timer >= t2 + 0.1
      DoEvents
    Loop

    TheUserForm.ScreenUpdate
    ... your code

  Loop
End Sub 

Of course, one problem of user the 'Timer' function is that at midnight your code may turn into a pumpkin (or crash). ;) You would need to make this smarter but if you generally only work during the day, like me, it's not a problem.

1

Just had this same question today. Here's the solution I was able to find that worked really well. It allows a timed event to fire on intervals as small as 1 millisecond, without taking control of the application or causing it to crash.

The one disadvantage I've been able to find is that TimerEvent() requires a blanket On Error Resume Next to ignore errors caused when it can't execute the code (like when you're editing another cell), which means it will have no idea when a legitimate error occurs.

Declare PtrSafe Function SetTimer Lib "user32" (ByVal hWnd As LongPtr, _ 
    ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr
Declare PtrSafe Function KillTimer Lib "user32" (ByVal hWnd As LongPtr, _
    ByVal nIDEvent As LongPtr) As Long

Public TimerID As Long

Sub StartTimer()
    ' Run TimerEvent every 100/1000s of a second
    TimerID = SetTimer(0, 0, 100, AddressOf TimerEvent)
End Sub

Sub StopTimer()
    KillTimer 0, TimerID
End Sub

Sub TimerEvent()
    On Error Resume Next
    Cells(1, 1).Value = Cells(1, 1).Value + 1
End Sub
ddavidd
  • 139
  • 1
  • 11
  • 1
    Note that your are using API declarations prior to Office 2010 (consider conditional compilation via `#If VBA7 Then` if you have to use them as well). If not, simply use: `Declare PtrSafe Function SetTimer Lib "user32" (ByVal hWnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr` and `Declare PtrSafe Function KillTimer Lib "user32" Alias "KillTimer" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As Long` and care to declare `TimerID` as LongPtr via `Public TimerID As LongPtr`, too. – T.M. Mar 28 '19 at 17:39
  • @T.M. I don't think it has to do with the Office year per se, but rather if you're using 32-bit Excel or 64-bit Excel. I assumed 32-bit, as that's still the most common, even for Excel 2016, which is what I'm using. But you're correct if it's 64-bit Excel. I'll try putting that into my answer if I get a chance tomorrow, thanks! – ddavidd Mar 28 '19 at 20:35
  • 1
    AFAIK in most cases you are on the safe side with *Office 2010* and *later versions* as the mentioned (and most other renewed) pointer safe declarations support API calls on **both** 32-bit and 64-bit versions of Windows and can be checked via `#If VBA7 ...`; there remain only a few type statements declarations needing an explicit check for 64bit vs 32bit (`#If Win64 ...`). – T.M. Mar 28 '19 at 20:53
  • 1
    **Related links:** [API Declarations 32/64 bit Office](http://www.microsoft.com/en-us/download/confirmation.aspx?id=9970) saving into folder `C:\Office 2010 Developer Resources\Documents\Office2010Win32API_PtrSafe`), and [Declaring API functions in 64 bit Office](https://jkp-ads.com/Articles/apideclarations.asp) – T.M. Mar 28 '19 at 21:02
  • David, excuse my imprecision in the first comment; if you are sure to stick exclusevily to **32-bit installations** (of *Office 2010+*) you need not change to `PtrSafe` declarations and `LongPtr` data types (reserved for pointers to a → handle or → memory location); anyway you'd be on the safe side to provide for both forms of Office installation. – T.M. Mar 29 '19 at 07:15
  • 1
    @T.M., I didn't realize VBA7 didn't show up until Office 2010, so you're comments are completely correct. I also didn't realize `PtrSafe` doesn't require the `#If` for 2010 or later, since the last time I messed with it I was using 2007. You're comments have been really helpful! I've updated my answer accordingly. – ddavidd Mar 29 '19 at 16:17
  • Strangely enough, in Office 365 on Windows 10, using `Public TimerID As LongPtr` doesn't allow you to kill the timer, it only works with `Public TimerID As Long`. Not sure why, but I'll edit my answer again to put it back. – ddavidd Mar 31 '19 at 23:49
0
' yes it is a problem
' it stops when  cell input occurs  or an cancel = false dblClick
' the timer API generally bombs out EXCEL  on these 
' or program errors  as VBA has no control over them
' this seems to work  and is in a format hopefully easy to adapt to
' many simultaneous timed JOBS   even an Array of Jobs.. will try it this week
' Harry  

Option Explicit

Public RunWhen#, PopIntervalDays#, StopTime#

Public GiveUpDays#, GiveUpWhen#, PopTimesec#, TotalRunSec!

Public PopCount&

Public Const cRunWhat = "DoJob"    ' the name of the procedure to run

Sub SetTimerJ1(Optional Timesec! = 1.2, Optional RunForSec! = 10, Optional GiveUpSec! = 20)

If Timesec < 0.04 Then Timesec = 0.05

' does about 150 per sec at .05   "

' does 50 per sec at  .6    ????????????

' does 4 per sec at  .9    ????????????

'iterations per sec =185-200 * Timesec  (  .1 < t < .9 )

' if   t >1  as int(t)

'  or set Timesec about  (iterationsNeeded  -185)/200

'
    PopTimesec = Timesec

   PopIntervalDays = PopTimesec / 86400#  ' in days

   StopTime = Now + RunForSec / 86400#

   GiveUpDays = GiveUpSec / 86400#

   TotalRunSec = 0

PopCount = 0

    StartTimerDoJob

End Sub

Sub StartTimerDoJob()

  RunWhen = Now + PopIntervalDays

    GiveUpWhen = Now + GiveUpDays

   Application.OnTime RunWhen, cRunWhat, GiveUpWhen

' Cells(2, 2) = Format(" At " & Now, "yyyy/mm/dd hh:mm:ss")


  'Application.OnTime EarliestTime:=Now + PopTime, Procedure:=cRunWhat, _

    Schedule:=True

End Sub

Sub DoJob()

  DoEvents

 PopCount = PopCount + 1
'Cells(8, 2) = PopCount


   If Now >= StopTime - PopIntervalDays / 2 Then ' quit DoJob

   On Error Resume Next

     Application.OnTime RunWhen, cRunWhat, , False

   Else

      StartTimerDoJob  ' do again

   End If

End Sub

Sub StopTimerJ1()

  On Error Resume Next

  Application.OnTime EarliestTime:=RunWhen, Procedure:=cRunWhat, _
                       schedule:=False

End Sub
David Rogers
  • 2,601
  • 4
  • 39
  • 84
Harry S
  • 481
  • 6
  • 5