Chapter 5  The Timer
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

The Windows timer is an input device that periodically notifies an
application when a specified interval of time has elapsed. Your program
tells Windows the interval, in effect saying, for example, "Give me a nudge
every 10 seconds." Windows then sends your program recurrent WM_TIMER
messages to signal the intervals.

At first, the Windows timer may seem a less important input device than the
keyboard or mouse, and certainly it is for many applications. But the timer
is more useful than you may think, and not only for programs (like the
Windows CLOCK) that keep time. The CALENDAR, CONTROL, REVERSI, SPOOLER,
TERMINAL, and WRITE programs supplied with Windows also use the timer. Here
are some uses for the Windows timer, some obvious and some perhaps not so
obvious:

  þ   Keeping time--Both the CLOCK and CONTROL programs that come with
      Windows display the current time. The timer tells the programs when to
      update the clock. The DIGCLOCK program, described later in this
      chapter, uses the timer to display a digital clock.

  þ   Maintaining an updated status report--The FREEMEM program, shown in
      this chapter, uses the timer to display available memory in Windows.
      The display is updated every second.

  þ   Waking up--The Windows CALENDAR program uses the timer to trigger a
      preset alarm.

  þ   Multitasking--Windows is a nonpreemptive multitasking environment, and
      it is important that programs return control to Windows as quickly as
      possible. If a program must do a large amount of processing, it can
      divide the job into smaller pieces and process each piece on receipt
      of a WM_TIMER message.

  þ   Implementing an "autosave" feature--The timer can prompt a Windows
      program to save a user's work to disk whenever a specified amount of
      time has elapsed.

  þ   Pacing movement--Graphical objects in a game or successive displays in
      a computer-assisted instruction program may need to proceed at a set
      rate. Using the timer eliminates the inconsistencies that might result
      from variations in microprocessor speed.

  þ   Terminating demonstration versions of programs--Some demonstration
      versions of programs are designed to terminate, say, 30 minutes after
      they begin. The timer can signal such applications when the time is
      up.

  þ   Using serial or parallel communications--Unlike most other input
      devices in Windows, serial or parallel communications ports do not
      generate messages. Rather, these programs must poll for input, and the
      timer can tell them when to do so. (An alternative to using the timer
      for polling involves a message loop built around the PeekMessage call.
      This technique is discussed in Chapter 15.)

This chapter also explores topics that extend beyond the timer to other
areas of Windows programming. Foremost among these topics is that of
"call-back" functions. To the uninitiated, these important functions might
seem to work in mysterious ways, and the timer is not the only place you
will encounter them. This chapter also discusses what to do when a program
cannot gain access to a timer--a problem that occurs because Windows
maintains only a limited number of timers. Solving this problem is
fundamental to working with the Windows timer, but the method presented here
can also be applied to error handling in other programs. Finally, the sample
programs shown here deal with such decidedly nontimer issues as Windows' use
of color, using a type of window known as a "popup," forcing an application
to be loaded as an icon, obtaining the amount of free memory available in
Windows, using floating-point mathematics in your Windows programs, and
accessing the WIN.INI file to obtain information about international time
and date formats.

TIMER BASICS

You can allocate a timer for your Windows program by calling the SetTimer
function. SetTimer includes a parameter specifying an interval that can
range (in theory) from 1 msec (millisecond) to 65,535 msec, or about 65.5
seconds. The value indicates the rate at which Windows sends your program
WM_TIMER messages. For instance, an interval of 1000 msec causes Windows to
send your program a WM_TIMER message every second.

When your program is done using the timer, it calls the KillTimer function
to stop the timer messages. You can program a "one-shot" timer by calling
KillTimer during the processing of the WM_TIMER message. The KillTimer call
purges the message queue of any pending WM_TIMER messages. Your program will
never receive a stray WM_TIMER message following a KillTimer call.

As you've undoubtedly learned from experimenting with loading multiple
instances of CLOCK, Windows allows only 16 timers to be active at one time.
If all 16 timers are already allocated, SetTimer returns NULL. Windows
programs that use a timer must include some way to deal with this problem.

SYSTEM.DRV and the Windows Timer

The Windows timer is a relatively simple extension of the timer logic built
into the IBM PC's hardware and ROM BIOS. The PC's ROM BIOS initializes an
Intel 8259 timer chip to generate the hardware Interrupt 08H. This interrupt
is sometimes called the "clock tick" or "timer tick" interrupt. An Interrupt
08H occurs every 54.925 msec, or about 18.2 times per second. Among other
purposes, the BIOS uses Interrupt 08H to update a "time-of-day" value stored
in the BIOS data area. MS-DOS uses this value to calculate the current time.

The SYSTEM.DRV driver located in the SYSTEM subdirectory of your Windows
directories handles hardware timer interrupts. SYSTEM.DRV sets a new
Interrupt 08H vector address during initialization and restores the original
vector address before Windows terminates. The Interrupt 08H routine within
SYSTEM.DRV calls the original Interrupt 08H handler before doing its own
processing so that underlying system functions that require this interrupt
will continue to work normally.

When SYSTEM.DRV receives an Interrupt 08H, it calls a routine within the
USER module of Windows that decrements counters for each timer set by
Windows applications. When a counter reaches 0, USER places a WM_TIMER
message in that application's message queue and resets the counter to the
original value.

Because a Windows application retrieves WM_TIMER messages from the normal
message queue, you never have to worry about your program being
"interrupted" by a sudden WM_TIMER message while doing other processing. In
this way, the timer is similar to the keyboard and mouse: The driver handles
the asynchronous hardware interrupt events, and Windows translates these
events into orderly, structured, serialized messages.

SYSTEM.DRV does not attempt to reprogram the 8259 timer chip in the IBM PC.
The Windows timer has the same 54.925-msec resolution as the underlying PC
timer. This fact has two important implications:

  þ   A Windows application cannot receive WM_TIMER messages at a rate
      faster than about 18.2 times per second when using a single timer.

  þ   The time interval you specify in the SetTimer call is always rounded
      down to an integral multiple of clock ticks. For instance, a 1000-msec


      interval divided by 54.925 msec is 18.207 clock ticks, which is
      rounded down to 18 clock ticks, which is really a 989-msec interval.
      For intervals less than 55 msec, each clock tick generates a single
      WM_TIMER message.

      Do not attempt to intercept the ROM BIOS timer interrupt in your
      Windows programs. Use the Windows timer instead.


