Windows编程第十一回 三问计时器

啥是计数器?

计时器是一种输入设备,它周期性地在每经过一个指定的时间间隔后就通知应用程序一次。当你的程序将时间间隔告诉Windows,例如“每10秒钟通知我一声”,然后Windows给你的程序发送周期性发生的WM_TIMER消息以表示时间到了。

我们可以通过调用SetTimer函数为的Windows程序分配一个定时器。SetTimer有一个时间间隔范围为1毫秒到4,294,967,295毫秒(将近50天)的整型参数,这个值指示Windows每隔多久时间给程序发送WM_TIMER消息。例如,如果间隔为1000毫秒,那么Windows将每秒给程序发送一个WM_TIMER消息。

当你的程序用完定时器时,它调用KillTimer函数来停止计时器消息。在处理WM_TIMER消息时,你可以通过调用KillTimer函数来编写一个“瞬间”的定时器。KillTimer调用除了会销毁以前调用SetTimer创建的定时器事件,还会清除消息队列中尚未被处理的WM_TIMER消息,从而使程序在调用KillTimer之后就不会再接收到WM_TIMER消息。

下面就介绍一下计时器的使用方法吧。

计时器怎么用?

如果你需要在整个程序执行期间都使用计时器,那么你将得从WinMain函数中或者在处理WM_CREATE消息时调用SetTimer,并在退出WinMain或响应WM_DESTROY消息时调用KillTimer。根据调用SetTimer时使用的参数,可以选择以下两种方法之一来使用计时器。

 

方法一

这是最方便的一种方法,它让Windows把WM_TIMER消息发送到应用程序的正常窗口过程中,SetTimer调用如下所示:

SetTimer (hwnd, 1, uiMsecInterval, NULL) ;

第一个参数是其窗口过程将接收WM_TIMER消息的窗口句柄。第二个参数是定时器ID,它是一个非0数值,在整个例子中假定为1。第三个参数是一个32位无符号整数,以毫秒为单位指定一个时间间隔,一个60,000的值将使Windows每分钟发送一次WM_TIMER消息。

你可以通过调用

KillTimer (hwnd, 1) ;

在任何时刻停止WM_TIMER消息(即使正在处理WM_TIMER消息)。此函数的第二个参数是SetTimer调用中所用的同一个定时器ID。在终止程序之前,你应该在响应WM_DESTROY消息中停止任何活动的定时器。

当你的窗口过程收到一个WM_TIMER消息①时,wParam参数等于定时器的ID值(上述情形为1),lParam参数为0。为了使程序更具有可读性,您可以使用#define叙述定义不同的定时器ID:

#define TIMER_SEC 1

#define TIMER_MIN 2

然后你可以使用两个SetTimer调用来设定两个定时器:

SetTimer (hwnd, TIMER_SEC, 1000, NULL) ;

SetTimer (hwnd, TIMER_MIN, 60000, NULL) ;

WM_TIMER的处理如下所示:

caseWM_TIMER:

       

    switch (wParam)

       

    {

       

    case TIMER_SEC:

       

            //每秒一次的处理

       

                  break ;

       

    case TIMER_MIN:

       

            //每分钟一次的处理

       

            break ;

       

    }

       

return 0 ;

       

如果你想将一个已经存在的定时器设定为不同的时间间隔,您可以简单地用不同的时间值再次调用SetTimer。

 

方法二

设定计时器的第一种方法是把WM_TIMER消息发送到通常的窗口过程,而第二种方法是让Windows直接将计时器消息发送给你程序的另一个函数。

接收这些计时器消息的函数被称为回调函数,这是一个在你的程序之中但是由Windows调用的函数(在第四回曾提到)。你先告诉Windows此函数的地址,然后Windows调用此函数。这看起来也很熟悉,因为程序的窗口过程实际上也是一种回调函数。当注册窗口类时,要将函数的地址告诉Windows,当发送消息给程序时,Windows会调用此函数。

像窗口过程一样,回调函数也必须定义为CALLBACK,因为它是由Windows从程序的程序代码段调用的。callback函数的参数和callback函数的返回值取决于callback函数的目的。跟计时器有关的callback函数中,输入参数与窗口过程的输入参数一样。计时器callback函数不向Windows返回值。

我们把以下的callback函数称为TimerProc(你能够选择与其它一些用语不会发生冲突的任何名称),它只处理WM_TIMER消息:

