10

I read in various places that API timers are risky in VBA, that if you edit a cell while the timer is running it will crash Excel.

This code from http://optionexplicitvba.wordpress.com written by Jordan Goldmeier does not seem to have this problem. It fades a pop-up using the timer and while its fading, I can click and enter text in cells and the formula bar.

When is the API timer safe and when is it not? Are there some specific principles to help me understand? And what is the mechanism of the crash: what is happening exactly to make Excel crash?

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

Public TimerID As Long
Public TimerSeconds As Single
Public bTimerEnabled As Boolean
Public iCounter As Integer
Public bComplete As Boolean

Public EventType As Integer

Public Sub Reset()
    With Sheet1.Shapes("MyLabel")
        .Fill.Transparency = 0
        .Line.Transparency = 0
        .TextFrame2.TextRange.Font.Fill.ForeColor.RGB = RGB(0, 0, 0)
    End With
    Sheet1.Shapes("MyLabel").Visible = msoTrue
End Sub

Sub StartTimer()
    iCounter = 1
    Reset
    TimerID = SetTimer(0&, 0&, 0.05 * 1000&, AddressOf TimerProc)
End Sub

Sub EndTimer()
    KillTimer 0&, TimerID
    bTimerEnabled = False
    bComplete = True
End Sub

Sub TimerProc(ByVal HWnd As Long, ByVal uMsg As Long, _
    ByVal nIDEvent As Long, ByVal dwTimer As Long)

    On Error Resume Next

    Debug.Print iCounter
    If iCounter > 50 Then
        With Sheet1.Shapes("MyLabel")
            .Fill.Transparency = (iCounter - 50) / 50
            .Line.Transparency = (iCounter - 50) / 50
            .TextFrame2.TextRange.Font.Fill.ForeColor.RGB = _
                RGB((iCounter - 50) / 50 * 224, _
                     (iCounter - 50) / 50 * 224, _
                     (iCounter - 50) / 50 * 224)
        End With
    End If

    If iCounter > 100 Then
        Sheet1.Shapes("MyLabel").Visible = msoFalse
        EndTimer
    End If

    iCounter = iCounter + 1
End Sub

Public Function ShowPopup(index As Integer)

    Sheet1.Range("Hotzone.Index").Value = index

    iCounter = 1

    If bTimerEnabled = False Then
        StartTimer
        bTimerEnabled = True
        Reset
    Else
        Reset
    End If

    With Sheet1.Shapes("MyLabel")
        .Left = Sheet1.Range("Hotzones").Cells(index, 1).Left + _
            Sheet1.Range("Hotzones").Cells(index, 1).Width
        .Top = Sheet1.Range("Hotzones").Cells(index, 1).Top - _
                (.Height / 2)
    End With
    Sheet1.Range("a4:a6").Cells(index, 1).Value = index

End Function
srm
  • 3,062
  • 16
  • 30
