Timer Loops
Animating the card backs illustrates one of the fundamental problems of Windows programming. Before we get to the specifics of making cards wink and flash, however, let’s talk about the problem in general.
Let’s say that you want to perform some operation (such as animation) in the background forever. This is easy in MS-DOS:
Do
Draw it, x, y
If x <= xMax Then x = x + 1 Else x = 0
If y <= yMax Then y = y + 1 Else y = 0
Loop
Even in MS-DOS, however, this often doesn’t work very well. Your animation might run too fast, or it might run too fast on some machines and too slowly on others. To even things out, you can insert a Wait statement:
Do
Draw it, x, y
If x <= xMax Then x = x + 1 Else x = 0
If y <= yMax Then y = y + 1 Else y = 0
Wait 100 ‘ microseconds
Loop
This might work fine for a machine running only MS-DOS, but depending on how Wait was implemented, it might be very rude indeed under 16-bit Windows. In the bad old days, Wait functions were often written as busy loops:
Sub Wait(msWait As Long)
Dim msEnd As Long
msEnd = GetTickCount() + msWait ‘ Get microseconds
Do
Loop While GetTickCount() < msEnd
End Sub
This is the height of bad manners in any non-preemptive multitasking operating system because you are grabbing the processor and throwing away all cycles until you are finished, thus blocking all other programs. A preemptive multitasking operating system such as Windows 95 or Windows NT might be able to jump in and steal control, but you’re nevertheless wasting a time slice that could be better used by someone else. Even your MS-DOS programs shouldn’t do this because it will cause them to hog the system when running in a Windows MS-DOS session.
One Visual Basic solution is to put DoEvents in the busy loop:
Sub DoWaitEvents(msWait As Long)
Dim msEnd As Long
msEnd = GetTickCount + msWait
Do
DoEvents
Loop While GetTickCount < msEnd
End Sub
This procedure resides in the UTILITY module in VBCore, and is a reasonable way to wait for short periods. I wouldn’t use it to wait for more than half a second or so. The DoEvents call releases your time slice to any other processes that are waiting for their turn to run. If DoEvents were written in Visual Basic, it would look something like this:
Do While PeekMessage(msg, pNull, 0, 0, PM_REMOVE)
TranslateMessage msg
DispatchMessage msg
Loop
Sleep 0
In other words, DoEvents handles all pending messages and surrenders its time slice before returning to deal with your next message.
Notice the call to Sleep at the end of the loop. I’ve heard quite a bit of debate about the relative merits of Sleep 0 versus Sleep 1 for giving up your time slice. Here’s what the documentation says: “A value of zero causes the thread to relinquish the remainder of its time slice to any other thread of equal priority that is ready to run.” This means that if other threads aren’t quite ready or aren’t of equal priority, they won’t run. I’ve seen tests indicating that Sleep 1 is often a more effective way of yielding.
So if Sleep 1 is good way to wait for a very short time, why wouldn’t Sleep 1000 be a good way of waiting for one second? The problem with Sleep is that it really sleeps. Your program gets absolutely no processing time and will not be able to paint or do anything else until the sleep is over. Generally, Sleep is useful for multithreaded applications in which one thread can sleep while another continues working. Sleep is usually a bad idea for a normal Visual Basic program that has only one thread. We’ll talk more about threads, processes, and waiting in Chapter 11.
The real problem with using Sleep or DoEvents to wait is that it’s not the Windows Way. If other processes use too much time, you’ll be way past quitting time when you get control back. In any multitasking system, someone else could want the same time slot you want. Your best chance of getting control at a specific time is to request it politely using a Windows Timer—which, in Visual Basic, means using the Timer control or a timer class.
You’re probably not accustomed to thinking of the Timer control as just another looping structure comparable to Do/Loop or For/Next. But why not? Consider the following “bad” loop:
Dim x As Integer, y As Integer, secStop As Double
Do
If x <= xMax Then x = x + 1 Else x = 0
If y <= yMax Then y = y + 1 Else y = 0
If Draw(it, x, y) = False Then Exit Do
secStop = Timer + .1 ‘ Wait one-tenth second
Do
DoEvents
Loop Until Timer > secStop
Loop
Notice the following points:
-
This loop is an endless Do Loop.
-
The loop has an exit in the middle via Exit Do.
-
The loop uses the Timer function in a loop to wait one tenth of a
second.
-
The loop uses normal local variables, reinitializing them when they
exceed a maximum.
Now let’s convert to a “good” loop:
tmrAnimate.Interval = 100 ‘ 100 microseconds is one-tenth second
§
Sub tmrAnimate_Timer()
Static x As Integer, y As Integer
If x <= xMax Then x = x + 1 Else x = 0
If y <= yMax Then y = y + 1 Else y = 0
If Draw(it, x, y) = False Then tmrAnimate.Enabled = False
End Sub
Here’s how the code is transformed:
-
A Sub statement replaces Do; an End Sub replaces Loop.
-
The exit changes from Exit Do to tmrAnimate.Enabled = False.
-
The time period is set with the Interval property outside the loop.
-
The variables must be declared static because you’ll leave the loop on every iteration, but you want them to be unchanged when you come back.
We’ll see how this technique works in the Fun ’n Games program after a brief look at the CTimer class.