VOID CALLBACK TimerProc (  HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime)

       

{

       

           //处理WM_TIMER消息

       

}

       

TimerProc的参数hwnd是在调用SetTimer时指定的窗口句柄。Windows只把WM_TIMER消息送给TimerProc,因此消息参数message总是等于WM_TIMER。iTimerID值是计时器ID,dwTimer值是与从GetTickCount函数的返回值相容的值。这是自Windows启动后所经过的毫秒数。

用第一种方法设定计时器时要求下面格式的SetTimer调用:

SetTimer (hwnd, iTimerID, iMsecInterval, NULL) ;

你使用回调函数处理WM_TIMER消息时,SetTimer的第四个参数由回调函数的地址取代,如下所示:

SetTimer (hwnd, iTimerID, iMsecInterval, TimerProc) ;

 

看个例子吧。

  1 #include <windows.h>
  2         
  3 #define ID_TIMER    1
  4         
  5 
  6 LRESULT     CALLBACK       WndProc   (HWND, UINT, WPARAM, LPARAM) ;
  7         
  8 VOID    CALLBACK   TimerProc (HWND, UINT, UINT,   DWORD ) ;
  9         
 10 
 11 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
 12         
 13                                                          PSTR szCmdLine, int iCmdShow)
 14         
 15 {
 16         
 17    static char   szAppName[]           = " Timer Demo " ;
 18         
 19            HWND                                 hwnd ;
 20         
 21            MSG                                  msg ;
 22         
 23            WNDCLASS                      wndclass ;
 24         
 25    
 26         
 27            wndclass.style                                       = CS_HREDRAW | CS_VREDRAW ;
 28         
 29            wndclass.lpfnWndProc                                 = WndProc ;
 30         
 31           wndclass.cbClsExtra                                  = 0 ;
 32         
 33            wndclass.cbWndExtra                                  = 0 ;
 34         
 35            wndclass.hInstance                                   = hInstance ;
 36         
 37            wndclass.hIcon                                       = LoadIcon (NULL, IDI_APPLICATION) ;
 38         
 39            wndclass.hCursor                                     = LoadCursor (NULL, IDC_ARROW) ;
 40         
 41           wndclass.hbrBackground                       = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
 42         
 43            wndclass.lpszMenuName                        = NULL ;
 44         
 45            wndclass.lpszClassName                       = szAppName ;
 46         
 47    
 48         
 49            if (!RegisterClass (&wndclass))
 50         
 51            {
 52         
 53                   MessageBox (  NULL, TEXT ("Program requires Windows NT!"),
 54         
 55                                                                         szAppName, MB_ICONERROR) ;
 56         
 57                   return 0 ;
 58         
 59            }
 60         
 61    
 62         
 63            hwnd = CreateWindow ( szAppName, "Timer Demo",WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT,
 70         
 71                         NULL, NULL, hInstance, NULL) ;
 72         
 73    
 74         
 75            ShowWindow (hwnd, iCmdShow) ;
 76         
 77            UpdateWindow (hwnd) ;
 78         
 79         
 80         
 81            while (GetMessage (&msg, NULL, 0, 0))
 82         
 83            {
 84         
 85                   TranslateMessage (&msg) ;
 86         
 87                   DispatchMessage (&msg) ;
 88         
 89            }
 90         
 91            return msg.wParam ;
 92         
 93 }
 94         
 95 
 96 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
 97         
 98 {
 99         
100    switch (message)
101         
102     {
103         
104            case   WM_CREATE:
105         
106                   SetTimer (hwnd, ID_TIMER, 1000, TimerProc) ;
107         
108                  return 0 ;
109         
110        
111         
112            case   WM_DESTROY:
113         
114                   KillTimer (hwnd, ID_TIMER) ;
115         
116                   PostQuitMessage (0) ;
117         
118                   return 0 ;
119         
120            }
121         
122            return DefWindowProc (hwnd, message, wParam, lParam) ;
123         
124 }
125         
126 
127 VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime)
128         
129 {
130         
131            static BOOL fFlipFlop = FALSE ;
132         
133            HBRUSH                        hBrush ;
134         
135            HDC                                  hdc ;
136         
137            RECT                                 rc ;
138        
139            fFlipFlop = !fFlipFlop ;
140         
141    
142         
143            GetClientRect (hwnd, &rc) ;
144         
145           hdc = GetDC (hwnd) ;
146         
147     hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ;
148                               //括号里的这种判断语句我估计大家都懂,就不解释了
149    
150         
151     FillRect (hdc, &rc, hBrush) ;
152         
153    ReleaseDC (hwnd, hdc) ;
154         
155    DeleteObject (hBrush) ;
156         
157 }