Cool Blue
  • 6,438
  • 6
  • 29
  • 68
  • Out of curiosity: why don't you use the VBA Timer? – Peter Albert Nov 29 '13 at 07:23
  • @Peter Albert In fact I am currently using the VBA OnTime function but I'm exploring the posability of a performance upgrade: the resolution for the OnTime Method is 1 second which is a bit klunky. Also, I use Hypertext to call UDF's and I can't kick the timer from that environment. OnTime calls are ignored when execution is initiated through a UDF call. – Cool Blue Nov 29 '13 at 07:36
  • Interesting - but maybe there lies your (or a) problem - pretty sure that setting a timer from a UDF is rather a hack and might have unforeseen consequences... – Peter Albert Nov 29 '13 at 08:11
  • @Peter Albert well it's two separate applications: one is from a sub and the other is a UDF. The first one is polling for window scrolls to create a floating header and the second is for switching off a shape that was activated by a call from a hypertext function. Which is a hack in itself I guess, but worth exploring. – Cool Blue Nov 29 '13 at 14:36
  • [Here](http://stackoverflow.com/questions/32669344/how-to-deal-with-runtime-error-50290-in-excel-when-using-settimer-api/32669404)'s the example of Excel crashing when using SetTimer API. I fail to see anything not robust in the code. At least it's not obvious that it's not robust. – x-yuri Sep 19 '15 at 14:36
  • This is valid for 32-bit Excel, and it will work in both VBA7 and in a 64-bit environment. However, this is no longer considered best practice: , you should be using the safe pointer type, and LongLong for 64-bit environments, with conditional compilation blocks for the declarations and the Timer callback functions. I will provide these declarations in a separate answer below. – Nigel Heffernan Oct 05 '15 at 11:42

6 Answers6

8

@CoolBlue: And what is the mechanism of the crash: what is happening exactly to make Excel crash?

I can can give you an expansion of Siddarth Rout's answer, but not a complete explanation.

API calls are not VBA: they exist outside VBA's error-handlers and when things go wrong they will either do nothing, or call on a resource in memory that doesn't exist, or attempt to read (or write!) to memory that's outside the designated memory space for Excel.exe

When that happens, the Operating System will step in and shut your application down. We used to call this a 'General Protection Fault' and that's still a useful description of the process.

Now for some details.

When you call a function in VBA, you just write the name - let's call it 'CheckMyFile()' - and that's all you need to know within VBA. If there's nothing called 'CheckMyFile' to call, or it's declared where your call can't see it, the compiler or the runtime engine will raise an error in the form of a breakpoint, or a warning before it compiles and runs.

Behind the scenes, there's a numeric address associated with the string 'CheckMyFile': I'm simplifying a bit, but we refer to that address as a Function Pointer - follow that address, and we get to a structured block of memory that stores definitions of the function parameters, space for their stored values and, behind that, addresses directing those parameters into the functional structures created to execute your VBA and return values to the address for the function's output.

Things can go wrong, and VBA does a lot of work to ensure that all this folds up gracefully when they do go wrong.

If you give that function pointer to something that isn't VBA - an external application or (say) an API Timer Call - your function can still be called, it can still run, and everything will work.

We refer to this as a 'Callback' when you hand the function pointer to the API, because you call its timer function, and it calls you back.

But there had better be a valid function behind that pointer.

If there isn't, the external application will call its own error-handlers, and they won't be as forgiving as VBA.

It might just drop the call and do nothing if Excel and VBA are in a 'busy' state or otherwise unavailable when it tries to use that function pointer: you might be lucky, just that once. But it might call down the wrath of the operating system on the Excel.exe process.

If the callback results in an error, and that error isn't handled by your code, VBA will raise the error to the caller - and, as the caller isn't VBA, it'll probably have no way of handling that: and it'll call for 'help' from the operation system.

If it's an API call, it was written for developers who are assumed to have put the error-handling and contingency management in place in the calling code.

Those assumptions are:

  1. There will definitely be a valid function behind that pointer;
  2. It definitely be available when it is called;
  3. ...And it will raise no errors to the caller.

With an API callback, caller is the operating system, and its response to detecting an error will be to shut you down.

So that's a very simple outline of the process - a 'why' rather than a 'what' explanation of it.

The full explanation, without the oversimplifications, is for C++ developers. If you really want the answer in depth, you must learn to program with pointers; and you must become fluent with the concepts and practice of memory allocation, exceptions, the consequences of a bad pointer and the mechanisms used by an operating system to manage running applications and detect an invalid operation.

VBA exists to shield you from that knowledge and simplify the task of writing applications.

Nigel Heffernan
  • 4,636
  • 37
  • 41
  • Yes, it's a GPF because the disposition of the call-back location has changed. I did figure it out eventually (it was a while ago) but my aim in posting this question was to create a resource where the issue is fully bottomed out. And now it is, so thank you @Nile! – Cool Blue Oct 01 '15 at 21:31
  • @CoolBlue And again here, why would it change? @Nile Pretty broad description, I have a hard time following it. Let's take [this simple example](http://stackoverflow.com/questions/32669344/how-to-deal-with-runtime-error-50290-when-using-settimer-api). I believe there are no messages involved there. Except for Excel handling its message queue. But has it anything to do with it? If Excel can't respond in time, `On Error Resume Next` won't solve the issue. Are you saying that the function would move to the other place in memory? Why would it? – x-yuri Oct 07 '15 at 20:13
  • ...Are you saying that some error occurs when `Range("A1") = "test"` is executed? For now I believe this is the most likely reason. But what kind of errors is this? I fail to find an answer on the page. Additionally, you might add an explanation for people with `C` background, like me. That might clarify things. – x-yuri Oct 07 '15 at 20:18
  • Here are [more](https://msdn.microsoft.com/en-us/library/367eeye0.aspx) relevant [info](https://msdn.microsoft.com/en-us/library/at4fb09f.aspx), which states that managed functions (those of VBA apparently) may be moved by garbage collecter. And: `You must manually keep the delegate from being collected by the garbage collector from managed code. The garbage collector does not track references to unmanaged code.` So, supposedly, VBA funcs might be moved or destroyed by GC, but how come `On Error Resume Next` helps? It won't get a chance to be executed, will it? – x-yuri Oct 08 '15 at 07:32
  • @x-yuri - I've never had a function pointer to a Public function in a public VBA module cleaned up by GC... Except, of source, in cases where a code reset from an error dialog or an 'End' statement has invoked Garbage Collection. That'll definitely do it! If you *are* encountering a problem, I'd suggest declaring a static variable inside the function - that should make GC a little more reluctant. – Nigel Heffernan Oct 08 '15 at 10:36
  • 2
    @x-yuri - 'Let's take this simple example. ' I had a look, and I can tell you exactly what is going wrong. It's the 'clicking in cells' - one or more of those clicks has moved the worksheet into the 'editing cell' state, which suspends calculation, visual basic, and events. **VBA will not respond to an external callback in this state** and the API timer doesn't handle that condition gracefully. And no, it isn't just a message on the Excel window message queue - it's callback with a function pointer, and there are thread-management considerations. – Nigel Heffernan Oct 08 '15 at 10:49
  • Okay, let me share how I understand the issue. For VBA to pass a callback to SetTimer API, it creates an unmanaged function (function pointer), which calls delegate, which calls VBA's function, which sets up error handling and tries to change the cell. My conjecture is that an error happens when this `Range("A1") = "test"` statement is executed. If what you're saying is true, although I don't quite understand where in the chain of calls it won't respond, then `On Error Resume Next` won't have an effect. On condition that it's a runtime statement. Where's a mistake in my speculation? – x-yuri Oct 08 '15 at 14:25
  • @CoolBlue Whoah, am I understanding correctly that your problem was *not* a result of an unhandled error from [async interaction with the Excel Object model](https://support.microsoft.com/en-gb/help/2800327/limitation-of-asynchronous-programming-to-the-excel-object-model) (e.g using `Range("A1") = 5` in your TIMERPROC without OERN), but was instead caused by the function pointer becoming invalid mid execution? That's very bad if it's the case, what drew you to that conclusion? As far as I can tell the reason the code in your post doesn't crash is because OERN suppresses Excel OM errors – Greedo Sep 08 '19 at 14:49
  • @NigelHeffernan you suggest that editing the sheet puts (Excel?) into a state where _VBA will not respond to an external callback [...] and the API timer doesn't handle that condition gracefully_. Could you possibly expand on this? I thought that the callback is only run when the thread's message loop processes the `WM_TIMER` made by SetTimer expiring; since VBA runs in Excel's UI thread, surely the OS will never attempt to invoke the callback while Excel is actually busy. Sure Excel's Object Model may be in edit mode and inaccessible to VBA, but won't the TIMERPROC still be callable/valid? – Greedo Sep 08 '19 at 15:23
  • @Greedo - _"...but won't the TIMERPROC still be callable/valid?_ " Nope. Try it, and see what happens: and good luck getting anything useful out of the crash dump file. – Nigel Heffernan Sep 09 '19 at 14:39
  • @NigelHeffernan I've not been able to reproduce this crash, running lots of timers at once and editing cells seems to work fine for me (see [this gif](https://i.stack.imgur.com/udU9l.gif)) without any mystery crashes. I really want to try and find ways to break timers because I'm writing a [friendly wrapper](https://codereview.stackexchange.com/q/229656/146810) that is meant to mitigate most issues. Anyway I'm really curious but I can't get the bug your talking about to happen. Do you know why / would you be able to come up with some reproducible example? It'd be really helpful! – Greedo Sep 27 '19 at 09:00
  • @Greedo - I'm not convinced, from that gif, that you've actually placed the cursor in the formula bar text box, for a cell containing a formula, and started an edit, *and left the cursor there* while processes are running. – Nigel Heffernan Sep 27 '19 at 15:23
  • @NigelHeffernan Ok yeah that gif is a bit unclear. But I just confirmed it indeed works (the gif is sped up a little, but try it for yourself if you want, just download the example file and 2 dependencies in that linked question). I can imagine a problem with multithreading; since the host owns the VBA interpreter, other threads running simultaneously would either require their own interpreter, or have to wait for one thread to finish using it (in this way, the host being busy would break stuff). But for single threaded asynchronous stuff I don't see why Excel being busy should affect things – Greedo Sep 29 '19 at 13:56
5

Pointer-Safe and 64-Bit declarations for the Windows Timer API in VBA:

As promised, here are the 32-Bit and 64-Bit API declarations for the Timer API, using LongLong and the Safe Pointer type:

Option Explicit
Option Private Module
#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows ' Use LongLong and LongPtr
Private Declare PtrSafe Function SetTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As LongPtr, _ ByVal uElapse As LongLong, _ ByVal lpTimerFunc As LongPtr _ ) As Long
Public Declare PtrSafe Function KillTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As LongPtr _ ) As Long Public TimerID As LongPtr
#ElseIf VBA7 Then ' 64 bit Excel in all environments ' Use LongPtr only, LongLong is not available
Private Declare PtrSafe Function SetTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As Long, _ ByVal uElapse As Long, _ ByVal lpTimerFunc As LongPtr) As Long
Private Declare PtrSafe Function KillTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As Long) As Long
Public TimerID As LongPtr
#Else ' 32 bit Excel
Private 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
#End If

' Call the timer as: ' SetTimer 0&, 0&, lngMilliseconds, AddressOf TimerProc

#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows ' Use LongLong and LongPtr ' Note that wMsg is always the WM_TIMER message, which actually fits in a Long
Public Sub TimerProc(ByVal hwnd As LongPtr, _ ByVal wMsg As LongLong, _ ByVal idEvent As LongPtr, _ ByVal dwTime As LongLong) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****
End Sub #ElseIf VBA7 Then ' 64 bit Excel in all environments ' Use LongPtr only
Public Sub TimerProc(ByVal hwnd As LongPtr, _ ByVal wMsg As Long, _ ByVal idEvent As LongPtr, _ ByVal dwTime As Long) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****
End Sub #Else ' 32 bit Excel
Public Sub TimerProcInputBox(ByVal hwnd As Long, _ ByVal wMsg As Long, _ ByVal idEvent As Long, _ ByVal dwTime As Long) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****
End Sub #End If

The hwnd parameter is set to zero in the sample code above, and should always will be zero if you're calling this from VBA instead of associating the call with (say) an InputBox or form.

A fully-worked example of this Timer API, including the use of the hwnd parameter for a window, is available on the Excellerando website:

Using the VBA InputBox for passwords and hiding the user's keyboard input with asterisks.




**Footnote:**

This has been published as a separate reply to my explanation of the system errors associated with calling the Timer API without careful error-handling: it's a separate topic, and StackOverflow will benefit from a separate and searchable answer with the Pointer-Safe and 64-Bit declarations for the Windows Timer API.

There are bad examples of the API declarations out there on the web; and there are very few examples for the common case of VBA7 (which supports the Safe Pointer type) installed on a 32-Bit Windows environment (which doesn't support the 64-Bit 'LongLong' integer).

Nigel Heffernan
  • 4,636
  • 37
  • 41
  • 1
    +1 and... Just in case it helps anyone... for 64 Bit VBA the LongLong must be used - using longPtr will start the timer, but won't stop it! – SlowLearner Sep 28 '18 at 04:19
  • Are you sure about these declarations? `uElapse` is a UINT which is a 32-bit data type regardless of OS bitness so should always be VBA `Long`. `nIDEvent` is a UINT_PTR- pointers should be declared as `LongPtr`. I don't understand the need to treat VBA7 on 32-bit OS differently; that's what `LongPtr` is for. Generally all you need is `#If VBA7 Then myPtr As LongPtr #Else myPtr As Long` simply because LongPtr doesn't exit pre-VBA7; but they're all <64 bit hosts so `Long` is fine for pointers. `#Win64` [is rarely needed](https://stackoverflow.com/a/56940710/6609896) – Greedo Sep 08 '19 at 14:20
  • (Sorry if that was a bit terse, I was trying to fit it all in one comment) – Greedo Sep 08 '19 at 14:22
  • @Greedo - rarely needed, except for an unpleasant 'gotcha' in Excel's VBA compiler, which gets it right, exactly as stated in that 'Stack reply... Once. If you ever open your application in another environment (and I was working in a 'standard desktop' user base of 64- and 32- bit OS *and* Office versions), you'll get weird runtime errors. There may be a fix in the compile-on-demand settings, but we're not going there. – Nigel Heffernan Sep 09 '19 at 14:49
  • @SlowLearner That is false. `LongPtr` will have correct size. Using `LongLong` explicitly is wrong. – GSerg Sep 09 '19 at 15:12
  • 1
    These declarations are wrong. It is not required to test for `Win64`. The third parameter of `SetTimer` is `Long` regardless of bitness, as opposed to `LongLong`. The return value of KillTimer is `Long` in all cases too. Most importantly, `VBA7` does not indicate 64-bit Excel. `VBA7` is True in all Excels starting from Office 2010. The correct declarations are [here](https://stackoverflow.com/a/57843773/11683), they work regardless of Excel bitness and OS bitness. – GSerg Sep 09 '19 at 15:19
  • @GSerg thank you for the update. At the time I found the code to function incorrectly using `longPtr` but `LongLong` definitely _worked_ for me. Nevertheless, I'd rather be correct and will update my code accordingly (if it's not already happened), appreciate the clarification. – SlowLearner Sep 10 '19 at 22:51
  • @NigelHeffernan *"Compile-on-demand [...] we're not going there"* - can we briefly:)? If I understand correctly, [VBA compiles source code into p-code as you type](https://github.com/bontchev/pcodedmp). This process adheres to conditional-compilation rules *for the OS/Office bitness the code is compiled on*. I can see why you might get issues if, when run in a different environment, that p-code does not re-compile - if that's the case then 32-bit Office may attempt to use `LongLong` for code compiled in 64-bit Office (assuming `LongPtr` is inlined to `LongLong` at compile time in the p-code). – Greedo Sep 17 '19 at 17:10
  • Anyway, that doesn't really tell me why your code would work. The way I read it you have: `If LongPtr defined AND 64-Bit Office: Use LongLong and LongPtr(=LongLong), If LongPtr defined AND 32-Bit Office: Use LongPtr(=Long) and Long, If LongPtr NOT defined: use Long (regardless of Office - since VBA6 is always 32 bit)`. Assuming p-code is not re-compiled, code compiled on 64-bit office will use `LongLong` in 32-bit office, causing an error. 32-bit code run on 64-bit will use `Long` and break. If it is re-compiled then checking Office bitness is redundant and `LongPtr` can be used for VBA7 code – Greedo Sep 17 '19 at 17:59
  • 1
    @ Nigel Heffernan I have run the original code from Jordan Goldmeier on my Windows 10 64 bit with Office 2019 Pro Plus. In this case I don't receive an error although I don't see the pop up either. If I add your code above the orignal code I do receive a Compile error: Ambigious name : TimerProc. I have replaced all original code including and above the line : "ByVal nIDEvent As Long) As Long" with your code. Do you know what can be the cause? Thanks a lot! – user2165379 Aug 10 '20 at 19:17
  • @user2165379 - thanks for testing! 'Compile error: Ambigious name : TimerProc' means that TimerProc is declared twice, and that is likely to be a failure of the #IF compiler directives – Nigel Heffernan Aug 13 '20 at 11:17
4

I read in various places that API timers are risky in VBA

Well the statement should be I read in various places that API timers are risky? And the reason why I say that is because these APIs can be use in VB6/VBA/VB.Net etc..

So are they risky? Yup they are but then so is tight rope walking. One false move and you are done. And this is not the case with just SetTimer API but with almost any API.

I created an example way back in 2009 which uses SetTimer API to create splash screens in Excel. Here is the LINK.

Now if you extract the files and you directly open the excel file then you will see that Excel Crashes. To make it work, press the SHIFT key and then open Excel so that the macros don't run. Next change the path of the images. The new path would be the path of the images that you extracted from the zip file. once you change the path, simply save and close the file. Next time when you run it, Excel won't crash.

Here is the code in the Excel file

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, TimerSeconds As Single, tim As Boolean
Dim Counter As Long
Sub StartTimer()
    '~~ Set the timer.
    TimerSeconds = 1
    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)
    If tim = False Then
        UserForm1.Image1.Picture = LoadPicture("C:\temp\1.bmp")
        tim = True
    Else
        UserForm1.Image1.Picture = LoadPicture("C:\temp\2.bmp")
        tim = False
    End If
    Counter = Counter + 1
    If Counter = 10 Then
        EndTimer
        Unload UserForm1
    End If
End Sub

When is the API timer safe and when is it not? Are there some broad principles to help me understand?

So it all boils down to one fact. How robust is your code. If your code handles every scenario, then the SetTimer API or as a matter of fact any API will not fail.

Siddharth Rout
  • 147,039
  • 17
  • 206
  • 250
  • Thanks for the example @Siddharth Rout I have edited my post. Please see the edits in bold italics. I'm wondering if people can help to compile a set of specific guidelines or provide more examples so that we can start to infer same. – Cool Blue Nov 29 '13 at 04:07
  • Hi @mehow I read the link you provided and, with the greatest respect, I don't think it applies. I have made the original question more specific, I haven't rolled on to a new question on a completely different topic: it's the same topic and I don't feel like its been answered. – Cool Blue Nov 29 '13 at 14:44
  • 1
    @CoolBlue: `And what is the mechanism of the crash: what is happening exactly to make Excel crash?` I wouldn't be able to answer this question as I don't know what is going behind the scenes. I can't even see the code of that API (user32.dll). so it is very difficult for me to tell you why that API is crashing. – Siddharth Rout Nov 29 '13 at 14:48
  • No problem @Siddharth Rout but this is a community, so even if you are unable to answer, I remain hopefull that somebody else has some knowledge about this. :) – Cool Blue Nov 29 '13 at 14:56
