类别:VC语言 点击:0 评论:0 推荐:

You only need to know about two functions to use timers. CWnd::SetTimer programs a timer to fire at specified intervals, and CWnd::KillTimer stops a running timer. Depending on the parameters passed to SetTimer, a timer notifies an application that a time interval has elapsed in one of two ways:

By sending a specified window a WM_TIMER message

By calling an application-defined callback function

The WM_TIMER method is the simpler of the two, but the callback method is sometimes preferable, particularly when multiple timers are used. Both types of timer notifications receive low priority when they are sent to an application. They are processed only when the message queue is devoid of other messages.

Timer notifications are never allowed to stack up in the message queue. If you set a timer to fire every 100 milliseconds and a full second goes by while your application is busy processing other messages, it won't suddenly receive ten rapid-fire timer notifications when the message queue empties. Instead, it will receive just one. You needn't worry that if you take too much time to process a timer notification, another will arrive before you're finished with the previous one and start a race condition. Still, a Windows application should never spend an excessive amount of time processing a message unless processing has been delegated to a background thread because responsiveness will suffer if the primary thread goes too long without checking the message queue.

Setting a Timer: Method 1

The easiest way to set a timer is to call SetTimer with a timer ID and a timer interval and then map WM_TIMER messages to an OnTimer function. A timer ID is a nonzero value that uniquely identifies the timer. When OnTimer is activated in response to a WM_TIMER message, the timer ID is passed as an argument. If you use only one timer, the ID value probably won't interest you because all WM_TIMER messages will originate from the same timer. An application that employs two or more timers can use the timer ID to identify the timer that generated a particular message.

The timer interval passed to SetTimer specifies the desired length of time between consecutive WM_TIMER messages in thousandths of a second. Valid values range from 1 through the highest number a 32-bit integer will hold: 232 - 1 milliseconds, which equals slightly more than 49½ days. The statement

SetTimer (1, 500, NULL);

allocates a timer, assigns it an ID of 1, and programs it to send the window whose SetTimer function was called a WM_TIMER message every 500 milliseconds. The NULL third parameter configures the timer to send WM_TIMER messages rather than call a callback function. Although the programmed interval is 500 milliseconds, the window will actually receive a WM_TIMER message about once every 550 milliseconds because the hardware timer on which Windows timers are based ticks once every 54.9 milliseconds, give or take a few microseconds, on most systems (particularly Intel-based systems). In effect, Windows rounds the value you pass to SetTimer up to the next multiple of 55 milliseconds. Thus, the statement

SetTimer (1, 1, NULL);

programs a timer to send a WM_TIMER message roughly every 55 milliseconds, as does the statement

SetTimer (1, 50, NULL);

But change the timer interval to 60, as in

SetTimer (1, 60, NULL);

and WM_TIMER messages will arrive, on average, every 110 milliseconds.

How regular is the spacing between WM_TIMER messages once a timer is set? The following list of elapsed times between timer messages was taken from a 32-bit Windows application that programmed a timer to fire at 500-millisecond intervals:

Notification No. Interval Notification No. Interval 1 0.542 second 11 0.604 second 2 0.557 second 12 0.550 second 3 0.541 second 13 0.549 second 4 0.503 second 14 0.549 second 5 0.549 second 15 0.550 second 6 0.549 second 16 0.508 second 7 1.936 seconds 17 0.550 second 8 0.261 second 18 0.549 second 9 0.550 second 19 0.549 second 10 0.549 second 20 0.550 second

As you can see, the average elapsed time is very close to 550 milliseconds, and most of the individual elapsed times are close to 550 milliseconds, too. The only significant perturbation, the elapsed time of 1.936 seconds between the sixth and seventh WM_TIMER messages, occurred as the window was being dragged across the screen. It's obvious from this list that Windows doesn't allow timer messages to accumulate in the message queue. If it did, the window would have received three or four timer messages in quick succession following the 1.936-second delay.

The lesson to be learned from this is that you can't rely on timers for stopwatch-like accuracy. If you write a clock application that programs a timer for 1,000-millisecond intervals and updates the display each time a WM_TIMER message arrives, you shouldn't assume that 60 WM_TIMER messages means that 1 minute has passed. Instead, you should check the current time whenever a message arrives and update the clock accordingly. Then if the flow of timer messages is interrupted, the clock's accuracy will be maintained.

If you write an application that demands precision timing, you can use Windows multimedia timers in lieu of conventional timers and program them for intervals of 1 millisecond or less. Multimedia timers offer superior precision and are ideal for specialized applications such as MIDI sequencers, but they also incur more overhead and can adversely impact other processes running in the system.

