Get a site

VC++ 6.0 ebook chapter index
homepage aranna.altervista.org
Free counters!

Using the Timer for a Clock

A clock is the most obvious application for the timer, so let's look at two of them, one digital and one analog.

Building a Digital Clock

The DIGCLOCK program, shown in Figure 8-3, displays the current time using a simulated LED-like 7-segment display.

Figure 8-3. The DIGCLOCK program.

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) ;
}

The DIGCLOCK window is shown in Figure 8-4.

Click to view at full size.

Figure 8-4. The DIGCLOCK display.

Although you can't see it in Figure 8-4, the clock numbers are red. DIGCLOCK's window procedure creates a red brush during the WM_CREATE message and destroys it during the WM_DESTROY message. The WM_CREATE message also provides DIGCLOCK with an opportunity to set a 1-second timer, which is stopped during the WM_DESTROY message. (I'll discuss the calls to GetLocaleInfo shortly.)

Upon receipt of a WM_TIMER message, DIGCLOCK's window procedure simply invalidates the entire window with a call to InvalidateRect. Aesthetically, this is not the best approach because it means that the entire window will be erased and redrawn every second, sometimes causing flickering in the display. A better solution is to invalidate only those parts of the window that need updating based on the current time. The logic to do this is rather messy, however.

Invalidating the window during the WM_TIMER message forces all the program's real activity into WM_PAINT. DIGCLOCK begins the WM_PAINT message by setting the mapping mode to MM_ISOTROPIC. Thus, DIGCLOCK will use arbitrarily scaled axes that are equal in the horizontal and vertical directions. These axes (set by a call to SetWindowExtEx) are 276 units horizontally by 72 units vertically. Of course, these axes seem quite arbitrary, but they are based on the size and spacing of the clock numbers.

DIGCLOCK sets the window origin to the point (138, 36), which is the center of the window extents, and the viewport origin to (cxClient / 2, cyClient / 2). This means that the clock display will be centered in DIGCLOCK's client area but that DIGCLOCK can use axes with an origin of (0, 0) at the upper-left corner of the display.

The WM_PAINT processing then sets the current brush to the red brush created earlier and the current pen to the NULL_PEN and calls the function in DIGCLOCK named DisplayTime.

Getting the Current Time

The DisplayTime function begins by calling the Windows function GetLocalTime, which takes as a single argument the SYSTEMTIME structure, defined in WINBASE.H like so:

typedef struct _SYSTEMTIME 
{
     WORD wYear ;
     WORD wMonth ;
     WORD wDayOfWeek ;
     WORD wDay ;
     WORD wHour ;
     WORD wMinute ;
     WORD wSecond ;
     WORD wMilliseconds ;
} 
SYSTEMTIME, * PSYSTEMTIME ;

As is obvious, the SYSTEMTIME structure encodes the date as well as the time. The month is 1-based (that is, January is 1), and the day of the week is 0-based (Sunday is 0). The wDay field is the current day of the month, which is also 1-based.

The SYSTEMTIME structure is used primarily with the GetLocalTime and GetSystemTime functions. The GetSystemTime function reports the current Coordinated Universal Time (UTC), which is roughly the same as Greenwich mean time—the date and time at Greenwich, England. The GetLocalTime function reports the local time, based on the time zone of the location of the computer. The accuracy of these values is entirely dependent on the diligence of the user in keeping the time accurate and in indicating the correct time zone. You can check the time zone set on your machine by double-clicking the time display in the task bar. A program to set your PC's clock from an accurate, exact time source on the Internet is shown in Chapter 23.

Windows also has SetLocalTime and SetSystemTime functions, as well as some other useful time-related functions that are discussed in /Platform SDK/Windows Base Services/General Library/Time.

Displaying Digits and Colons

DIGCLOCK might be somewhat simplified if it used a font that simulated a 7-segment display. Instead, it has to do all the work itself using the Polygon function.

The DisplayDigit function in DIGCLOCK defines two arrays. The fSevenSegment array has 7 BOOL values for each of the 10 decimal digits from 0 through 9. These values indicate which of the segments are illuminated (a 1 value) and which are not (a 0 value). In this array, the 7 segments are ordered from top to bottom and from left to right. Each of the 7 segments is a 6-sided polygon. The ptSegment array is an array of POINT structures indicating the graphical coordinates of each point in each of the 7 segments. Each digit is then drawn by this code:

for (iSeg = 0 ; iSeg < 7 ; iSeg++)
     if (fSevenSegment [iNumber][iSeg])
          Polygon (hdc, ptSegment [iSeg], 6) ;

Similarly (but more simply), the DisplayColon function draws the colons that separate the hour and minutes, and the minutes and seconds. The digits are 42 units wide and the colons are 12 units wide, so with 6 digits and 2 colons, the total width is 276 units, which is the size used in the SetWindowExtEx call.

Upon entry to the DisplayTime function, the origin is at the upper left corner of the position of the leftmost digit. DisplayTime calls DisplayTwoDigits, which calls DisplayDigit twice, and after each time calls OffsetWindowOrgEx to move the window origin 42 units to the right. Similarly, the DisplayColon function moves the window origin 12 units to the right after drawing the colon. In this way, the functions can use the same coordinates for the digits and colons, regardless of where the object is to appear within the window.

The only other tricky aspects of this code involve displaying the time in a 12-hour or 24-hour format and suppressing the leftmost hours digit if it's 0.

Going International

Although displaying the time as DIGCLOCK does is fairly foolproof, for any more complex displays of the date or time you should rely upon Windows' international support. The easiest way to format a date or time is to use the GetDateFormat and GetTimeFormat functions. These functions are documented in /Platform SDK/Windows Base Services/General Library/String Manipulation/String Manipulation Reference/String Manipulation Functions, but they are discussed in /Platform SDK/Windows Base Services/International Features/National Language Support. These functions accept SYSTEMTIME structures and format the date and time based on options the user has chosen in the Regional Settings applet of the Control Panel.

DIGCLOCK can't use the GetDateFormat function because it knows how to display only digits and colons. However, DIGCLOCK should respect the user's preferences for displaying the time in a 12-hour or 24-hour format, and for suppressing (or not suppressing) the leading hours digit. You can obtain this information from the GetLocaleInfo function. Although GetLocaleInfo is documented in /Platform SDK/Windows Base Services/General Library/String Manipulation/String Manipulation Reference/String Manipulation Functions, the identifiers you use with this function are documented in /Platform SDK/Windows Base Services/International Features/National Language Support/National Language Support Constants.

DIGCLOCK initially calls GetLocaleInfo twice while processing the WM_CREATE message—the first time with the LOCALE_ITIME identifier (to determine whether the 12-hour or 24-hour format is to be used) and then with the LOCALE_ITLZERO identifier (to suppress a leading zero on the hour display). The GetLocaleInfo function returns all information in strings, but in most cases it's fairly easy to convert this to integer data if necessary. DIGCLOCK stores the settings in two static variables and passes them to the DisplayTime function.

If the user changes any system setting, the WM_SETTINGCHANGE message is broadcast to all applications. DIGCLOCK processes this message by calling GetLocaleInfo again. In this way, you can experiment with different settings by using the Regional Settings applet of the Control Panel.

In theory, DIGCLOCK should probably also call GetLocaleInfo with the LOCALE_

STIME identifier. This returns the character that the user has selected for separating the hours, minutes, and seconds parts of the time. Because DIGCLOCK is set up to display only colons, this is what the user will get even if something else is preferred. To indicate whether the time is A.M. or P.M., an application can use GetLocaleInfo with the LOCALE_S1159 and LOCALE_S2359 identifiers. These identifiers let the program obtain strings that are appropriate for the user's country and language.

We could also have DIGCLOCK process WM_TIMECHANGE messages, which notifies applications of changes to the system date or time. Because DIGCLOCK is updated every second by WM_TIMER messages, this is unnecessary. Processing WM_TIMECHANGE messages would make more sense for a clock that was updated every minute.

Building an Analog Clock

An analog clock program needn't concern itself with internationalization, but the complexity of the graphics more than make up for that simplification. To get it right, you'll need to know some trigonometry. The CLOCK program is shown in Figure 8-5.

Figure 8-5. The CLOCK program.

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) ;
}

The CLOCK screen display is shown in Figure 8-6.

Click to view at full size.

Figure 8-6. The CLOCK display.

The isotropic mapping mode is once again ideal for such an application, and setting it is the responsibility of the SetIsotropic function in CLOCK.C. After calling SetMapMode, the function sets the window extents to 1000 and the viewport extents to half the width of the client area and the negative of half the height of the client area. The viewport origin is set to the center of the client area. As I discussed in Chapter 5, this creates a Cartesian coordinate system with the point (0,0) in the center of the client area and extending 1000 units in all directions.

The RotatePoint function is where the trigonometry comes into play. The three parameters to the function are an array of one or more points, the number of points in that array, and the angle of rotation in degrees. The function rotates the points clockwise (as is appropriate for a clock) around the origin. For example, if the point passed to the function is (0,100)—that is, the position of 12:00—and the angle is 90 degrees, the point is converted to (100,0)—which is 3:00. It does this using these formulas:

x' = x * cos (a) + y * sin (a)

y' = y * cos (a) - x * sin (a)

The RotatePoint function is useful for drawing both the dots of the clock face and the clock hands, as we'll see shortly.

The DrawClock function draws the 60 clock face dots starting with the one at the top (12:00 high). Each of them is 900 units from the origin, so the first is located at the point (0, 900) and each subsequent one is 6 additional clockwise degrees from the vertical. Twelve of the dots are 100 units in diameter; the rest are 33 units. The dots are drawn using the Ellipse function.

The DrawHands function draws the hour, minute, and second hands of the clock. The coordinates defining the outlines of the hands (as they appear when pointing straight up) are stored in an array of POINT structures. Depending upon the time, these coordinates are rotated using the RotatePoint function and are displayed with the Windows Polyline function. Notice that the hour and minute hands are displayed only if the bChange parameter to DrawHands is TRUE. When the program updates the clock hands, in most cases the hour and minute hands will not need to be redrawn.

Now let's turn our attention to the window procedure. During the WM_CREATE message, the window procedure obtains the current time and also stores it in the variable named dtPrevious. This variable will later be used to determine whether the hour or minute has changed from the previous update.

The first time the clock is drawn is during the first WM_PAINT message. That's just a matter of calling the SetIsotropic, DrawClock, and DrawHands functions, the latter with the bChange parameter set to TRUE.

During the WM_TIMER message, WndProc first obtains the new time and determines if the hour and minute hands need to be redrawn. If so, all the hands are drawn with a white pen using the previous time, effectively erasing them. Otherwise, only the second hand is erased using the white pen. Then, all the hands are drawn with a black pen.