2

@CoolBlue I wrote the code you posted above. It's true that APIs can act unpredictably, at least compared to normal code. However, if your code is robust enough (following @Siddharth Rout's comments from above), then it's no longer a prediction. In fact, that unpredictability comes in during development.

For example, in my first iteration of the rollover popup created above, I had accidentally typed KillTimer in the IF statement. Basically, where EndTimer exists now I had written KillTimer. I did this without thinking. I knew I had a procedure that would end the timer, but I momentarily confused EndTimer with KillTimer.

So here's why I bring this up: typically, when you make this type of mistake in Excel, you'd receive a runtime error. However, because you are working with APIs, you just get an illegal error, and the entire Excel application becomes unresponsive and quits. So, if you haven't saved before starting the timer, you lose everything (which is essentially what happened to me the first time through). Worse, because you don't receive a runtime error, you won't know immediately which line caused the error. In a project like this, you have to expect several illegal errors (and subsequent reloading of Excel) to diagnose the error. It can be a painful process, sometimes. But this is a typical debugging situation that happens when you worki with APIs. That the errors are not highlighted directly - and illegal errors appear to happen at random - are why many have described APIs as unpredictable and risky.

But they're not risky, so long as you can find and diagnose errors. In my code above, I believe I've created an essentially closed form solution. There aren't any errors someone could introduce that would cause a problem later. (Don't take that as a challenge folks.)

And just to give you some specific guidelines to avoid errors:

  • If you start a timer, ensure you kill it later. If you have an Excel runtime error before the timer is killed, it could go on forever and eat your memory. Use the console (Debug.Print) to write a line every time the TimerProc is called. If it keeps ticking away in you console even after your code is done executing, then you have a runaway timer. Quit Excel and come back in when this happens.
  • Don't use multiple timers. Use ONE timer to handle multiple timing elements.
  • Don't start a new timer without killing an old one.
  • Most important: test on your friend's computer to ensure it works across different platforms.

Also, just to be clear: there's no problem using the API timer and editing a cell at the same time. There's nothing about Timers that will preclude your ability to edit anything on the sheet.

  • "...there's no problem using the API timer and editing a cell at the same time." You are quite, quite sure that there's no problem sending a message to the Excel Application when you're editing a cell and external applications will get a 'not responding' error? If the function pointer still exists, and the memory structure for the parameters behind it is accessible, you are perfectly correct. But the majority of applications that interact with Excel in real time are unable to handle Excel's refusal to respond when a cell is being edited: we've all learned to be cautious about this. – Nigel Heffernan Jan 21 '15 at 19:01
  • @Nile Could you elaborate who sends a message to the Excel Application? What external applications are you talking about? Why would they get a "not responding" error? What function pointer? And so on. – x-yuri Sep 19 '15 at 13:36
  • @Jordan Goldmeier Could you explain `On Error Resume Next` line? What is it for in the code? Is there any better way? – x-yuri Sep 19 '15 at 14:38
  • @x-yuri, I'll take a stab at `On Error Resume Next`... One thing it does do is add a reference to the next location in the function to the `error` object and that means that the particular execution context will not be garbage collected during the lifetime of that object. If the `Kill Timer` call fails and times out, it will return to that referenced location and windows will have preserved its disposition of valid, executable memory with a properly registered owner. Otherwise, control might return to that location after the context is GCed resulting in a GPF. – Cool Blue Oct 05 '15 at 12:37
  • 1
    @x-yuri, the answer to your first question is an explanation of windows' message based architecture. It really is very nice. If you study Programming Windows (Developer Reference) by Charles Petzold, you will learn all about it. – Cool Blue Oct 05 '15 at 12:44
  • @CoolBlue I read [this one](http://www.amazon.com/Programming-Applications-Microsoft-Windows-Series/dp/1572319968), and some others, and wrote a bunch of apps using WinAPI. It was 10 or so years ago though. Anyway, I fail to understand what you're talking about. We have WinAPI calling VBA's callback. What messages? As to `On Error Resume Next`, I didn't mean to ask how it works. I meant, why do we need it in the first place. What error might occur during its execution? You seem to generalize the issue a lot, and as such it becomes difficult to understand. – x-yuri Oct 07 '15 at 19:52
  • ...I have [this simple example](http://stackoverflow.com/questions/32669344/how-to-deal-with-runtime-error-50290-when-using-settimer-api), no `KillTimer`. But the error apparently still occurs. Considering your answer, I don't understand what is execution context, and why it may be GCed? – x-yuri Oct 07 '15 at 19:55
0

I have also been faced with the fact that Excel crashes while entering a value and found this contribution. Great! My problem was solved as soon I added this line:

On Error Resume Next 

to "TimerProc".

0

With the API timer, as soon as I set the time lapse too short, Excel would crash because it had not finished the previous timed task before the next was scheduled. This does not happen with ontime because you set ontime after finishing TimerProc.

Maybe it is possible to kill the timer first thing in Timerproc, and set a new one just before finishing.

You should be aware that Killtimer actually fails at times, leaving the timer alive and continuing to call the procedure forever. So a machine gun code with feedback control is necessary to be sure it is really dead.

//pseudo code :
start_kill = timer()
While still_alive= (negative result of killtimer) do
Still_ailve = KillTimer TimerID.
If timer - start_kill > 10 then msgbox "not dead     find a bigger gun" Exit sub
Wend

Of course you need a time out to exit this loop.

RalfFriedl
  • 1,134
  • 3
  • 11
  • 12
gudule
  • 1