Timer Messages Are Not Asynchronous

Non-Windows programs written for the IBM PC and compatibles can use the
timer tick interrupt by intercepting Interrupt 08H or Interrupt 1CH (a
software interrupt called by the BIOS Interrupt 08H handler). When the
hardware interrupt occurs, the program currently running is suspended, and
control passes to the interrupt handler. When the interrupt handler is done,
it passes control back to the interrupted program.

Like the hardware keyboard and mouse interrupts, the hardware timer tick
interrupt is sometimes called an asynchronous interrupt because it occurs
randomly with respect to the program that it interrupts. (Actually, the term
isochronous is more accurate than asynchronous for a timer interrupt because
the interrupts occur at equal intervals. But the interrupts are still
asynchronous with respect to other processing.)

Although the SYSTEM.DRV driver also handles asynchronous Interrupt 08H clock
ticks, the WM_TIMER messages that Windows sends to applications are not
asynchronous. The WM_TIMER messages are placed in the normal message queue
and ordered with all the other messages. Therefore, if you specify 1000 msec
in the SetTimer call, your program is not guaranteed to receive a WM_TIMER
message every second or even (as I mentioned above) every 989 msec. If
another application is busy for more than a second, your program will not
get any WM_TIMER messages during that time. Only when the other application
yields control to Windows (by calling GetMessage, PeekMessage, or
WaitMessage) will your program retrieve its next WM_TIMER message from the
queue.

You can easily demonstrate this to yourself with the CLOCK program included
with Windows or with the sample programs shown in this chapter. If another
program has a long paint job and does not immediately relinquish control,
CLOCK will stop. When CLOCK regains control, it will jump ahead to the
correct time. In fact, Windows handles WM_TIMER messages much like WM_PAINT
messages. Both these messages are low priority. If a program's message queue
contains only WM_PAINT or WM_TIMER messages, and another program's message
queue contains messages other than WM_PAINT or WM_TIMER, Windows will pass
control to the other application.

The WM_TIMER messages are similar to WM_PAINT messages in another respect:
Windows does not keep loading up the message queue with multiple WM_TIMER
messages. Instead, Windows combines several WM_TIMER messages in the message
queue into a single message. Therefore, the application won't get a bunch of
them all at once, although it may get two WM_TIMER messages in quick
succession. An application cannot determine the number of "missing" WM_TIMER
messages that result from this process.

When CLOCK regains control and jumps ahead to the correct time, it is not
because it gets several WM_TIMER messages in a row. CLOCK must determine the
actual time and then set itself. The WM_TIMER messages only inform CLOCK
when it should be updated. A program can't keep time itself solely by
counting WM_TIMER messages. (Later in this chapter we will write a clock
application that updates every second, and we'll see precisely how this is
accomplished.)

For convenience, I'll be talking about the timer in terms such as "getting a
WM_TIMER message every second." But keep in mind that these messages are not
precise clock tick interrupts.



USING THE TIMER: THREE METHODS

If you need a timer for the entire duration of your program, you'll probably
call SetTimer from the WinMain function or while processing the WM_CREATE
message, and KillTimer in response to a WM_DESTROY message. Setting the
timer in WinMain provides the easiest error handling if a timer is
unavailable. You can use a timer in one of three ways, depending on the
parameters to the SetTimer call.

Method One

This method, the easiest, causes Windows to send WM_TIMER messages to the
normal window procedure of the application. The SetTimer call looks like
this:

SetTimer (hwnd, 1, wMsecInterval, NULL) ;

The first parameter is a handle to the window whose window procedure will
receive the WM_TIMER messages. The second parameter is the timer ID, which
should be a nonzero number. I have arbitrarily set it to 1 in this example.
The third parameter is a WORD (16-bit unsigned integer) that specifies an
interval in milliseconds. The largest value (65535) will deliver a WM_TIMER
message about once a minute.

You can stop the WM_TIMER messages at any time (even while processing a
WM_TIMER message) by calling:

KillTimer (hwnd, 1) ;

The second parameter is the same timer ID used in the SetTimer call. You
should kill any active timers in response to a WM_DESTROY message before
your program terminates.

When your window procedure receives a WM_TIMER message, wParam is equal to
the timer ID (which in the above case is simply 1), and lParam is 0. If you
need to set more than one timer, use a different timer ID for each timer.
The value of wParam will  differentiate the WM_TIMER messages passed to your
window procedure. To make your program more readable, you may want to use
#define statements for the different timer IDs:

#define TIMER_SEC  1
#define TIMER_MIN  2

You can then set the two timers with two SetTimer calls:

SetTimer (hwnd, TIMER_SEC, 1000, NULL) ;
SetTimer (hwnd, TIMER_MIN, 60000, NULL) ;

The WM_TIMER logic might look something like this:

case WM_TIMER :
     switch (wParam)
          {
          case TIMER_SEC :
[once-per-second processing]
               break ;
          case TIMER_MIN :
[once-per-minute processing]
               break ;
          }
     return 0 ;

If you want to set an existing timer to a different elapsed time, kill the
timer and call SetTimer again. This code assumes that the timer ID is 1:

KillTimer (hwnd, 1) ;
SetTimer (hwnd, 1, wMsecInterval, NULL) ;

The wMsecInterval parameter is the new elapsed time in milliseconds. The
Windows CLOCK application uses this method to change the timer from 1000
msec to 60,000 msec when it becomes an icon. As an icon, CLOCK needs to
update the clock every minute rather than every second. When it is expanded
from an icon to a window, CLOCK changes the timer back to 1000 msec.

What to do if no timer is available

Windows allows only 16 timers to be active at any time. If no timer is
available, SetTimer returns NULL. Your program might be able to function
reasonably well without the timer, but if you need the timer (as CLOCK
certainly does), the application has no choice but to terminate if it can't
get one. If you call SetTimer in WinMain, you can terminate the program
simply by returning FALSE from WinMain.

Let's assume you want a 1000-msec timer. Following the CreateWindow call but
before the message loop, you might have a statement like this:

if (!SetTimer (hwnd, 1, 1000, NULL))
     return FALSE ;

This is the unfriendly way to terminate. The user is left wondering why the
application will not load. (Surely the 16 clocks sitting down in the icon
area have nothing to do with it!) It's much friendlier--and fairly easy--to
use a Windows message box for displaying a message. A complete discussion of
message boxes awaits you in Chapter 10, but this will get you started.

