定时器
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硬件和ROMBIOS架构下之定时器一种相对简单的扩充。回到Windows以前的MS-DOS程序写作环境下,应用程序能够通过拦截者称为timertick的BIOS中断来实作时钟或定时器。一些为MS-DOS编写的程序自己拦截这个硬件中断以实作时钟和定时器。这些中断每54.915毫秒产生一次,或者大约每秒18.2次。这是原始的IBMPC的微处理器时脉值4.772720 MHz被218所除而得出的结果。
Windows应用程序不拦截BIOS中断,相反地,Windows本身处理硬件中断,这样应用程序就不必进行处理。对于目前拥有定时器的每个程序,Windows储存一个每次硬件timertick减少的计数。当这个计数减到0时,Windows在应用程序消息队列中放置一个WM_TIMER消息,并将计数重置为其最初值。
因为Windows应用程序从正常的消息队列中取得WM_TIMER消息,所以您的程序在进行其它处理时不必担心WM_TIMER消息会意外中断了程序。在这方面,定时器类似于键盘和鼠标。驱动程序处理异步硬件中断事件,Windows把这些事件翻译为规律、结构化和顺序化的消息。
在Windows98中,定时器与其下的PC定时器一样具有55毫秒的分辨率。在MicrosoftWindows NT中,定时器的分辨率为10毫秒。
Windows应用程序不能以高于这些分辨率的频率(在Windows98下,每秒18.2次,在WindowsNT下,每秒大约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毫秒和60000毫秒间切换就可以了。
程序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 BEEPER1BEEPER1.C /*------------------------------------------------------------------------- BEEPER1.C -- Timer Demo Program No. 1 (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER1 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 ; HBRUSHhBrush ; HDC hdc ; PAINTSTRUCT ps ; RECTrc ; 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_TIMER1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; VOIDCALLBACK 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)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"),szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, "Beeper2 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) { switch (message) { case WM_CREATE: SetTimer (hwnd, ID_TIMER, 1000, TimerProc) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, ID_TIMER) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime) { static BOOL fFlipFlop = FALSE ; HBRUSHhBrush ; HDC hdc ; RECT rc ; MessageBeep (-1) ; fFlipFlop = !fFlipFlop ; GetClientRect (hwnd, &rc) ; hdc = GetDC (hwnd) ; hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ; FillRect (hdc, &rc, hBrush) ; ReleaseDC (hwnd, hdc) ; DeleteObject (hBrush) ; }
方法三
设定定时器的第三种方法类似于第二种方法,只是传递给SetTimer的hwnd参数被设定为NULL,并且第二个参数(通常为定时器ID)被忽略了,最后,此函数传回定时器ID:
iTimerID = SetTimer (NULL, 0, wMsecInterval, TimerProc) ;
如果没有可用的定时器,那么从SetTimer传回的iTimerID值将为NULL。
KillTimer的第一个参数(通常是窗口句柄)也必须为NULL,定时器ID必须是SetTimer的传回值:
KillTimer (NULL, iTimerID) ;
传递给TimerProc定时器函数的hwnd参数也必须是NULL。这种设定定时器的方法很少被使用。如果在您的程序在不同时刻有一系列的SetTimer呼叫,而又不希望追踪您已经用过了那些定时器ID,那么使用此方法是很方便的。
既然您已经知道了如何使用Windows定时器,就可以开始讨论一些有用的定时器程序了。
定时器用于时钟
时钟是定时器最明显的应用,因此让我们来看看两个时钟,一个数字时钟,一个模拟时钟。
建立数字时钟
程序8-3所示的DIGCLOCK程序,使用类似LED的7个显示方块显示了目前的时间。
程序8-3 DIGCLOCK DIGCLOCK.C /*---------------------------------------------------------------------------- DIGCLOCK.C -- Digital Clock (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("DigClock") ; 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 ("Digital Clock"), 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 ; } void DisplayDigit (HDC hdc, int iNumber) { static BOOL fSevenSegment [10][7] = {1, 1, 1, 0, 1, 1, 1,// 00, 0, 1, 0, 0, 1, 0,// 11, 0, 1, 1, 1, 0, 1,// 21, 0, 1, 1, 0, 1, 1,// 30, 1, 1, 1, 0, 1, 0,// 41, 1, 0, 1, 0, 1, 1,// 5 1, 1, 0, 1, 1, 1, 1,// 61, 0, 1, 0, 0, 1, 0, // 71, 1, 1, 1, 1, 1, 1,// 81, 1, 1, 1, 0, 1, 1 } ; // 9 static POINT ptSegment [7][6] = { 7, 6, 11, 2, 31, 2, 35, 6, 31, 10, 11, 10, 6, 7, 10, 11, 10, 31, 6, 35, 2, 31, 2, 11, 36, 7, 40, 11, 40, 31, 36, 35, 32, 31, 32, 11, 7 , 36, 11, 32, 31, 32, 35, 36, 31, 40, 11, 40, 6 , 37, 10, 41, 10, 61, 6, 65, 2, 61, 2, 41, 36, 37, 40, 41, 40, 61, 36, 65, 32, 61, 32, 41, 7 , 66, 11, 62, 31, 62, 35, 66, 31, 70, 11, 70 } ; int iSeg ; for (iSeg = 0 ; iSeg < 7 ; iSeg++) if (fSevenSegment [iNumber][iSeg]) Polygon (hdc, ptSegment [iSeg], 6) ; } void DisplayTwoDigits (HDC hdc, int iNumber, BOOL fSuppress) { if (!fSuppress || (iNumber / 10 != 0)) DisplayDigit (hdc, iNumber / 10) ; OffsetWindowOrgEx (hdc, -42, 0, NULL) ; DisplayDigit (hdc, iNumber % 10) ; OffsetWindowOrgEx (hdc, -42, 0, NULL) ; } void DisplayColon (HDC hdc) { POINT ptColon [2][4] = { 2, 21,6, 17,10,21,6,25, 2,51,6, 47,10,51,6, 55 } ; Polygon (hdc, ptColon [0], 4) ; Polygon (hdc, ptColon [1], 4) ; OffsetWindowOrgEx (hdc, -12, 0, NULL) ; } void DisplayTime (HDC hdc, BOOL f24Hour, BOOL fSuppress) { SYSTEMTIME st ; GetLocalTime (&st) ; if (f24Hour)DisplayTwoDigits (hdc, st.wHour, fSuppress) ; else DisplayTwoDigits (hdc, (st.wHour %= 12) ? st.wHour : 12, fSuppress) ; DisplayColon (hdc) ; DisplayTwoDigits (hdc, st.wMinute, FALSE) ; DisplayColon (hdc) ; DisplayTwoDigits (hdc, st.wSecond, FALSE) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static BOOL f24Hour, fSuppress ; static HBRUSH hBrushRed ; static intcxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; TCHARszBuffer [2] ; switch (message) { case WM_CREATE: hBrushRed = CreateSolidBrush (RGB (255, 0, 0)) ; SetTimer (hwnd, ID_TIMER, 1000, NULL) ;// fall through case WM_SETTINGCHANGE:GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALE_ITIME, szBuffer, 2) ;f24Hour = (szBuffer[0] == '1') ; GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALE_ITLZERO, szBuffer, 2) ;fSuppress = (szBuffer[0] == '0') ; InvalidateRect (hwnd, NULL, TRUE) ;return 0 ; case WM_SIZE:cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 276, 72, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; SetWindowOrgEx (hdc, 138, 36, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SelectObject (hdc, GetStockObject (NULL_PEN)) ; SelectObject (hdc, hBrushRed) ; DisplayTime (hdc, f24Hour, fSuppress) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, ID_TIMER) ; DeleteObject (hBrushRed) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
DIGCLOCK窗口如图8-1所示。
图8-1 DIGCLOCK的屏幕显示 |
虽然,在图8-1中您看不到时钟的数字是红色的。DIGCLOCK的窗口消息处理程序在处理WM_CREATE消息处理期间建立了一个红色的画刷并在处理WM_DESTROY消息处理期间清除它。WM_CREATE消息也为DIGCLOCK设定了一个一秒的定时器,该定时器在处理WM_DESTROY消息处理期间被终止(待会将讨论对GetLocaleInfo的呼叫)。
在收到WM_TIMER消息后,DIGCLOCK的窗口过程调用InvalidateRect简单地使整个窗口无效。这不是最佳方法,因为每秒整个窗口都要被擦除和重画,有时会引起显示器的闪烁。依据目前的时间使窗口需要更新的部分无效是最好的解决方法。然而,在逻辑上这样做的确很复杂。
在处理WM_TIMER消息处理期间使窗口无效会迫使所有程序的真正活动转入WM_PAINT。DIGCLOCK在WM_PAINT消息一开始将映像方式设定为MM_ISOTROPIC。这样,DIGCLOCK将使用水平方向和垂直方向相等的轴。这些轴(由SetWindowExtEx呼叫设定)是水平276个单位,垂直72个单位。当然,这些轴定得有点太随意了,但它们是按照时钟数字元的大小和间距安排的。
DIGCLOCK将窗口原点设定为(138,36),这是窗口范围的中心;将视埠原点设定为(cxClient/ 2,cyClient /2)。这意味着时钟的显示位于DIGCLOCK显示区域的中心,但是该DIGCLOCK也可以使用在显示屏左上角的原点(0,0)的轴。
然后WM_PAINT将目前画刷设定为之前建立的红画刷,将目前画笔设定为NULL_PEN,并呼叫DIGCLOCK中的函数DisplayTime。
取得目前时间
DisplayTime函数开始呼叫Windows函数GetLocalTime,它带有一个的SYSTEMTIME结构的参数,在WINBASE.H中定义为:
typedef struct _SYSTEMTIME { WORD wYear ; WORD wMonth ; WORD wDayOfWeek ; WORD wDay ; WORD wHour ; WORD wMinute ; WORD wSecond ; WORD wMilliseconds ; } SYSTEMTIME, * PSYSTEMTIME ;
很明显,SYSTEMTIME结构包含日期和时间。月份由1开始递增(也就是说,一月是1),星期由0开始递增(星期天是0)。wDay成员是本月目前的日子,也是由1开始递增的。
SYSTEMTIME主要用于GetLocalTime和GetSystemTime函数。GetSystemTime函数传回目前的世界时间(CoordinatedUniversalTime,UTC),大概与英国格林威治时间相同。GetLocalTime函数传回当地时间,依据计算机所在的时区。这些值的精确度完全决定于使用者所调整的时间精确度以及是否指定了正确的时区。可以双击工作列的时间显示来检查计算机上的时区设定。第二十三章会有一个程序,能够通过Internet精确地设定时间。
Windows还有SetLocalTime和SetSystemTime函数,以及在/PlatformSDK/Windows Base Services/GeneralLibrary/Time中说明的其它与时间有关的函数。
显示数字和冒号
如果DIGCLOCK使用一种仿真7段显示的字体将会简单一些。否则,它就得使用Polygon函数做所有的工作。
DIGCLOCK中的DisplayDigit函数定义了两个数组。fSevenSegment数组有7个BOOL值,用于从0到9的每个十进制数。这些值指出了哪一段需要显示(为1),哪一段不需要显示(为0)。在这个数组中,7段由上到下、由左到右排序。7段中的每个段都是一个6边的多边形。ptSegment数组是一个POINT结构的数组,指出了7个段中每个点的图形坐标。每个数字由下列程序代码画出:
for (iSeg = 0 ; iSeg < 7 ; iSeg++) if ( fSevenSegment [iNumber][iSeg]) Polygon (hdc, ptSegment [iSeg], 6) ;
类似地(但更简单),DisplayColon函数在小时与分钟、分钟与秒之间画一个冒号。数字是42个单位宽,冒号是12个单位宽,因此6个数字与2个冒号,总宽度是276个单位,SetWindowExtEx呼叫中使用了这个大小。
回到DisplayTime函数,原点位于最左数字位置的左上角。DisplayTime呼叫DisplayTwoDigits,DisplayTwoDigits呼叫DisplayDigit两次,并且在每次呼叫OffsetWindowOrgEx后,将窗口原点向右移动42个单位。类似地,DisplayColon函数在画完冒号后,将窗口原点向右移动12个单位。用这种方法,不管对象出现在窗口内的哪个地方,函数对数字和冒号都使用同样的坐标。
这个程序的其它技巧是以12小时或24小时的格式显示时间以及当最左边的小时数字为0时不显示它。
国际化
尽管像DIGCLOCK这样显示时间是非常简单的,但是要显示复杂的日期和时间还是要依赖Windows的国际化支持。格式化日期和时间的最简单的方法是呼叫GetDateFormat和GetTimeFormat函数。这些函数在/PlatformSDK/Windows Base Services/General Library/StringManipulation/String Manipulation Reference/String ManipulationFunctions中有记载,但是它们在/Platform SDK/Windows BaseServices/International Features/National LanguageSupport中进行了说明。这些函数接受SYSTEMTIME结构并且依据使用者在「控制台」的「区域设定」程序中所做的选择而将日期和时间格式化。
DIGCLOCK不能使用GetDateFormat函数,因为它只知道显示数字和冒号,然而,DIGCLOCK应该能够根据使用者的参数选择来显示12小时或24小时的格式,并禁止(或不禁止)开头的小时数字。您可以从GetLocaleInfo函数中取得这种信息。虽然GetLocaleInfo在/PlatformSDK/Windows Base Services/General Library/StringManipulation/String Manipulation Reference/String ManipulationFunctions中有记载,但是这个函数使用的标识符在/PlatformSDK/Windows Base Services/International Features/NationalLanguage Support/National Language SupportConstants中有说明。
DIGCLOCK在处理WM_CREATE消息时,最初呼叫GetLocaleInfo两次,第一次使用LOCALE_ITIME标识符(确定使用的是12小时还是24小时格式),然后使用LOCALE_ITLZERO标识符(在小时显示中禁止前面显示0)。GetLocaleInfo函数在字符串中传回所有的信息,但是在大多数情况下把字符串转变为整数并不是非常容易。DIGCLOCK把字符串储存在两个静态变量中并把它们传递给DisplayTime函数。
如果使用者更改了任何系统设定,则会将WM_SETTINGCHANGE消息传送给所有的应用程序。DIGCLOCK通过再次呼叫GetLocaleInfo处理这个消息。以这种方式,您可以在「控制台」的「区域设定」程序中进行不同的设定来实验一下。
在理论上,DIGCLOCK也应该使用LOCALE_STIME标识符呼叫GetLocaleInfo。这会传回使用者为时间的小时、分钟和秒等单个部分选择的字符。因为DIGCLOCK被设定为仅显示冒号,所以不管选择了什么,都会得到冒号。要指出时间是A.M.或P.M.,应用程序可以使用带有LOCALE_S1159和LOCALE_S2359标识符的GetLocaleInfo函数。这些标识符使程序获得适合于使用者国家/地区和语言的字符串。
我们也可以让DIGCLOCK处理WM_TIMECHANGE消息,这样它将系统时间与日期发生变化的消息通知应用程序。DIGCLOCK因WM_TIMER消息而每秒更新一次,实际上没有必要这样作,对WM_TIMECHANGE消息的处理使得每分钟更新一次的时钟变得更为合理。
建立模拟时钟
模拟时钟不必关心国际化问题,但是由于图形所引起的复杂性却抵消了这种简化。为了正确地产生时钟,您需要知道一些三角函数。CLOCK如程序8-4所示。
程序8-4 CLOCKCLOCK.C /*--------------------------------------------------------------------------- CLOCK.C -- Analog Clock Program (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #define ID_TIMER 1 #define TWOPI (2 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Clock") ; 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 = NULL ; 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 ("Analog Clock"),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 ; } void SetIsotropic (HDC hdc, int cxClient, int cyClient) { SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; } void RotatePoint (POINT pt[], int iNum, int iAngle) { int i ; POINT ptTemp ; for (i = 0 ; i < iNum ; i++) { ptTemp.x = (int) (pt[i].x * cos (TWOPI * iAngle / 360) +pt[i].y * sin (TWOPI * iAngle / 360)) ; ptTemp.y = (int) (pt[i].y * cos (TWOPI * iAngle / 360) - pt[i].x * sin (TWOPI * iAngle / 360)) ; pt[i] = ptTemp ; } } void DrawClock (HDC hdc) { int iAngle ; POINT pt[3] ; for (iAngle = 0 ; iAngle < 360 ; iAngle += 6) { pt[0].x = 0 ; pt[0].y = 900 ; RotatePoint (pt, 1, iAngle) ; pt[2].x = pt[2].y = iAngle % 5 ? 33 : 100 ; pt[0].x - = pt[2].x / 2 ; pt[0].y - = pt[2].y / 2 ; pt[1].x = pt[0].x + pt[2].x ; pt[1].y = pt[0].y + pt[2].y ; SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ; Ellipse (hdc, pt[0].x, pt[0].y, pt[1].x, pt[1].y) ; } } void DrawHands (HDC hdc, SYSTEMTIME * pst, BOOL fChange) { static POINT pt[3][5] ={0, -150, 100, 0, 0, 600, -100, 0, 0, -150, 0, -200, 50,0, 0, 800, -50, 0, 0,-200,0,0, 0, 0, 0, 0, 0, 0, 0, 800 } ; int i, iAngle[3] ; POINT ptTemp[3][5] ; iAngle[0] = (pst->wHour * 30) % 360 + pst->wMinute / 2 ; iAngle[1] = pst->wMinute * 6 ; iAngle[2] = pst->wSecond * 6 ; memcpy (ptTemp, pt, sizeof (pt)) ; for (i = fChange ? 0 : 2 ; i < 3 ; i++) { RotatePoint (ptTemp[i], 5, iAngle[i]) ; Polyline (hdc, ptTemp[i], 5) ; } } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static intcxClient, cyClient ; static SYSTEMTIME stPrevious ; BOOL fChange ; HDC hdc ; PAINTSTRUCT ps ; SYSTEMTIME st ; switch (message) { case WM_CREATE : SetTimer (hwnd, ID_TIMER, 1000, NULL) ; GetLocalTime (&st) ; stPrevious = st ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER : GetLocalTime (&st) ;fChange = st.wHour! = stPrevious.wHour || st.wMinute ! = stPrevious.wMinute ; hdc = GetDC (hwnd) ; SetIsotropic (hdc, cxClient, cyClient) ; SelectObject (hdc, GetStockObject (WHITE_PEN)) ; DrawHands (hdc, &stPrevious, fChange) ;SelectObject (hdc, GetStockObject (BLACK_PEN)) ; DrawHands (hdc, &st, TRUE) ; ReleaseDC (hwnd, hdc) ; stPrevious = st ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; SetIsotropic (hdc, cxClient, cyClient) ; DrawClock(hdc) ; DrawHands(hdc, &stPrevious, TRUE) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : KillTimer (hwnd, ID_TIMER) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
CLOCK屏幕显示如图8-2。
图8-2 CLOCK的屏幕显示 |
等方向性(isotropic)映像对于这样的应用来说是理想的,CLOCK.C中的SetIsotropic函数负责设定此模式。在呼叫SetMapMode之后,SetIsotropic将窗口范围设定为1000,并将视端口范围设定为显示区域的一半宽度和显示区域的负的一半高度。视端口原点被设定为显示区域的中心。我在第五章中讨论过,这将建立一个笛卡儿坐标系,其点(0,0)位于显示区域的中心,在所有方向上的范围都是1000。
RotatePoint函数是用到三角函数的地方,此函式的三个参数分别是一个或者多个点的数组、数组中点的个数以及以度为单位的旋转角度。函式以原点为中心按顺时针方向(这对一个时钟正合适)旋转这些点。例如,如果传给函式的点是(0,100)-即12:00的位置-而角度为90度,那么该点将被变换为(100,0)-即3:00。它使用下列公式来做到这一点:
x' = x * cos (a) + y * sin (a) y' = y * cos (a) - x * sin (a)
RotatePoint函数在绘制时钟表面的点和表针时都是有用的,我们将马上看到这一点。
DrawClock函数绘制60个时钟表面的点,从顶部(12:00)开始,其中每个点离原点900单位,因此第一个点位于(0,900),此后的每个点按顺时针依次增加6度。这些点中的l2个直径为100个单位;其余的为33个单位。使用Ellipse函数来画点。
DrawHands函数绘制时钟的时针、分针和秒针。定义表针轮廓(当它们垂直向上时的形状)的坐标存放在一个POINT结构的数组中。根据时间,这些坐标使用RotatePoint函数进行旋转,并用Windows的Polyline函数进行显示。注意时针和分针只有当传递给DrawHands的bChange参数为TRUE时才被显示。当程序更新时钟的表针时,大多数情况下时针和分针不需要重画。
现在让我们将注意力转到窗口消息处理程序。在WM_CREATE消息处理期间,窗口消息处理程序取得目前时间并将它存放在名为dtPrevious的变量中,这个变量将在以后被用于确定时针或者分针从上次更新以来是否改变过。
第一次绘制时钟是在第一个WM_PAINT消息处理期间,这只不过是依次呼叫SetIsotropic、DrawClock和DrawHands,后者的bChange参数被设定为TRUE。
在WM_TIMER消息处理期间,WndProc首先取得新的时间并确定是否需要重新绘制时针和分针。如果需要,则使用一个白色画笔和上一次时间绘制所有的表针,从而有效地擦除它们。否则,只对秒针使用白色画笔进行擦除,然后,再使用一个黑色画笔绘制所有的表针。
以定时器进行状态报告
本章的最后一个程序是我在第五章提到过的。它是一个使用GetPixel函数的好例子。
WHATCLR (见程序8-5)显示了鼠标光标下目前图素的RGB颜色。
程序8-5 WHATCLRWHATCLR.C /*-------------------------------------------------------------------------- WHATCLR.C -- Displays Color Under Cursor (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER1 void FindWindowSize (int *, int *) ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("WhatClr") ; HWND hwnd ; int cxWindow, cyWindow ; 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 ("This program requires Windows NT!"),szAppName, MB_ICONERROR) ; return 0 ; } FindWindowSize (&cxWindow, &cyWindow) ; hwnd = CreateWindow (szAppName, TEXT ("What Color"), WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_BORDER, CW_USEDEFAULT, CW_USEDEFAULT, cxWindow, cyWindow, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void FindWindowSize (int * pcxWindow, int * pcyWindow) { HDC hdcScreen ; TEXTMETRICtm ; hdcScreen = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; GetTextMetrics (hdcScreen, &tm) ; DeleteDC (hdcScreen) ; * pcxWindow = 2 * GetSystemMetrics (SM_CXBORDER) + 12 * tm.tmAveCharWidth ; * pcyWindow = 2 * GetSystemMetrics (SM_CYBORDER) + GetSystemMetrics (SM_CYCAPTION) + 2 * tm.tmHeight ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static COLORREF cr, crLast ; static HDChdcScreen ; HDC hdc ; PAINTSTRUCT ps ; POINT pt ; RECT rc ; TCHAR szBuffer [16] ; switch (message) { case WM_CREATE: hdcScreen = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; SetTimer (hwnd, ID_TIMER, 100, NULL) ; return 0 ; case WM_TIMER: GetCursorPos (&pt) ; cr = GetPixel (hdcScreen, pt.x, pt.y) ; SetPixel (hdcScreen, pt.x, pt.y, 0) ; if (cr != crLast) { crLast = cr ; InvalidateRect (hwnd, NULL, FALSE) ; } return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rc) ; wsprintf (szBuffer, TEXT (" %02X %02X %02X "), GetRValue (cr), GetGValue (cr), GetBValue (cr)) ; DrawText (hdc, szBuffer, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteDC (hdcScreen) ; KillTimer (hwnd, ID_TIMER) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
WHATCLR在WinMain中做了一点与以往不同的事。因为WHATCLR的窗口只需要显示十六进制RGB值那么大,所以它在CreateWindow函数中使用WS_BORDER窗口样式建立了一个不能改变大小的窗口。要计算窗口的大小,WHATCLR通过先呼叫CreateIC再呼叫GetSystemMetrics以取得用于视讯显示的设备内容信息。计算好的窗口宽度和高度值被传递给CreateWindow。
WHATCLR的窗口消息处理程序在处理WM_CREATE消息处理期间,呼叫CreateDC建立了用于整个视讯显示的设备内容。这个设备内容在程序的生命周期内都有效。在处理WM_TIMER消息处理期间,程序取得目前鼠标光标位置的图素。在处理WM_PAINT消息处理期间显示RGB颜色。
您可能想知道,从CreateDC函数中取得的设备内容句柄是否能让您在屏幕的任意位置显示一些东西,而不光只是取得图素颜色。答案是可以的,一般而言,让一个应用程序在另一个程控的画面区域上画图是不好的,但在某些特殊情况下,这可能会非常有用。