Timers

Timers

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.IntervalNotification No.Interval
10.542 second110.604 second
20.557 second120.550 second
30.541 second130.549 second
40.503 second140.549 second
50.549 second150.550 second
60.549 second160.508 second
71.936 seconds170.550 second
80.261 second180.549 second
90.550 second190.549 second
100.549 second200.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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
vue-timers 是一个基于 Vue.js 的定时器插件,它提供了一种简单的方式在 Vue 组件中管理和处理定时任务。它可以帮助开发者在 Vue 应用程序中处理各种定时操作,如定期更新数据、显示时间、执行延时操作等。 vue-timers 的主要特点是易于使用和灵活性。它提供了一组内置的指令和方法,可以轻松地管理定时器。通过使用指令,我们可以直接在模板中设置定时器,而不需要在 JavaScript 代码中编写额外的逻辑。这大大简化了定时器的使用过程,并且可以充分利用 Vue.js 的响应式能力。 另外,vue-timers 还支持循环定时器、一次性定时器以及延时执行等功能。我们可以根据需求来选择合适的方式来处理定时任务。它还提供了一些实用的方法,如取消定时器、暂停和恢复定时器等,使得我们在处理定时任务时更加灵活和可控。 使用 vue-timers,我们可以轻松地在 Vue 组件中实现各种复杂的定时操作,而不需要编写大量的代码或依赖外部库。不仅如此,vue-timers 还与 Vue 生态系统完美集成,可以充分发挥 Vue.js 的强大特性,如组件的数据绑定和响应式更新等。 总之,vue-timers 是一个功能强大且易于使用的 Vue.js 定时器插件,它为我们处理定时任务提供了便捷和灵活的方法。无论是在单个组件中还是在整个应用程序中使用,vue-timers 都能够满足我们的需求,并提供了丰富的功能和选项来处理各种定时操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值