A message box is a popup window that always appears in the center of the
display. Message boxes have a caption bar but no size box. The caption bar
usually contains the name of the application. The message box encloses a
message and one, two, or three buttons (some combination of OK, Retry,
Cancel, Yes, No, and others). The message box can also contain a predefined
icon: a lowercase "i" (which stands for "information"), an exclamation
point, a question mark, or a stop sign. You have probably seen plenty of
message boxes when working with Windows.

This code creates an informatory message box that you can use when SetTimer
fails to allocate a timer:

if (!SetTimer (hwnd, 1, 1000, NULL))
     {
     MessageBox (hwnd,
          "Too many clocks or timers!",
          "Program Name",
          MB_ICONEXCLAMATION | MB_OK) ;
     return FALSE ;
     }

The message box is shown in Figure 5-1. When the user presses Enter or
clicks the OK button, WinMain terminates by returning FALSE.

  (Figure 5-1. may be found in the printed book.)

By default, message boxes are "application modal" windows. This means that a
user must respond to the message box before the application will continue.
However, the user can switch to other applications by pressing Alt-Tab or
Alt-Esc or by clicking the mouse in the window of another program.

Why not give the user the opportunity to close one of those 16 minimized
clocks at the bottom of the display and successfully load your application?
That's what this code does:

while (!SetTimer (hwnd, 1, 1000, NULL))

     if (IDCANCEL == MessageBox (hwnd,
               "Too many clocks or timers!",
               "Program Name",
               MB_ICONEXCLAMATION | MB_RETRYCANCEL))
          return FALSE ;

This message box, shown in Figure 5-2, has two buttons, labeled Retry and
Cancel. If the user selects Cancel, the MessageBox function returns a value
equal to IDCANCEL, and the program terminates. If the user selects Retry,
SetTimer is called again.

  (Figure 5-2. may be found in the printed book.)


A sample program

Figure 5-3 shows a sample program that uses the timer. This program, called
BEEPER1, sets a timer for 1-second intervals. When it receives a WM_TIMER
message, it alternates coloring the client area blue and red, and it beeps
by calling the function MessageBeep. (Although MessageBeep is documented as
a companion to MessageBox, it's really an all-purpose beep function. The
WORD parameter to MessageBeep can be any value.) BEEPER1 sets the timer in
the WinMain function and processes the WM_TIMER messages in the WndProc
window procedure. During the WM_TIMER message, BEEPER1 calls MessageBeep,
inverts the value of bFlipFlop and invalidates the window to generate a
WM_PAINT message. During the WM_PAINT message, BEEPER1 obtains a RECT
structure for the size of the window by calling GetClientRect and colors the
window by calling FillRect.

 BEEPER1.MAK

#-----------------------
# BEEPER1.MAK make file
#-----------------------

beeper1.exe : beeper1.obj beeper1.def
     link beeper1, /align:16, NUL, /nod slibcew libw, beeper1
     rc beeper1.exe

beeper1.obj : beeper1.c
     cl -c -Gsw -Ow -W2 -Zp beeper1.c

 BEEPER1.C

/*-----------------------------------------
   BEEPER1.C  -- Timer Demo Program No. 1
                 (c) Charles Petzold, 1990
  -----------------------------------------*/

#include 
#define ID_TIMER    1

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Beeper1" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          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 = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Beeper1 Timer Demo",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     while (!SetTimer (hwnd, ID_TIMER, 1000, NULL))
          if (IDCANCEL == MessageBox (hwnd,
                              "Too many clocks or timers!", szAppName,
                              MB_ICONEXCLAMATION | MB_RETRYCANCEL))
               return FALSE ;
     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL fFlipFlop = FALSE ;
     HBRUSH      hBrush ;
     HDC         hdc ;
     PAINTSTRUCT ps ;
     RECT        rc ;

     switch (message)
          {
          case WM_TIMER :
               MessageBeep (0) ;

               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.DEF

;------------------------------------
; BEEPER1.DEF module definition file
;------------------------------------

NAME           BEEPER1

DESCRIPTION    'Timer Demo Program No. 1 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

Because BEEPER1 audibly indicates every WM_TIMER message it receives, you
can get a good idea of the erratic nature of WM_TIMER messages by loading
BEEPER1 and performing some other actions within Windows. For instance, try
moving or resizing a window. This stops all messages, and BEEPER1 stops
beeping. When you complete the move or resize, you'll note that BEEPER1
doesn't get all the WM_TIMER messages it has missed, although the first two
messages may be less than a second apart.

This is our first encounter with a Windows program that uses color, so a
brief look at how Windows handles color is worthwhile here.


Windows' use of color

Windows uses an unsigned long (32-bit) integer value to represent a color.
The lowest three bytes specify red, green, and blue values that range from 0
through 255, as illustrated by Figure 5-4. This results in a potential 224
(or about 16 million) colors.

  (Figure 5-4. may be found in the printed book.)

This unsigned long is often referred to as an "RGB color." The WINDOWS.H
header file provides several macros for working with RGB color values. The
RGB macro in WINDOWS.H takes three arguments representing red, green, and
blue values and combines them into an unsigned long:

#define RGB(r,g,b) ((DWORD)(((BYTE)(r) | \
                        ((WORD)(g) << 8)) | \
                        (((DWORD)(BYTE)(b)) << 16)))

Thus, the value:

RGB (255, 0, 255)

is really 0x00FF00FF, an RGB color value for magenta. When all three
arguments are set to 0, the color is black; when the arguments are set to
255, the color is white. The GetRValue, GetGValue, and GetBValue macros
extract the unsigned character primary-color values from an unsigned long
RGB color value. These macros are sometimes handy when you're using Windows
functions that return RGB color values to your program.

The most common video display adapters used for Windows are the Enhanced
Graphics Adapter (EGA) and Video Graphics Array (VGA). In the display
resolutions that Windows uses, both these adapters can display 16 different
colors. (Some "Super VGA" boards can display 256 different colors under
Windows.) Windows can display additional colors by "dithering," which is
creating a pixel pattern that combines pixels of different pure colors.

Not all unique combinations of red, green, and blue bytes produce different
dithering patterns. For instance, on a color EGA or VGA, a red, green, or
blue value must generally be incremented by 4 to produce a different
dithering pattern. So for these adapters, you have 218 (or 262,144) dithered
colors.

BEEPER1 uses the FillRect function to color its client area. The first
parameter to FillRect is the device context handle, the second is a pointer
to the RECT structure, and the third is a handle to a "brush." A brush is a
graphics object that Windows uses to fill an area. Brushes can be solid
colors or composed of various hatchmarks or patterns.

BEEPER1 creates a brush of a solid color by calling CreateSolidBrush. The
only parameter is an RGB color value. Depending on the value of fFlipFlop,
BEEPER sets this parameter to RGB(255,0,0), which is red, or RGB(0,0,255),
which is blue.

A brush is a graphics object. If you create a brush, you must also delete it
when you're finished. After calling FillRect, BEEPER1 deletes the brush by
calling DeleteObject.



Method Two

The first method for setting the timer causes WM_TIMER messages to be sent
to the normal window procedure. With this second method, you can direct
Windows to send the timer messages to another function within your program.

The function that will receive these timer messages is termed a "call-back"
function. This is a function within your program that is called by Windows.
You tell Windows the address of this function (well, not really the address
of the function, but we'll get to that), and Windows later calls the
function. This should sound familiar because a program's window procedure is
really a type of call-back function. You tell Windows the address of the
function when registering the window class, and Windows calls the function
when sending messages to the program. However, call-back functions that are
not window procedures must be handled a little differently.

SetTimer is not the only Windows function that uses a call-back function.
The CreateDialog and DialogBox functions (discussed in Chapter 10) use
call-back functions to process messages in a dialog box; several Windows
functions (EnumChildWindows, EnumFonts, EnumObjects, EnumProps, and
EnumWindows) pass enumerated information to call-back functions; and several
less commonly used functions (GrayString, LineDDA, SetResourceHandler, and
SetWindowsHook) also require call-back functions. Call-back functions are
often a major hang-up for beginning Windows programmers. Some strange things
are involved. I'm first going to tell you how to use call-back functions,
and then I'll tell you the reasons for what you're doing.

Like a window procedure, a call-back function must be defined as FAR PASCAL
because it is called by Windows from outside the code segment of the
program. The parameters to the call-back function and the value returned
from the call-back function depend on the purpose of the call-back function.
In the case of the call-back function associated with the timer, the input
parameters are the same as the input parameters to a window procedure. The
timer call-back function returns a WORD value to Windows.

Let's name the call-back function TimerProc. (You can name it anything you
like.) It will process only WM_TIMER messages.

WORD FAR PASCAL TimerProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
[process WM_TIMER messages]
     return 0 ;
     }

The hwnd input parameter is the handle to the window specified when you call
SetTimer. Windows will send only WM_TIMER messages to TimerProc, so a
message will always equal WM_TIMER. The wParam value is the timer ID, and
the lParam value can be ignored. (It is set to the address of the function.)

Just as you must include your regular window procedure in the EXPORTS
section of the module definition (.DEF) file, you must also include the
names of any call-back functions within your program. When using a call-back
function named TimerProc, your module definition file contains the following
lines:

EXPORTS WndProc
        TimerProc

As I noted earlier, the first method for setting a timer requires a SetTimer
call that looks like this:

SetTimer (hwnd, 1, wMsecInterval, NULL) ;

When you use a call-back function to process WM_TIMER messages, the fourth
parameter to SetTimer is instead the far address of the call-back function.

But not really.

Now listen carefully: The far address that you must pass to Windows as the
fourth parameter of the SetTimer call is not the address of the function
within the program. It is instead a far address obtained from the Windows
function MakeProcInstance. To use MakeProcInstance, first define a variable
that is a far pointer to a function. You can use the WINDOWS.H identifier
FARPROC for this definition:

FARPROC lpfnTimerProc ;

In WinMain (or any other section of your program that is executed only once
for each instance), call MakeProcInstance. The parameters to
MakeProcInstance are the address of TimerProc and the value of hInstance.
MakeProcInstance returns a far pointer that you save in lpfnTimerProc:

lpfnTimerProc = MakeProcInstance (TimerProc, hInstance) ;

You can now use this lpfnTimerProc value when you call SetTimer:

SetTimer (hwnd, 1, wMsecInterval, lpfnTimerProc) ;

You're done. Now that's not too bad, is it?

Well, you may say, "This is so weird that I'll never use this method for
setting a timer. I'll use the first method, where I don't have to bother
with call-back functions." That's fine. But you're going to be forced to
deal with call-back functions, EXPORTS, and MakeProcInstance when we start
discussing dialog boxes. You can't do a dialog box without them. So you can
pay your dues now, or you can pay them later.

A sample program

Let's look at some sample code so you can see how this stuff fits together.
Then we'll explore MakeProcInstance some more. The BEEPER2 program, shown in
Figure 5-5, is functionally the same as BEEPER1 except that Windows sends
the timer messages to TimerProc rather than WndProc. To tell the C compiler
that TimerProc is a function so that we can use the name of the function
when calling MakeProcInstance, we declare TimerProc at the top of the
program along with WndProc. Notice that the program calls MakeProcInstance
for each instance.

I mentioned above that the lParam value passed to TimerProc is the address
of the TimerProc function. Not exactly. It is actually the value of
lpfnTimerProc returned from MakeProcInstance, if you ever need to use it.

 BEEPER2.MAK

#-----------------------
# BEEPER2.MAK make file
#-----------------------


beeper2.exe : beeper2.obj beeper2.def
     link beeper2, /align:16, NUL, /nod slibcew libw, beeper2
     rc beeper2.exe

beeper2.obj : beeper2.c
     cl -c -Gsw -Ow -W2 -Zp beeper2.c

 BEEPER2.C

/*----------------------------------------
   BEEPER2.C -- Timer Demo Program No. 2
                (c) Charles Petzold, 1990
  ----------------------------------------*/

#include 
#define ID_TIMER    1

long FAR PASCAL WndProc   (HWND, WORD, WORD, LONG) ;
WORD FAR PASCAL TimerProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Beeper2" ;
     FARPROC     lpfnTimerProc ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          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 = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Beeper2 Timer Demo",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     lpfnTimerProc = MakeProcInstance (TimerProc, hInstance) ;

     while (!SetTimer (hwnd, ID_TIMER, 1000, lpfnTimerProc))
          if (IDCANCEL == MessageBox (hwnd,
                              "Too many clocks or timers!", szAppName,
                              MB_ICONEXCLAMATION | MB_RETRYCANCEL))
               return FALSE ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     switch (message)
          {
          case WM_DESTROY :
               KillTimer (hwnd, ID_TIMER) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

WORD FAR PASCAL TimerProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
     static BOOL fFlipFlop = FALSE ;
     HBRUSH      hBrush ;
     HDC         hdc ;
     RECT        rc ;
     MessageBeep (0) ;
     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) ;

     return 0 ;
     }

 BEEPER2.DEF

;------------------------------------
; BEEPER2.DEF module definition file
;------------------------------------

NAME           BEEPER2

DESCRIPTION    'Timer Demo Program No. 2 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               TimerProc


Proper handling of call-back functions

Let's summarize the three requirements for call-back functions. Any function
within your program that is called by Windows must be handled as follows:

  1.  The function must be defined as FAR PASCAL.

  2.  The function must be included in the EXPORTS section of the module
      definition (.DEF) file.

  3.  The address of the function that you give to Windows must be the
      return value from a MakeProcInstance call. (This third rule does not
      apply to window procedures that are passed to Windows as part of a
      window's class structure in a RegisterClass call. Windows itself
      handles the MakeProcInstance requirement in this case.)

      As you'll discover in Chapter 7, "Memory Management," these three
      requirements are closely related to each other. They are part of the
      overhead necessary for Windows to run several instances of the same
      program using the same code segment, where each instance must have its
      own data segment. Windows also requires this overhead to move the code
      segment and the data segments around in memory. Here are the practical
      results of the three requirements:

      When you define a function as FAR and you compile with the -Gw
      (Windows) flag, the compiler inserts special prolog and epilog code in
      the function. In assembly language, the prolog code sets the value of
      the AX register equal to the DS (data segment) register by using the
      PUSH DS and POP AX instructions. It then saves the value of DS (with
      PUSH DS) and sets DS equal to AX by using a MOV DS, AX instruction.
      The epilog code at the end of the function pops the original value of
      DS off the stack.

  4.  If the FAR function is also exported (that is, if the function is
      listed in the EXPORTS section of the module definition file), Windows
      replaces the PUSH DS and POP AX instructions at the top of the
      function prolog with NOP (no operation) instructions when the code
      segment is loaded into memory. With this change the function sets DS
      from the value of AX. But what is the value of AX on entry to the
      function? Well, if you didn't do anything else, the value of AX would
      be indeterminate. So would the operation of your program.

  5.  MakeProcInstance creates a small piece of code elsewhere in Windows
      called a "thunk." The far address returned from MakeProcInstance is
      the address of this thunk. The thunk loads the segment address of the
      data segment in AX and branches to the function. The function prolog
      then loads DS from AX. Perfect.

Note that MakeProcInstance requires hInstance as a parameter. You must use
MakeProcInstance to create a different thunk for each instance because each
instance has its own data segment. For a particular call-back function, the
thunks for each instance all branch to the same function address (because
all instances use the same code segment), but each thunk sets AX to a
different data segment--the data segment for that instance. When Windows
moves a data segment in memory, it must change the thunk for that instance
so that the thunk sets AX to the new data segment address. The thunk itself
is always in an unmoveable area of memory.

(To further complicate this matter, any reference in your program to a FAR
function--such as the address passed to the MakeProcInstance call and the
address that the thunk branches to--is not even the address of the function
within your program. Another small routine sits between the thunk and the
actual function. This routine loads the code segment into memory if it has
not yet been loaded or if it has been discarded from memory. But let's
forget about this for now and come back to it in Chapter 7. The last thing I
want to do here is make this subject sound as complex as it actually is.)

These requirements imply that you should never call exported far functions
directly from within your program. For instance, you might want to simulate
a timer message with the following statement:

TimerProc (hwnd, WM_TIMER, 1, 0L) ;  // WRONG !!!

It looks OK, but don't do it! The prolog of TimerProc will set DS equal to
AX, but the value of AX could be anything, and it very likely is not the
segment address of the program's data segment. If you need to directly call
an exported function from within your program, use the far pointer returned
from MakeProcInstance:

(*lpfnTimerProc) (hwnd, WM_TIMER, 1, 0L) ;  // RIGHT

This code calls the thunk, and the thunk sets AX equal to the correct data
segment before branching to the function.



Method Three

The third method of setting the timer is similar to the second method. It
requires a far pointer created from MakeProcInstance to a function that
processes the WM_TIMER messages. However, the hwnd parameter to SetTimer is
set to NULL, and the second parameter (normally the timer ID) is ignored.
Instead, the function returns a timer ID:

nTimerID = SetTimer (NULL, 0, wMsecInterval, lpfnTimerProc) ;

The nTimerID returned from SetTimer will be NULL if no timer is available.

The first parameter to KillTimer (usually the window handle) must also be
NULL. The timer ID must be the value returned from SetTimer:

KillTimer (NULL, nTimerID) ;

The hwnd parameter passed to the TimerProc timer function will also be NULL.
The wParam parameter is the timer ID, and lParam is lpfnTimerProc, the same
as in the second method.

This method for setting a timer is rarely used. It might come in handy if
you do a lot of SetTimer calls at different times in your program and don't
want to keep track of which timer IDs you've already used.

Now that you know how to use the Windows timer, you're ready for a couple of
useful timer programs.



USING THE TIMER FOR A STATUS REPORT

One use of a Windows timer is to periodically update a status report
displayed on the screen. The program can relinquish control until the next
WM_TIMER message and thus not hog precious processing time. The FREEMEM
program, shown in Figure 5-6 on the following pages, displays the amount of
free memory available in Windows in megabytes. The free memory value is
updated every second and is consistent with the figure shown in the Program
Manager's and File Manager's About box. FREEMEM can let you know how close
Windows is to running out of memory. While testing a new Windows program,
you may want to keep an eye on FREEMEM for a rough indication of how your
program is allocating and freeing memory.

 FREEMEM.MAK

#-----------------------
# FREEMEM.MAK make file
#-----------------------

freemem.exe : freemem.obj freemem.def
     link freemem, /align:16, NUL, /nod slibcew win87em libw, freemem
     rc freemem.exe

freemem.obj : freemem.c
     cl -c -Gsw -Ow -W2 -Zp freemem.c

 FREEMEM.C

/*------------------------------------------
   FREEMEM.C -- Free Memory Display Program
                (c) Charles Petzold, 1990
  ------------------------------------------*/

