📄 8. 定时器.txt
字号:
定时器
智能中国——游戏组 整理编译
--------------------------------------------------------------------------------
Microsoft Windows定时器是一种输入设备,它周期性地在每经过一个指定的时间间隔后就通知应用程序一次。您的程序将时间间隔告诉Windows,例如「每10秒钟通知我一声」,然后Windows给您的程序发送周期性发生的WM_TIMER消息以表示时间到了。
初看之下,Windows定时器似乎不如键盘和鼠标设备重要,而且对许多应用程序来说确实如此。但是,定时器比您可能认为的要重要得多,它不只用于计时程序,比如出现在工具列中的Windows时钟和这一章中的两个时钟程序。下面是Windows定时器的其它应用,有些可能并不那么明显:
多任务虽然Windows 98是一个优先权式的多任务环境,但有时候如果程序尽快将控制传回给Windows效率会更高。如果一个程序必须进行大量的处理,那么它可以将作业分成小块,每接收到一个WM_TIMER消息处理一块(我将在第二十章中对此做更多的讨论)。
维护更新过的状态报告程序可以利用定时器来显示持续变化信息的「实时」更新,比如关于系统资源的变化或某个任务的进展情况。
实作「自动储存」功能定时器提示Windows程序在指定的时间过去后把使用者的工作储存到磁盘上。
终止程序展示版本的执行一些程序的展示版本被设计成在其开始后,多长时间结束,比如说,30分钟。如果时间已到,那么定时器就会通知应用程序。
步进移动游戏中的图形对象或计算机辅助教学程序中的连续显示,需要按指定的速率来处理。利用定时器可以消除由于微处理器速度不同而造成的不一致。
多媒体播放CD声音、声音或音乐的程序通常在背景播放声音数据。一个程序可以使用定时器来周期性地检查已播放了多少声音数据,并据此协调屏幕上的视觉信息。
另一项应用可以保证程序在退出窗口消息处理程序后,能够重新得到控制。在大多数时情况下,程序不能够知道何时下一个消息会到来。
定时器入门
您可以通过呼叫SetTimer函数为您的Windows程序分配一个定时器。SetTimer有一个时间间隔范围为1毫秒到4,294,967,295毫秒(将近50天)的整数型态参数,这个值指示Windows每隔多久时间给您的程序发送WM_TIMER消息。例如,如果间隔为1000毫秒,那么Windows将每秒给程序发送一个WM_TIMER消息。
当您的程序用完定时器时,它呼叫KillTimer函数来停止定时器消息。在处理WM_TIMER消息时,您可以通过呼叫KillTimer函数来编写一个「限用一次」的定时器。KillTimer呼叫清除消息队列中尚未被处理的WM_TIMER消息,从而使程序在呼叫KillTimer之后就不会再接收到WM_TIMER消息。
系统和定时器
Windows定时器是PC硬件和ROM BIOS架构下之定时器一种相对简单的扩充。回到Windows以前的MS-DOS程序写作环境下,应用程序能够通过拦截者称为timer tick的BIOS中断来实作时钟或定时器。一些为MS-DOS编写的程序自己拦截这个硬件中断以实作时钟和定时器。这些中断每54.915毫秒产生一次,或者大约每秒18.2次。这是原始的IBM PC的微处理器时脉值4.772720 MHz被218所除而得出的结果。
Windows应用程序不拦截BIOS中断,相反地,Windows本身处理硬件中断,这样应用程序就不必进行处理。对于目前拥有定时器的每个程序,Windows储存一个每次硬件timer tick减少的计数。当这个计数减到0时,Windows在应用程序消息队列中放置一个WM_TIMER消息,并将计数重置为其最初值。
因为Windows应用程序从正常的消息队列中取得WM_TIMER消息,所以您的程序在进行其它处理时不必担心WM_TIMER消息会意外中断了程序。在这方面,定时器类似于键盘和鼠标。驱动程序处理异步硬件中断事件,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消息并不是异步的。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消息」这样的叙述,但是请记住,这些消息并非精确的tick中断。
定时器的使用:三种方法
如果您需要在整个程序执行期间都使用定时器,那么您将得从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。如果需要设定多个定时器,那么对每个定时器都使用不同的定时器ID。wParam的值将随传递到窗口消息处理程序的WM_TIMER消息的不同而不同。为了使程序更具有可读性,您可以使用#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。在时钟程序里,如果显示秒或不显示秒是可以选择的,您就可以这样做,只需简单地将时间间隔在1000毫秒和60 000毫秒间切换就可以了。
程序8-1显示了一个使用定时器的简单程序,名为BEEPER1,定时器的时间间隔设定为1秒。当它收到WM_TIMER消息时,它将显示区域的颜色由蓝色变为红色或由红色变为蓝色,并通过呼叫MessageBeep函数发出响声。(虽然MessageBeep通常用于MessageBox,但它确实是一个全功能的鸣叫函数。在有声卡的PC机上,一般可以使用不同的MB_ICON参数作为MessageBeep的一个参数以用于MessageBox,来播放使用者在「控制台」的「声音」程序中选择的不同声音)。
BEEPER1在窗口消息处理程序处理WM_CREATE消息时设定定时器。在处理WM_TIMER消息处理期间,BEEPER1呼叫MessageBeep,翻转bFlipFlop的值并使窗口无效以产生WM_PAINT消息。在处理WM_PAINT消息处理期间,BEEPER1通过呼叫GetClientRect获得窗口大小的RECT结构,并通过呼叫FillRect改变窗口的颜色。
程序8-1 BEEPER1
BEEPER1.C
/*-------------------------------------------------------------------------
BEEPER1.C -- Timer Demo Program No. 1
(c) Charles Petzold, 1998
-------------------------------------------------------------------------*/
#include <windows.h>
#define ID_TIMER 1
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("Beeper1") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("Program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Beeper1 Timer Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static BOOL fFlipFlop = FALSE ;
HBRUSH hBrush ;
HDC hdc ;
PAINTSTRUCT ps ;
RECT rc ;
switch (message)
{
case WM_CREATE:
SetTimer (hwnd, ID_TIMER, 1000, NULL) ;
return 0 ;
case WM_TIMER :
MessageBeep (-1) ;
fFlipFlop = !fFlipFlop ;
InvalidateRect (hwnd, NULL, FALSE) ;
return 0 ;
case WM_PAINT :
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rc) ;
hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ;
FillRect (hdc, &rc, hBrush) ;
EndPaint (hwnd, &ps) ;
DeleteObject (hBrush) ;
return 0 ;
case WM_DESTROY :
KillTimer (hwnd, ID_TIMER) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
因为BEEPER1每次收到WM_TIMER消息时,都用颜色的变换显示出来,所以您可以通过呼叫BEEPER1来查看WM_TIMER消息的性质,并完成Windows内部的一些其它操作。
例如,首先呼叫控制台的 显示器程序,选择效果,确定 拖曳时显示窗口内容复选框没有被选中。现在,试着移动或者缩放BEEPER1窗口,这将导致程序进入「模态消息循环」。Windows通过在内部消息而非您程序的消息循环中拦截所有消息,来禁止对移动或者缩放操作的任何干扰。通过此循环到达程序窗口的大多数消息都被丢弃,这就是BEEPER1停止蜂鸣的原因。当完成了移动与缩放之后,您将会注意到BEEPER1不能取得它所丢弃的所有WM_TIMER消息,尽管前两个消息的间隔可能少于1秒。
在「拖曳时显示窗口内容」复选框被选中时,Windows中,的模态消息循环会试图给您的窗口消息处理程序传递一些丢失的消息。这样做有时工作得很好,有时却不行。
方法二
设定定时器的第一种方法是把WM_TIMER消息发送到通常的窗口消息处理程序,而第二种方法是让Windows直接将定时器消息发送给您程序的另一个函数。
接收这些定时器消息的函数被称为「callback」函数,这是一个在您的程序之中但是由Windows呼叫的函数。您先告诉Windows此函数的地址,然后Windows呼叫此函数。这看起来也很熟悉,因为程序的窗口消息处理程序实际上也是一种callback函数。当注册窗口类别时,要将函数的地址告诉Windows,当发送消息给程序时,Windows会呼叫此函数。
SetTimer并非是唯一使用callback函数的Windows函数。CreateDialog和DialogBox函数(将在第十一章中介绍)使用callback函数处理对话框中的消息;有几个Windows函数(EnumChildWindow、EnumFonts、EnumObjects、EnumProps和EnumWindow)把列举信息传递给callback函数;还有几个不那么常用的函数(GrayString、LineDDA和SetWindowHookEx)也要求callback函数。
像窗口消息处理程序一样,callback函数也必须定义为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,因此消息参数总是等于WM_TIMER。iTimerID值是定时器ID,dwTimer值是与从GetTickCount函数的传回值相容的值。这是自Windows启动后所经过的毫秒数。
在BEEPER1中已经看到过,用第一种方法设定定时器时要求下面格式的SetTimer呼叫:
SetTimer (hwnd, iTimerID, iMsecInterval, NULL) ;
您使用callback函数处理WM_TIMER消息时,SetTimer的第四个参数由callback函数的地址取代,如下所示:
SetTimer (hwnd, iTimerID, iMsecInterval, TimerProc) ;
我们来看看一些范例程序代码,这样您就会了解这些东西是如何组合在一起的。在功能上,除了Windows发送一个定时器消息给TimerProc而非WndProc之外,程序8-2所示的BEEPER2程序与BEEPER1是相同的。注意,TimerProc和WndProc一起被声明在程序的开始处。
程序8-2 BEEPER2
BEEPER2.C
/*---------------------------------------------------------------------------
BEEPER2.C -- Timer Demo Program No. 2
(c) Charles Petzold, 1998
---------------------------------------------------------------------------*/
#include <windows.h>
#define ID_TIMER 1
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
VOID CALLBACK TimerProc (HWND, UINT, UINT, DWORD ) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static char szAppName[] = "Beeper2" ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -