三、Windows编程之定时器用于时钟
时钟是定时器最明显的应用,因此让我们来看看两个时钟,一个数字时钟,一个模拟时钟。
建立数字时钟
程序8-3所示的DIGCLOCK程序,使用类似LED的7个显示方块显示了目前的时间。
程序8-3 DIGCLOCK DIGCLOCK.C /*---------------------------------------------------------------------------- DIGCLOCK.C -- Digital Clock (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 ("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, // 0 0, 0, 1, 0, 0, 1, 0, // 1 1, 0, 1, 1, 1, 0, 1, // 2 1, 0, 1, 1, 0, 1, 1, // 3 0, 1, 1, 1, 0, 1, 0, // 4 1, 1, 0, 1, 0, 1, 1, // 5 1, 1, 0, 1, 1, 1, 1, // 6 1, 0, 1, 0, 0, 1, 0, // 7 1, 1, 1, 1, 1, 1, 1, // 8 1, 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 int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; TCHAR szBuffer [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的窗口消息处理程序在处理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函数传回目前的世界时间(Coordinated Universal Time,UTC),大概与英国格林威治时间相同。GetLocalTime函数传回当地时间,依据计算机所在的时区。这些值的精确度完全决定于使用者所调整的时间精确度以及是否指定了正确的时区。可以双击工作列的时间显示来检查计算机上的时区设定。 第二十三章会有一个程序,能够通过Internet精确地设定时间。
Windows还有SetLocalTime和SetSystemTime函数,以及在/Platform SDK/Windows Base Services/General Library/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函数。这些函数在/Platform SDK/Windows Base Services/General Library/String Manipulation/String Manipulation Reference/String Manipulation Functions中有记载,但是它们在/Platform SDK/Windows Base Services/International Features/National Language Support中进行了说明。这些函数接受SYSTEMTIME结构并且依据使用者在「控制台」的「区域设定」 程序中所做的选择而将日期和时间格式化。
DIGCLOCK不能使用GetDateFormat函数,因为它只知道显示数字和冒号,然而,DIGCLOCK应该能够根据使用者的参数选择来显示12小时或24小时的格式,并禁止(或不禁止)开头的小时数字。您可以从GetLocaleInfo函数中取得这种信息。虽然GetLocaleInfo在/Platform SDK/Windows Base Services/General Library/String Manipulation/String Manipulation Reference/String Manipulation Functions中有记载,但是这个函数使用的标识符在/Platform SDK/Windows Base Services/International Features/National Language Support/National Language Support Constants中有说明。
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所示。
CLOCK.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 int cxClient, 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。
等方向性(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首先取得新的时间并确定是否需要重新绘制时针和分针。如果需要,则使用一个白色画笔和上一次时间绘制所有的表针,从而有效地擦除它们。否则,只对秒针使用白色画笔进行擦除,然后,再使用一个黑色画笔绘制所有的表针。