The value returned by SetTimer is the timer ID if the function succeeded or 0 if it failed. In 16-bit versions of Windows, timers were a shared global resource and only a limited number were available. In 32-bit Windows, the number of timers the system can dole out is virtually unlimited. Failures are rare, but it's still prudent to check the return value just in case the system is critically low on resources. (Don't forget, too, that a little discretion goes a long way. An application that sets too many timers can drag down the performance of the entire system.) The timer ID returned by SetTimer equals the timer ID specified in the function's first parameter unless you specify 0, in which case SetTimer will return a timer ID of 1. SetTimer won't fail if you assign two or more timers the same ID. Rather, it will assign duplicate IDs as requested.

You can also use SetTimer to change a previously assigned time-out value. If timer 1 already exists, the statement

SetTimer (1, 1000, NULL);

reprograms it for intervals of 1,000 milliseconds. Reprogramming a timer also resets its internal clock so that the next notification won't arrive until the specified time period has elapsed.

Responding to WM_TIMER Messages

MFC's ON_WM_TIMER message-map macro directs WM_TIMER messages to a class member function named OnTimer. OnTimer is prototyped as follows:

afx_msg void OnTimer (UINT nTimerID)

nTimerID is the ID of the timer that generated the message. You can do anything in OnTimer that you can do in other message processing functions, including grabbing a device context and painting in a window. The following code sample uses an OnTimer handler to draw ellipses of random sizes and colors in a frame window's client area. The timer is programmed for 100-millisecond intervals in the window's OnCreate handler:

BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_CREATE () ON_WM_TIMER () END_MESSAGE_MAP () int CMainWindow::OnCreate (LPCREATESTRUCT lpcs) { if (CFrameWnd::OnCreate (lpcs) == -1) return -1; if (!SetTimer (ID_TIMER_ELLIPSE, 100, NULL)) { MessageBox (_T ("Error: SetTimer failed")); return -1; } return 0; } void CMainWindow::OnTimer (UINT nTimerID) { CRect rect; GetClientRect (&rect); int x1 = rand () % rect.right; int x2 = rand () % rect.right; int y1 = rand () % rect.bottom; int y2 = rand () % rect.bottom; CClientDC dc (this); CBrush brush (RGB (rand () % 255, rand () % 255, rand () % 255)); CBrush* pOldBrush = dc.SelectObject (&brush); dc.Ellipse (min (x1, x2), min (y1, y2), max (x1, x2), max (y1, y2)); dc.SelectObject (pOldBrush); }

Here's how the same code fragment would look if the application were modified to use two timers—one for drawing ellipses and another for drawing rectangles:

BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_CREATE () ON_WM_TIMER () END_MESSAGE_MAP () int CMainWindow::OnCreate (LPCREATESTRUCT lpcs) { if (CFrameWnd::OnCreate (lpcs) == -1) return -1; if (!SetTimer (ID_TIMER_ELLIPSE, 100, NULL) ¦¦ !SetTimer (ID_TIMER_RECTANGLE, 100, NULL)) { MessageBox (_T ("Error: SetTimer failed")); return -1; } return 0; } void CMainWindow::OnTimer (UINT nTimerID) { CRect rect; GetClientRect (&rect); int x1 = rand () % rect.right; int x2 = rand () % rect.right; int y1 = rand () % rect.bottom; int y2 = rand () % rect.bottom; CClientDC dc (this); CBrush brush (RGB (rand () % 255, rand () % 255, rand () % 255)); CBrush* pOldBrush = dc.SelectObject (&brush); if (nTimerID == ID_TIMER_ELLIPSE) dc.Ellipse (min (x1, x2), min (y1, y2), max (x1, x2), max (y1, y2)); else // nTimerID == ID_TIMER_RECTANGLE dc.Rectangle (min (x1, x2), min (y1, y2), max (x1, x2), max (y1, y2)); dc.SelectObject (pOldBrush); }

As you can see, this version of OnTimer inspects the nTimerID value passed to it to decide whether to draw an ellipse or a rectangle.

You might not write too many applications that draw ellipses and rectangles endlessly, but using timer messages to execute a certain task or a sequence of tasks repeatedly provides an easy solution to a common problem encountered in Windows programming. Suppose you write an application with two push button controls labeled "Start" and "Stop" and that clicking the Start button starts a drawing loop that looks like this:

m_bContinue = TRUE; while (m_bContinue) DrawRandomEllipse ();

The loop draws ellipses over and over until the Stop button is clicked, which sets m_bContinue to FALSE so that the while loop will fall through. It looks reasonable, but try it and you'll find that it doesn't work. Once Start is clicked, the while loop runs until the Windows session is terminated or the application is aborted with Task Manager. Why? Because the statement that sets m_bContinue to FALSE gets executed only if the WM_COMMAND message generated by the Stop button is retrieved, dispatched, and routed through the message map to the corresponding ON_COMMAND handler. But as long as the while loop is spinning in a continuous cycle without checking for messages, the WM_COMMAND message sits idly in the message queue, waiting to be retrieved. m_bContinue never changes from TRUE to FALSE, and the program gets stuck in an infinite loop.

You can solve this problem in several ways. One solution is to do the drawing in a secondary thread so that the primary thread can continue to pump messages. Another is to add a message pump to the while loop to periodically check the message queue as ellipses are drawn. A third solution is to draw ellipses in response to WM_TIMER messages. In between WM_TIMER messages, other messages will continue to be processed as normal. The only drawback to this solution is that drawing ellipses at a rate of more than about 18 per second requires multiple timers, whereas a thread that starts drawing the next ellipse as soon as the previous one is finished might draw hundreds of ellipses per second, depending on the speed of the video hardware and the sizes of the ellipses.

An important point to take home here is that WM_TIMER messages are not processed asynchronously with respect to other messages. That is, one WM_TIMER message will never interrupt another WM_TIMER message in the same thread, nor will it interrupt a nontimer message, for that matter. WM_TIMER messages wait their turn in the message queue just as other messages do and aren't processed until they are retrieved and dispatched by the message loop. If a regular message handling function and an OnTimer function use a common member variable, you can safely assume that accesses to the variable won't overlap as long as the two message handlers belong to the same window or to windows running on the same thread.

Setting a Timer: Method 2

Timers don't have to generate WM_TIMER messages. If you prefer, you can configure a timer to call a callback function inside your application rather than send it a WM_TIMER message. This method is often used in applications that use multiple timers so that each timer can be assigned a unique handling function.

A common misconception among Windows programmers is that timer callbacks are processed more expediently than timer messages because callbacks are called directly by the operating system whereas WM_TIMER messages are placed in the message queue. In reality, timer callbacks and timer messages are handled identically up to the point at which ::DispatchMessage is called. When a timer fires, Windows sets a flag in the message queue to indicate that a timer message or callback is waiting to be processed. (The on/off nature of the flag explains why timer notifications don't stack up in the message queue. The flag isn't incremented when a timer interval elapses but is merely set to "on.") If ::GetMessage finds that the message queue is empty and that no windows need repainting, it checks the timer flag. If the flag is set, ::GetMessage builds a WM_TIMER message that is subsequently dispatched by ::DispatchMessage. If the timer that generated the message is of the WM_TIMER variety, the message is dispatched to the window procedure. But if a callback function is registered instead, ::DispatchMessage calls the callback function. Therefore, callback timers enjoy virtually no performance advantage over message timers. Callbacks are subject to slightly less overhead than messages because neither a message map nor a window procedure is involved, but the difference is all but immeasurable. In practice, you'll find that WM_TIMER-type timers and callback timers work with the same regularity (or irregularity, depending on how you look at it).

To set a timer that uses a callback, specify the name of the callback function in the third parameter to SetTimer, like this:

SetTimer (ID_TIMER, 100, TimerProc);

The callback procedure, which is named TimerProc in this example, is prototyped as follows:

void CALLBACK TimerProc (HWND hWnd, UINT nMsg, UINT nTimerID, DWORD dwTime)

The hWnd parameter to TimerProc contains the window handle, nMsg contains the message ID WM_TIMER, nTimerID holds the timer ID, and dwTime specifies the number of milliseconds that have elapsed since Windows was started. (Some documentation says that dwTime "specifies the system time in Coordinated Universal Time format." Don't believe it; it's a bug in the documentation.) The callback function should be a static member function or a global function to prevent a this pointer from being passed to it. For more information on callback functions and the problems that nonstatic member functions pose for C++ applications, refer to Chapter 7.

One obstacle you'll encounter when using a static member function as a timer callback is that the timer procedure doesn't receive a user-defined lParam value as some Windows callback functions do. When we used a static member function to field callbacks from ::EnumFontFamilies in Chapter 7, we passed a CMainWindow pointer in lParam to permit the callback function to access nonstatic class members. In a timer procedure, you have to obtain that pointer by other means if you want to access nonstatic function and data members. Fortunately, you can get a pointer to your application's main window with MFC's AfxGetMainWnd function:

CMainWindow* pMainWnd = (CMainWindow*) AfxGetMainWnd ();

Casting the return value to a CMainWindow pointer is necessary if you want to access CMainWindow function and data members because the pointer returned by AfxGetMainWnd is a generic CWnd pointer. Once pMainWnd is initialized in this way, a TimerProc function that is also a member of CMainWindow can access nonstatic CMainWindow function and data members as if it, too, were a nonstatic member function.

Stopping a Timer

The counterpart to CWnd::SetTimer is CWnd::KillTimer, which stops a timer and stops the flow of WM_TIMER messages or timer callbacks. The following statement releases the timer whose ID is 1:

KillTimer (1);

A good place to kill a timer created in OnCreate is in the window's OnClose or OnDestroy handler. If an application fails to free a timer before it terminates, 32-bit versions of Windows will clean up after it when the process ends. Still, good form dictates that every call to SetTimer should be paired with a call to KillTimer to ensure that timer resources are properly deallocated.