这里计时器的时间间隔设定为1秒。当它收到WM_TIMER消息时,它将显示区域的颜色由蓝色变为红色或由红色变为蓝色。

程序在窗口过程处理WM_CREATE消息时设定计时器。在处理WM_TIMER消息处理期间,翻转bFlipFlop的值并使窗口无效以产生WM_PAINT消息。在处理WM_PAINT消息处理期间,通过调用GetClientRect获得窗口大小的RECT结构,并通过调用FillRect改变窗口的颜色。

计时器精确吗?

很可惜它不精确,原因如下。

原因一:Windows计时器是PC硬件和ROM BIOS构造的计时器逻辑的一种相对简单的扩展。回到Windows以前的MS-DOS编程,应用程序能够通过捕获称为timer tick的BIOS中断来实现时钟或计时器。这些中断每54.915毫秒产生一 次,或者大约每秒18.2次。一些为MS-DOS编写的程序自己捕获这个硬件中断以实现时钟和计时器。这是原始的IBM PC的微处理器频率值4.772720 MHz被262144所除而得出的结果。Windows应用程序不拦截BIOS中断,相反地,Windows本身处理硬件中断,这样应用程序就不必进行处理。在Windows 98中,计时器与其下的PC计时器一样具有55毫秒的分辨率,在Microsoft Windows NT中,计时器的分辨率为10毫秒。即Windows应用程序不能以高于这些分辨率的频率(在Windows 98下,每秒18.2次,在Windows NT下,每秒大约100次)接收WM_TIMER消息。在SetTimer中指定的时间间隔总是截尾后tick数的整数倍。例如,1000毫秒的间隔除以 54.925毫秒,得到18.207个tick,截尾后是18个tick,它实际上是989毫秒。对每个小于55毫秒的间隔,每个tick都会产生一个 WM_TIMER消息。 

  可见,计时器并不能严格按照指定的时间间隔发送WM_TIMER消息,它总要相差那么几毫秒。 

 

  即使忽略这几个毫秒的差别,计时器仍然不精确。请看原因二: 

   WM_TIMER消息放在正常的消息队列之中,和其他消息排列在一起,因此,如果在SetTimer中指定间隔为1000毫秒,那么不能保证程序每 1000毫秒或者989毫秒就会收到一个WM_TIMER消息。如果其他程序的运行时间超过一秒,在此期间内,你的程序将收不到任何WM_TIMER消息。事实上, Windows对WM_TIMER消息的处理非常类似于对WM_PAINT消息的处理,这两个消息都是低优先级的,程序只有在消息队列中没有其他消息时才接收它们。 

  WM_TIMER还在另一方面和WM_PAINT相似:Windows不能持续向消息队列中放入多个 WM_TIMER消息,而是将多余的WM_TIMER消息组合成一个消息。因此,应用程序不会一次收到多个这样的消息,尽管可能在短时间内得到两个 WM_TIMER消息。应用程序不能确定这种处理方式所导致的WM_TIMER消息“丢失”的数目。 

  可见,WM_TIMER消息并不能及时被应用程序所处理,WM_TIMER在消息队列中的延误可能就不能用毫秒来计算了。 

 

   由以上两点,你不能通过在处理WM_TIMER时一秒一秒计数的方法来计时。如果要实现一个时钟程序,可以使用系统的时间函数如 GetLocalTime ,而在时钟程序中,计时器的作用是定时调用GetLocalTime获得新的时间并刷新时钟画面,当然这个刷新的间隔要等于或小于1秒。 

①WM_TIMER消息。

    wParam为计数器的ID:如果需要设定多个计时器,那么对每个计时器都使用不同的计

时器ID。wParam的值将随传递到窗口过程的WM_TIMER消息的不同而不同。

    lParam为指向TimerProc的指针,如果调用SetTimer时没有指定TimerProc(其参数值

为NULL,即第一种用法),则lParam为0,显然在第二种用法中此值就不为0了。

 注:部分内容参考《Windows计时器》一文

posted @ 2012-04-29 20:00  hu_jiacheng  阅读(1699)  评论(2编辑  收藏  举报