#include 
#include 
#define ID_TIMER    1

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "FreeMem" ;
     HDC         hdc ;
     HWND        hwnd ;

     MSG         msg ;
     TEXTMETRIC  tm ;
     WNDCLASS    wndclass ;



     if (hPrevInstance)
          return FALSE ;

     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 = GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     RegisterClass (&wndclass) ;

     hwnd = CreateWindow (szAppName, "Free Memory",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     hdc = GetDC (hwnd) ;
     GetTextMetrics (hdc, &tm) ;
     ReleaseDC (hwnd, hdc) ;

     if (4 * tm.tmAveCharWidth > GetSystemMetrics (SM_CXICON) ||
               2 * tm.tmHeight > GetSystemMetrics (SM_CYICON))
          {
          MessageBox (hwnd, "Icon size too small for display!",
                      szAppName, MB_ICONEXCLAMATION | MB_OK) ;
          return FALSE ;
          }

     if (!SetTimer (hwnd, ID_TIMER, 1000, NULL))
          {
          MessageBox (hwnd, "Too many clocks or timers!",
                      szAppName, MB_ICONEXCLAMATION | MB_OK) ;
          return FALSE ;
          }

     ShowWindow (hwnd, SW_SHOWMINNOACTIVE) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static DWORD  dwFreeMem, dwPrevMem ;
     static RECT   rect ;
     char          cBuffer [20] ;
     HDC           hdc ;
     PAINTSTRUCT   ps ;

     switch (message)
          {
          case WM_TIMER :
               dwFreeMem = GetFreeSpace (0) ;

               if (dwFreeMem != dwPrevMem)
                    InvalidateRect (hwnd, NULL, TRUE) ;

               dwPrevMem = dwFreeMem ;
               return 0 ;

          case WM_SIZE :
               GetClientRect (hwnd, &rect) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               DrawText (hdc, cBuffer,
                         sprintf (cBuffer, "%.2f megs",
                                  dwFreeMem / 1024.0 / 1024.0),
                         &rect, DT_WORDBREAK) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_QUERYOPEN :
               return 0 ;

          case WM_DESTROY :
               KillTimer (hwnd, ID_TIMER) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 FREEMEM.DEF

;------------------------------------
; FREEMEM.DEF module definition file
;------------------------------------

NAME           FREEMEM

DESCRIPTION    'Free Memory Display (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

Because FREEMEM doesn't need much display space, I've written it to appear
as an icon at the bottom of the Windows screen. (See Figure 5-7.) This is
about as unobtrusive a window as you can create in Windows.

  (Figure 5-7. may be found in the printed book.)

Although FREEMEM's use of the timer is simple enough, the program
illustrates some interesting tricks that admittedly have nothing to do with
the timer.

Creative Use of Icons

Unlike most Windows programs, FREEMEM starts out life as an icon, displays
everything it needs to display within the icon, and cannot be opened into a
regular window. Most Windows applications have static pictorial icons that
you specify in the windows class structure. These icons are usually created
with the ICONEDIT utility supplied with the Windows Software Development Kit
(as you'll see in Chapter 8). So far, we've been using predefined Windows
icons in our programs. Alternatively, you can specify a NULL icon in the
window class structure with the statement:

wndclass.hIcon = NULL ;

A NULL icon means that the application is responsible for drawing the icon.
Windows sends the application WM_PAINT messages when the icon needs to be
painted. Thus, a  program can change the appearance of the icon while the
program is running. The CLOCK application included with Windows uses this
technique to display the clock even when the window is an icon. A NULL icon
is really just a tiny window that you can draw on in the same way that you
draw on the client area of a normal window. If you need to know when your
application is becoming an icon, you can get the information from the wParam
parameter of a WM_SIZE message. For instance, when CLOCK becomes an icon, it
eliminates the second hand from the clock and changes the timer from a
1-second interval to a 1-minute interval.

FREEMEM displays two lines of text within its icon. This will work fine on
the most common displays used for Windows, but it may not work for some
high-resolution boards that may use a larger system font. Because FREEMEM
cannot display correctly on these video boards, it checks the size of the
system font against the icon size and uses a message box to inform the user
if the icon size is too small.


Forcing the Icon

Shortly before entering the message loop in WinMain, most Windows
applications execute the function:

ShowWindow (hwnd, nCmdShow) ;

The nCmdShow variable is passed to the program as a parameter to WinMain.

If you run a program from the File Manager (by selecting Run from the File
menu, pressing Enter when the cursor is on the program name, or
double-clicking the program), nCmdShow is set equal to SW_SHOWNORMAL. If you
load an application as an icon (by selecting Load from the File menu or
pressing Shift-Enter with the cursor on the program name), nCmdShow is set
equal to SW_SHOWMINNOACTIVE. Your application usually doesn't have to figure
this out but simply passes this parameter to ShowWindow.

However, you aren't required to use the nCmdShow variable with ShowWindow.
Instead, FREEMEM uses the line:

ShowWindow (hwnd, SW_SHOWMINNOACTIVE) ;

This forces the window to appear as an icon regardless of the value of
nCmdShow. The active program remains active.

You can perform other tricks with this technique. If you always want a
particular application to appear first as a maximized full-screen display,
you can use:

ShowWindow (hwnd, SW_SHOWMAXIMIZED) ;

You can even force FREEMEM to occupy a particular icon position at the
bottom of the display. If you replace the existing ShowWindow call in
FREEMEM with:

ShowWindow (hwnd, (int) 0xFF8F) ;

the icon will be positioned in icon slot 15. This syntax is a little obscure
but is documented in the Programmer's Reference.


Keeping the Icon an Icon

FREEMEM does not allow itself to be opened into a regular window. Would you
believe this trick requires merely two simple lines in the WndProc function?
Here they are:

case WM_QUERYOPEN :
     return 0 ;

These two lines don't seem to be doing very much, but let's take a closer
look.

Windows sends a WM_QUERYOPEN message to a program when it wants to open an
icon into a window. Normally, WM_QUERYOPEN is passed on to the DefWindowProc
function, which returns a nonzero value; Windows then opens the icon. With
the two lines shown above, however, WndProc returns a value of 0 for a
WM_QUERYOPEN message. So when Windows asks, "Do you want to be opened?"
WndProc answers "Zero," which in this case means "No thanks."


Calculating Free Memory

When Windows sends FREEMEM a WM_TIMER message, FREEMEM must determine the
amount of free memory. Like the About box in the Program Manager and File
Manager, FREEMEM gets a free memory value by calling GetFreeSpace with a
parameter of 0. The GetFreeSpace function is new in Windows 3. Prior to
Windows 3, Windows programs used GlobalCompact with a parameter of 0 to
obtain the largest block of contiguous free memory in the system, which is
not nearly as useful as the total amount of free memory. (Windows memory
management is covered in detail in Chapter 7.)

If the free memory value has changed since the last GetFreeSpace call,
FREEMEM invalidates the client area to generate a WM_PAINT message. FREEMEM
processes WM_PAINT messages by calling DrawText, a convenient function for
simple word-wrapped text. FREEMEM converts the free memory in bytes to a
floating-point value in megabytes, and sprintf stores it formatted to two
decimal places.


Using Floating-Point Math

When a Windows program uses floating-point math (as FREEMEM does) and you
link with the floating-point emulator library (SLIBCEW.LIB for small model),
you must also include the WIN87LIB.LIB import library in the library field
of the LINK command.

Alternatively, you can compile with the -FPa switch and link with the
"alternate math library" (SLIBCAW.LIB for small library). This library does
not use the math coprocessor chip, even if one is present.



USING THE TIMER FOR A CLOCK

A clock is the most obvious application for the timer. Although digital
clocks were once in fashion, the pendulum has swung back (so to speak) to
analog clocks. But you already have an analog clock with Windows. Although
the CLOCK program has a digital-clock setting, I'm going to write a good old
digital-clock program--because it provides an interesting example of the use
of the timer. The DIGCLOCK program, shown in Figure 5-8, creates a popup
window that positions itself in the lower right corner of the display in the
icon area. The program displays the day of the week, the time, and the date.
(See Figure 5-9 on page 202.)

 DIGCLOCK.MAK

#------------------------
# DIGCLOCK.MAK make file
#------------------------

digclock.exe : digclock.obj digclock.def
     link digclock, /align:16, NUL, /nod slibcew libw, digclock
     rc digclock.exe

digclock.obj : digclock.c
     cl -c -Gsw -Ow -W2 -Zp digclock.c

 DIGCLOCK.C

/*-----------------------------------------
   DIGCLOCK.C -- Digital Clock Program
                 (c) Charles Petzold, 1990
  -----------------------------------------*/

#include 
#include 
#define ID_TIMER    1

#define YEAR  (datetime->tm_year % 100)
#define MONTH (datetime->tm_mon  + 1)
#define MDAY  (datetime->tm_mday)
#define WDAY  (datetime->tm_wday)
#define HOUR  (datetime->tm_hour)
#define MIN   (datetime->tm_min)
#define SEC   (datetime->tm_sec)



long FAR PASCAL WndProc (HWND, WORD, WORD, LONG);
void SizeTheWindow (short *, short *, short *, short *) ;

char  sDate [2], sTime [2], sAMPM [2][5] ;
int   iDate, iTime ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "DigClock" ;
     HWND        hwnd;
     MSG         msg;
     short       xStart, yStart, xClient, yClient ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          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 = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     SizeTheWindow (&xStart, &yStart, &xClient, &yClient) ;

     hwnd = CreateWindow (szAppName, szAppName,
                          WS_POPUP | WS_DLGFRAME | WS_SYSMENU,
                          xStart,  yStart,
                          xClient, yClient,
                          NULL, NULL, hInstance, NULL) ;

     if (!SetTimer (hwnd, ID_TIMER, 1000, NULL))
          {
          MessageBox (hwnd, "Too many clocks or timers!", szAppName,
                      MB_ICONEXCLAMATION | MB_OK) ;
          return FALSE ;
          }

     ShowWindow (hwnd, SW_SHOWNOACTIVATE) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

void SizeTheWindow (short *pxStart,  short *pyStart,
                    short *pxClient, short *pyClient)
     {
     HDC        hdc ;
     TEXTMETRIC tm ;

     hdc = CreateIC ("DISPLAY", NULL, NULL, NULL) ;
     GetTextMetrics (hdc, &tm) ;
     DeleteDC (hdc) ;

     *pxClient = 2 * GetSystemMetrics (SM_CXDLGFRAME) + 16*tm.tmAveCharWidth
;
     *pxStart  =     GetSystemMetrics (SM_CXSCREEN)   - *pxClient ;
     *pyClient = 2 * GetSystemMetrics (SM_CYDLGFRAME) + 2*tm.tmHeight ;
     *pyStart  =     GetSystemMetrics (SM_CYSCREEN)   - *pyClient ;
     }

void SetInternational (void)
     {
     static char cName [] = "intl" ;

     iDate = GetProfileInt (cName, "iDate", 0) ;
     iTime = GetProfileInt (cName, "iTime", 0) ;

     GetProfileString (cName, "sDate",  "/", sDate,     2) ;
     GetProfileString (cName, "sTime",  ":", sTime,     2) ;
     GetProfileString (cName, "s1159", "AM", sAMPM [0], 5) ;
     GetProfileString (cName, "s2359", "PM", sAMPM [1], 5) ;
     }

void WndPaint (HWND hwnd, HDC hdc)
     {
     static char szWday[] = "Sun\0Mon\0Tue\0Wed\0Thu\0Fri\0Sat" ;
     char        cBuffer[40] ;
     long        lTime ;
     RECT        rect ;
     short       nLength ;
     struct tm   *datetime ;

     time (&lTime) ;
     datetime = localtime (&lTime) ;
     nLength = wsprintf (cBuffer, "  %s  %d%s%02d%s%02d  \r\n",
               (LPSTR) szWday + 4 * WDAY,
               iDate == 1 ? MDAY  : iDate == 2 ? YEAR  : MONTH, (LPSTR)
sDate,
               iDate == 1 ? MONTH : iDate == 2 ? MONTH : MDAY,  (LPSTR)
sDate,
               iDate == 1 ? YEAR  : iDate == 2 ? MDAY  : YEAR) ;

     if (iTime == 1)
          nLength += wsprintf (cBuffer + nLength, "  %02d%s%02d%s%02d  ",
                               HOUR, (LPSTR) sTime, MIN, (LPSTR) sTime, SEC)
;
     else
          nLength += wsprintf (cBuffer + nLength, "  %d%s%02d%s%02d %s  ",
                               (HOUR % 12) ? (HOUR % 12) : 12,
                               (LPSTR) sTime, MIN, (LPSTR) sTime, SEC,
                               (LPSTR) sAMPM [HOUR / 12]) ;

     GetClientRect (hwnd, &rect) ;
     DrawText (hdc, cBuffer, -1, &rect, DT_CENTER | DT_NOCLIP) ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     HDC         hdc ;
     PAINTSTRUCT ps ;

     switch (message)
          {
          case WM_CREATE :
               SetInternational () ;
               return 0 ;

          case WM_TIMER :
               InvalidateRect (hwnd, NULL, FALSE) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;
               WndPaint (hwnd, hdc) ;
               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_WININICHANGE :
               SetInternational () ;
               InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;

          case WM_DESTROY :
               KillTimer (hwnd, ID_TIMER) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 DIGCLOCK.DEF

;-------------------------------------
; DIGCLOCK.DEF module definition file
;-------------------------------------

NAME           DIGCLOCK

DESCRIPTION    'Digital Clock (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

  (Figure 5-9. may be found in the printed book.)

All the programs shown so far have used the window style WS_OVERLAPPEDWINDOW
as the third parameter to the CreateWindow function. DIGCLOCK uses the
window style:

WS_POPUP | WS_DLGFRAME | WS_SYSMENU

This creates a style of window known as "popup," with a dialog box frame and
a system menu. The popup style is most commonly used for dialog boxes and
message boxes, and only rarely for applications. DIGCLOCK also uses yet
another variation of the ShowWindow call:

ShowWindow (hwnd, SW_SHOWNOACTIVATE) ;

Normally, a program becomes the active window when you run it.
SW_SHOWNOACTIVATE tells Windows that the program that loaded DIGCLOCK should
remain the active window. You can make DIGCLOCK active, however, by clicking
on its window with the mouse or by pressing Alt-Tab or Alt-Esc. Although
DIGCLOCK has no system menu box, you can still access the system menu when
DIGCLOCK is active by pressing Alt-Spacebar. If you select Move, you can
move the window with the keyboard.

Positioning and Sizing the Popup

The DIGCLOCK popup window is positioned at the lower right corner of the
display. The window must be large enough to accommodate two lines of text of
16 characters each. The SizeTheWindow procedure in DIGCLOCK.C determines the
correct parameters to use in the CreateWindow call. Normally, a program
cannot obtain a text size without first creating a window, because it needs
the window handle to obtain a device context handle. DIGCLOCK gets around
this problem by obtaining an information device context handle for the
screen using CreateIC. This function is similar to CreateDC (used in the
BLOWUP program in Chapter 4) but is used to obtain information from the
device context. The text size in combination with information available from
GetSystemMetrics is enough to derive an initial starting position and window
size.


Getting the Date and Time

In its WndPaint function, DIGCLOCK uses the time and localtime C functions
available in the Microsoft C Compiler library to determine the current date
and time. The localtime function puts all the information we need into a
structure; several macro definitions near the top of the program help make
the wsprintf calls more readable. (You should avoid making MS-DOS or ROM
BIOS function calls in your Windows programs; use Windows functions or the C
run time library instead.)


Going International

Windows includes international support. The WIN.INI file created during
installation of Windows contains a section headed [intl]. This lists
information concerning formats of dates, time, currency, and numbers. You
can display dates in one of three different formats: month-day-year,
year-month-day, or day-month-year. The separator between these three numbers
can be a slash, a dash, a period, or, in fact, any character you like. You
can display the time in either 12-hour or 24-hour format; a semicolon or a
period is commonly used to separate hours, minutes, and seconds.

The SetInternational function in DIGCLOCK retrieves this formatting
information from the WIN.INI file by using the Windows GetProfileInt (for
integers) and GetProfileString (for strings). These calls must include
default values if Windows cannot find the values in WIN.INI.
SetInternational stores the values in global variables that have the same
names as the text strings that identify them in WIN.INI. The WndPaint
function uses the values obtained from WIN.INI to format the date and time
displays and then calls DrawText to center the two lines of text within the
window.

As you would expect, whenever DIGCLOCK's window procedure receives a
WM_TIMER message, it invalidates the window to generate a WM_PAINT message.
But WndProc also invalidates the window when it receives a WM_WININICHANGE
message. Any application that changes WIN.INI sends the WM_WININICHANGE
message to all active Windows applications. If the [intl] section of WIN.INI
is changed, DIGCLOCK will know and will obtain the new international
information. To see how this works, load DIGCLOCK, load the CONTROL PANEL
program included with Windows, select Country Settings from the Preferences
menu, and change either the date format, the date separator, the time
format, or the time separator. Now press Enter. The Control Panel updates
the WIN.INI file, and DIGCLOCK's display reflects that change--Windows'
message magic at work.

When the window procedure receives a WM_WININICHANGE message, it invalidates
the window using:

InvalidateRect (hwnd, NULL, TRUE) ;

When DIGCLOCK receives a WM_TIMER message, it invalidates the window using:

InvalidateRect (hwnd, NULL, FALSE) ;

A value of TRUE in the last parameter tells Windows to erase the background
before drawing the window. A value of FALSE tells Windows simply to draw
over the existing background. We use FALSE when processing WM_TIMER messages
because this approach reduces flickering of the display. You may be
wondering why we need to use the TRUE value at all.

A TRUE value is necessary when processing WM_WININICHANGE messages because
the length of the displayed strings can change by several characters if you
switch the time format from 12 hours to 24 hours. However, the largest
change that occurs as a result of a WM_TIMER message is two characters--for
instance, when the date advances from 12/31/87 to 1/1/88--and the formatted
string that WndPaint uses for the display has a couple of blanks on each end
to account for this change in length and the proportional font.

We could also have DIGCLOCK process WM_TIMECHANGE messages, which notify
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.



WINDOWS STANDARD TIME

If you've been scouting around the Programmer's Reference of the Windows
Software Development Kit, you may be wondering why the Windows
GetCurrentTime function is not used in DIGCLOCK. The answer is that
GetCurrentTime tells you about "Windows time" rather than real time. This is
the time (in milliseconds) since the beginning of the current Windows
session. GetCurrentTime is used mostly for calculating a difference from the
time returned from GetMessageTime. You can use these two calls while
processing a message to determine how long the message was in the message
queue before you retrieved it for processing.