Chapter 2  Painting with Text
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

In the previous chapter you saw a simple Windows program that displayed a
single line of text in the center of its client area. The client area
occupies all the space of the window that is not taken up by the caption
bar, the window-sizing border, the menu bar (if any), and scroll bars (if
any). The client area is the part of the window on which a program is free
to draw. You can do almost anything you want with that client
area--anything, that is, except assume that it will be a particular size or
that the size will remain constant while your program is running. If you are
accustomed to writing programs for the IBM PC, this exception may come as a
bit of a shock. You can no longer think in terms of 25 lines and 80 columns
of text. Your program shares the video display with other Windows programs.
The user controls how the programs are arranged on the screen. Your program
must accept the size it's given and do something reasonable with it. (A
program could create a window of a specific fixed size, but it isn't very
common.)

This works both ways. Just as your program may find itself with a client
area barely large enough in which to say "Hello," it may also someday be run
on a big-screen high-resolution video system and discover a client area big
enough for two entire pages of text and plenty of closet space besides.
Dealing intelligently with both these eventualities is an important part of
Windows programming.

Although Windows has extensive Graphics Device Interface (GDI) functions for
displaying graphics, in this chapter I'll stick to displaying simple lines
of text. I'll also ignore the various fonts (typefaces) and font sizes that
Windows makes available and use only Windows' default "system font." This
may seem limiting, but it really isn't. The problems  we encounter--and
solve--in this chapter apply to all Windows programming. When you display a
combination of text and graphics (as, for instance, the Windows CALENDAR,
CARDFILE, and CALCULATOR programs do), the character dimensions of Windows'
default system font often determine the dimensions of the graphics.

This chapter is ostensibly about learning how to paint, but it's really
about learning the basics of device-independent programming. Windows
programs can assume little about their environment. Instead, they must use
the facilities that Windows provides to obtain information about the
environment.

PAINTING AND REPAINTING

Under MS-DOS, a program using the display in a full-screen mode can write to
any part of the display. What the program puts on the display will stay
there and will not mysteriously disappear. The program can then discard
information needed to re-create the screen display. If another program (such
as a RAM-resident popup) overlays part of the display, then the popup is
responsible for restoring the display when it leaves.

In Windows, you can display only to the client area of your window, and you
cannot be assured that what you display to the client area will remain there
until your program specifically writes over it. For instance, the dialog box
from another application may overlay part of your client area. Although
Windows will attempt to save and restore the area of the display underneath
the dialog box, it sometimes cannot do so. When the dialog box is removed
from the screen, Windows will request that your program repaint this portion
of your client area.

Windows is a message-driven system. Windows informs applications of various
events by posting messages in the application's message queue or sending
messages to the appropriate window procedure. Windows informs a window
procedure that part of the window's client area needs updating by posting a
WM_PAINT message.

The WM_PAINT Message

Most Windows programs call the function UpdateWindow during initialization
in WinMain shortly before entering the message loop. Windows takes this
opportunity to send the window procedure its first WM_PAINT message. That
message informs your window procedure that the client area is ready to be
painted. Thereafter, that window procedure should be ready at any time to
process additional WM_PAINT messages and even repaint the entire client area
of the window if necessary. A window procedure receives a  WM_PAINT message
whenever one of the following occurs:

  þ   A previously hidden area of the window is brought into view when a
      user moves a window or uncovers a window.

  þ   The user resizes the window (if the window class style has the CS-
      _HREDRAW and CS_VREDRAW bits set).

  þ   The program uses the ScrollWindow function to scroll part of its
      client area.

  þ   The program uses the InvalidateRect or InvalidateRgn function to
      explicitly generate a WM_PAINT message.

In some cases in which part of the client area is temporarily written over,
Windows attempts to save an area of the display and restore it later. This
is not always successful. Windows may sometimes post a WM_PAINT message
when:

  þ   Windows removes a dialog box or message box that was overlaying part
      of the window.

  þ   A menu is pulled down and then released.

In a few cases, Windows always saves the area of the display it overwrites
and then restores it. This is the case whenever:

  þ   The cursor is moved across the client area.

  þ   An icon is dragged across the client area.

Dealing with WM_PAINT messages requires that you alter your thinking about
how you write to the display. Your program should be structured so that it
accumulates all the information necessary to paint the client area but
paints only "on demand"--when Windows sends the window procedure a WM_PAINT
message. If your program needs to update its client area, it can force
Windows to generate this WM_PAINT message. This may seem a roundabout method
of displaying something on the screen, but the structure of your programs
will benefit from it.


Valid and Invalid Rectangles

Although a window procedure should be prepared to update the entire client
area whenever it receives a WM_PAINT message, it often needs to update only
a smaller rectangular area. This is most obvious when part of the client
area is overlaid by a dialog box. Repainting is required only for the
rectangular area uncovered when the dialog box is removed.

That rectangular area is known as an "invalid rectangle." The presence of an
invalid rectangle in a client area is what prompts Windows to place a
WM_PAINT message in the application's message queue. Your window procedure
receives a WM_PAINT message only if part of your client area is invalid.

Windows internally maintains a "paint information structure" for each
window. This structure contains (among other information) the coordinates of
the invalid rectangle. If another rectangular area of the client area
becomes invalid before the window procedure processes the WM_PAINT message,
Windows calculates a new invalid rectangle that encompasses both areas and
stores this updated information in the paint information structure. Windows
does not place multiple WM_PAINT messages in the message queue.

A window procedure can invalidate a rectangle in its own client area by
calling InvalidateRect. If the message queue already contains a WM_PAINT
message, Windows calculates a new invalid rectangle. Otherwise, it places a
WM_PAINT message in the message queue. A window procedure can obtain the
coordinates of the invalid rectangle when it receives a WM_PAINT message (as
we'll see shortly). It can also obtain these coordinates at any other time
by calling GetUpdateRect.

After the window procedure calls EndPaint during the WM_PAINT message, the
entire client area is validated. A program can also validate any rectangular
region in the client area by calling the ValidateRect function. If this call
has the effect of validating the entire invalid area, then any WM_PAINT
message currently in the queue is deleted.



AN INTRODUCTION TO GDI

To paint the client area of your window, you use Windows' Graphics Device
Interface (GDI) functions. (A full discussion of GDI is in Chapters 11\-15.)
Windows provides five GDI functions for writing text strings to the client
area of the window. We've already encountered the DrawText function in
Chapter 1, but the most popular text output function by far is TextOut. This
function has the following format:

TextOut (hdc, x, y, lpsString, nLength) ;

TextOut writes a character string to the display. The lpsString parameter is
a long (or far) pointer to the character string, and nLength is the length
of the string. The x and y parameters define the starting position, in
"logical coordinates," of the character string in the client area. The hdc
parameter is a "handle to a device context," and it is an important part of
GDI. Virtually every GDI function requires this handle as the first
parameter to the function.

The Device Context

A handle, you'll recall, is simply a number that Windows uses for internal
reference to an object. You obtain the handle from Windows and then use the
handle in other functions. The device context handle is your window's
passport to the GDI functions. With that device context handle you are free
to paint your client area and make it as beautiful or as ugly as you like.

The device context (also called the "DC") is really a data structure
maintained by GDI. A device context is associated with a particular display
device, such as a printer, plotter, or video display. For a video display, a
device context is usually associated with a particular window on the
display.

Windows uses the values in the device context structure (also called
"attributes" of the device context) in conjunction with the GDI functions.
With TextOut, for instance, the  attributes of the device context determine
the color of the text, the color of the text background, how the
x-coordinate and y-coordinate are mapped to the client area of the window,
and what font Windows uses when displaying the text.

When a program needs to paint, it must first obtain a handle to a device
context. After it has finished painting, the program should release the
handle. When a program releases the handle, the handle is no longer valid
and must not be used. The program should obtain the handle and release the
handle during processing of a single message. Except for a device context
created with a call to CreateDC, you should not keep a device context handle
around from one message to another.

Windows applications generally use two methods for getting the handle to the
device context in preparation for painting the screen.


Getting a Device Context Handle: Method One

You use this method when you process WM_PAINT messages. Two functions are
involved: BeginPaint and EndPaint. These two functions require the handle to
the window (passed to the window procedure as a parameter) and the address
of a structure variable of type PAINTSTRUCT. Windows programmers usually
name this structure variable ps and define it within the window procedure,
like so:

PAINTSTRUCT ps ;

While processing a WM_PAINT message, a Windows function first calls
BeginPaint to fill in the fields of the ps structure. The value returned
from BeginPaint is the device context handle. This is commonly saved in a
variable named hdc. You define this variable in your window procedure like
this:

HDC hdc ;

The HDC data type is defined in WINDOWS.H as a HANDLE. The program may then
use GDI functions such as TextOut. A call to EndPaint releases the device
context handle and validates the window.

Typically, processing of the WM_PAINT message looks like this:

case WM_PAINT :
     hdc = BeginPaint (hwnd,&ps) ;
[use GDI functions]
     EndPaint (hwnd, &ps) ;
     return 0 ;

The window procedure must call BeginPaint and EndPaint as a pair while
processing the WM_PAINT message. If a window procedure does not process
WM_PAINT messages, then it must pass the WM_PAINT message to DefWindowProc
(the default window procedure) located in Windows.

DefWindowProc processes WM_PAINT messages with the following code:

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

This sequence of BeginPaint and EndPaint with nothing in between simply
validates the previously invalid rectangle. But don't do this:

case WM_PAINT :
     return 0 ;   // WRONG !!!

Windows places a WM_PAINT message in the message queue because part of the
client area is invalid. Unless you call BeginPaint and EndPaint (or
ValidateRect), Windows will not validate that area. Instead, Windows will
send you another WM_PAINT message. And another, and another, and another...


The Paint Information Structure

Earlier I mentioned a "paint information structure" that Windows maintains
for each window. That's what PAINTSTRUCT is. The structure is defined in
WINDOWS.H as follows:

typedef struct tagPAINTSTRUCT
  {
    HDC       hdc ;
    BOOL      fErase ;
    RECT      rcPaint ;
    BOOL      fRestore ;
    BOOL      fIncUpdate ;
    BYTE      rgbReserved[16] ;
  } PAINTSTRUCT ;

Windows fills in the fields of this structure when your program calls
BeginPaint. Your program may use only the first three fields. The others are
used internally by Windows.

The hdc field is the handle to the device context. In a redundancy typical
of Windows, the value returned from BeginPaint is also this device context
handle.

In most cases, fErase will be flagged TRUE (nonzero), meaning that Windows
has erased the background of the invalid rectangle. Windows erases the
background using the brush specified in the hbrBackground field of the
WNDCLASS structure that you use when registering the window class during
WinMain initialization. Many Windows programs use a white brush:

wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;

However, if your program invalidates a rectangle of the client area by
calling the Windows function InvalidateRect, one of the parameters to this
function specifies whether  you want the background erased. If this
parameter is FALSE (or 0), then Windows will not erase the background, and
the fErase field will also be FALSE.

The rcPaint field of the PAINTSTRUCT structure is a structure of type RECT.
As you learned in Chapter 1, the RECT structure defines a rectangle. The
four fields are left, top, right, and bottom. The rcPaint field in the
PAINTSTRUCT structure defines the boundaries of the invalid rectangle, as
shown in Figure 2-1. The values are in units of pixels relative to the upper
left corner of the client area. The invalid rectangle is the area that you
should repaint. Although a Windows program can simply repaint the entire
client area of the window whenever it receives a WM_PAINT message,
repainting only the area of the window defined by that rectangle saves time.

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

The rcPaint rectangle in PAINTSTRUCT is not only the invalid rectangle; it
is also a  "clipping" rectangle. This means that Windows restricts painting
to within the clipping rectangle. When you use the device context handle
from the PAINTSTRUCT structure, Windows will not paint outside the rcPaint
rectangle.

To paint outside this rcPaint rectangle while processing WM_PAINT messages,
you can make this call:

InvalidateRect (hWnd, NULL, TRUE) ;

before calling BeginPaint. This invalidates the entire client area and
erases the background. A FALSE value in the last parameter will not erase
the background, however. Whatever was there will stay.

In the HELLOWIN program in Chapter 1, we didn't care about invalid
rectangles or clipping rectangles when processing the WM_PAINT message. If
the area where the text was displayed happened to be within the invalid
rectangle, then DrawText restored it. If not, then at some point during
processing of the DrawText call, Windows determined it didn't have to write
anything to the display. But this determination takes time. A programmer
concerned about performance and speed will want to use the invalid-rectangle
dimensions during processing of WM_PAINT to avoid unnecessary GDI calls.


Getting a Device Context Handle: Method Two

You can also obtain a handle to a device context if you want to paint the
client area when processing messages other then WM_PAINT or if you need the
device context handle for other purposes, such as obtaining information
about the device context. Call GetDC to obtain the handle to the device
context, and ReleaseDC after you're done with it:

hdc = GetDC (hwnd) ;
[use GDI functions]
ReleaseDC (hwnd, hdc) ;

Like BeginPaint and EndPaint, the GetDC and ReleaseDC functions should be
called in pairs. When you call GetDC while processing a message, you should
call ReleaseDC before you exit the window procedure. Do not call GetDC in
response to one message and ReleaseDC in response to another.

Unlike the device context handle obtained from the PAINTSTRUCT structure,
the device context handle returned from GetDC has a clipping rectangle equal
to the entire client area. You can paint on any part of the client area, not
merely on the invalid rectangle (if indeed there is an invalid rectangle).
Unlike EndPaint, ReleaseDC does not validate any invalid rectangles.


TextOut: The Details

When you obtain the handle to the device context, Windows fills the device
context structure with default values. As you'll see in later chapters, you
can change these defaults with GDI functions. The GDI function we're
interested in right now is TextOut:

TextOut (hdc, x, y, lpsString, nLength) ;

Let's examine this function in more detail.

The first parameter is the handle to the device context--either the hdc
value returned from GetDC or the hdc value returned from BeginPaint during
processing of a WM_PAINT message.

The attributes of the device context control the characteristics of this
displayed text. For instance, one attribute of the device context specifies
the text color. The default color is black. The default device context also
defines a background color of white. When a program writes text to the
display, Windows uses this background color to fill in the space surrounding
the characters.

This text background color is not the same background you set when defining
the window class. The background in the window class is a brush--which is a
pattern that may or may not be a pure color--that Windows uses to erase the
client area. It is not part of the device context structure. When defining
the window class structure, most Windows applications use WHITE_BRUSH so
that the background color in the default device context is the same color as
the brush Windows uses to erase the background of the client area.

The lpsString parameter is a long pointer to a character string, and nLength
is the length of the string. The string should not contain any ASCII control
characters such as carriage returns, linefeeds, tabs, or backspaces. Windows
displays these control characters as solid blocks. TextOut does not
recognize a 0 as denoting the end of the string and requires the nLength
parameter for the length.

The x and y values in TextOut define the starting point of the character
string within the client area. The x value is the horizontal position; the y
value is the vertical position. The upper left corner of the first character
in the string is positioned at x and y. In the default device context, the
origin (the point where x and y both equal 0) is the upper left corner of
the client area. If you use 0 values for x and y in TextOut, the character
string starts flush against the upper left corner of the client area.

GDI coordinates are "logical coordinates." Windows has a variety of "mapping
modes" that govern how the logical coordinates specified in GDI functions
are translated to the physical pixel coordinates of the display. The mapping
mode is defined in the device context. The default mapping mode is called
MM_TEXT (using the WINDOWS.H identifier). Under the MM_TEXT mapping mode,
logical units are the same as physical units, which are pixels. Values of x
increase as you move to the right in the client area and values of y
increase as you move down in the client area. (See Figure 2-2 on the
following page.) The MM_TEXT coordinate system is identical to the
coordinate system that Windows uses to define the invalid rectangle in the
PAINTSTRUCT structure. Very convenient. (This is not the case with other
mapping modes, however.)

The device context also defines a clipping region. As you've seen, the
default clipping region is the entire client area for a device context
handle obtained from GetDC and the invalid rectangle for the device context
handle obtained from BeginPaint. Windows will not display any part of the
character string that lies outside the clipping rectangle. If a character is
partly within the clipping rectangle, Windows displays only the portion of
the character inside the rectangle. Writing outside the client area of your
window isn't easy to do, so don't worry about doing it inadvertently.

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


The System Font

The device context also defines the font that Windows uses when writing text
to the client area. The default is a font called the "system font" or (using
the WINDOWS.H identifier) SYSTEM_FONT. The system font is the font that
Windows uses for text in caption bars, menus, and dialog boxes.

Under Windows 3, the system font is a variable-width font, which means that
different characters have different widths. A "W" is wider than an "i." In
earlier versions of Windows, the system font was a fixed-pitch font in which
all the characters had the same width.

The system font is a "raster font," which means that the characters are
defined as blocks of pixels. The floppy disks for the Windows installation
include several system fonts in various sizes for use with different video
display adapters.

When manufacturers of a new video board develop a new Windows display
driver, they are also responsible for developing a new system font
appropriate for the resolution of the display. Alternatively, the
manufacturer might specify that one of the system font files supplied with
the retail version of Windows be used. The system font must be designed so
that at least 25 lines of 80-character text can fit on the display. That is
the only guarantee you have about the relationship between screen size and
font size in Windows.


The Size of a Character

To display multiple lines of text using the TextOut function, you need to
determine the dimensions of font characters. You can space successive lines
of text based on the height of a character, and you can space columns of
text across the client area based on the width of a character.

You can obtain character dimensions with the GetTextMetrics call.
GetTextMetrics requires a handle to the device context because it returns
information about the font currently selected in the device context. Windows
copies the various values of text metrics into a structure of type
TEXTMETRIC. The values are in units that depend on the mapping mode selected
in the device context. In the default device context, this mapping mode is
MM_TEXT, so the dimensions are in units of pixels.

To use the GetTextMetrics function, you first need to define a structure
variable (commonly called tm):

TEXTMETRIC tm ;

Next, get a handle to the device context and call GetTextMetrics:

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

After you examine the values in the text metric structure (and probably save
a few of them for future use), you release the device context:

ReleaseDC (hwnd, hdc) ;


Text Metrics: The Details

The TEXTMETRIC structure provides a wealth of information about the current
font selected in the device context. However, the vertical size of a font is
defined by only five values, as shown in Figure 2-3 on the following page.

These are fairly self-explanatory. The tmInternalLeading value is the amount
of space allowed for an accent mark above a character. If the value is set
to 0, accented capital letters are made a little shorter so that the accent
fits within the ascent of the character. The tmExternalLeading value is the
amount of space that the designer of the font is suggesting be added between
character rows. You can accept or reject the font designer's suggestion for
including external leading when spacing lines of text.

The TEXTMETRIC structure has two fields that describe character width:
tmAveCharWidth (a weighted average width of lowercase characters) and
tmMaxCharWidth (the width of the widest character in the font). For a
fixed-pitch font, these two values are the same.

The sample programs in this chapter will require another character
width--the average width of uppercase letters. This can be calculated as
150% of tmAveCharWidth.

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

It's important to realize that the dimensions of the system font are
dependent on the resolution of the video display on which Windows runs.
Windows provides a device- independent graphics interface, but you have to
help. Don't write your Windows program so that it guesses at character
dimensions. Don't hard code any values. Use the GetTextMetrics function to
obtain this information.


Formatting Text

Because the dimensions of the system font do not change during a Windows
session, you need to call GetTextMetrics only once when your program
executes. A good place to make this call is while processing the WM_CREATE
message in the window procedure. The WM_CREATE message is the first message
the window procedure receives. Windows calls your window procedure with a
WM_CREATE message when you call CreateWindow in WinMain.

Suppose you're writing a Windows program that displays several lines of text
running down the client area. You'll want to obtain values for the character
width and height. Within the window procedure you can define two variables
to save the average character width (cxChar) and the total height (cyChar):

static short cxChar, cyChar ;

The prefix c added to the variable names stands for "count," and in
combination with x or y refers to a width or a height. These variables are
defined as static because they must be valid when the window procedure
processes other messages (such as WM_PAINT). If the variables are defined
outside any functions, they need not be defined as static.

Here's the WM_CREATE code:

case WM_CREATE :
     hdc = GetDC  (hwnd) ;

     GetTextMetrics (hdc, &tm) ;
     cxChar = tm.tmAveCharWidth ;
     cyChar = tm.tmHeight + tm.tmExternalLeading ;

     ReleaseDC (hwnd, hdc) ;
     return 0 ;

If you do not want to include external leading to space lines of text, you
can use:

cyChar = tm.tmHeight ;

How you use this character size to calculate display coordinates is up to
you. A simple method is to leave a cyChar margin at the top of the client
area and a cxChar margin at the left. To display several lines of
left-justified text, use the following x-coordinate values when calling the
TextOut function:

cxChar

The y-coordinate values in TextOut are:

cyChar * (1 + i)

where i is the line number starting at 0.

You'll often find it necessary to display formatted numbers as well as
simple character strings. If you were programming in MS-DOS using standard C
library functions, you would probably use printf for this formatting. You
cannot use printf in Windows, because printf writes to the standard output
device, and that concept makes no sense under Windows.

Instead, you can use sprintf. The sprintf function works just like printf
except that it puts the formatted string into a character array. You can
then use TextOut to write the string to the display. Very conveniently, the
value returned from sprintf is the length of  the string--you can pass this
value to TextOut as the nLength parameter. This code shows a typical sprintf
and TextOut combination:

short nLength ;
char  szBuffer [40] ;
[other program lines]
nLength = sprintf (szBuffer, "The sum of %d and %d is %d",
                    nA, nB, nA + nB) ;
TextOut (hdc, x, y, szBuffer, nLength) ;

For something as simple as this you could dispense with the nLength
definition and combine the two statements into one:

TextOut (hdc, x, y, szBuffer,
     sprintf (szBuffer, "The sum of %d and %d is %d",
                    nA, nB, nA + nB)) ;

It's not pretty, but it works.

If you don't need to display floating-point numbers, you can use wsprintf
rather than sprintf. The wsprintf function has the same syntax as sprintf,
but it's included in Windows, so using it won't increase the size of your
.EXE file.


Putting It All Together

Now we seem to have everything we need to write a simple program that
displays multiple lines of text on the screen. We know how to get a handle
to a device context, how to use the TextOut function, and how to space text
based on the size of a single character. The only thing left to do is to
display something interesting.

The information available in the Windows GetSystemMetrics call looks
interesting enough. This function returns information about the size of
various graphical items in Windows, such as icons, cursors, caption bars,
and scroll bars. These sizes vary with the display adapter and driver.
GetSystemMetrics requires a single parameter called an "index." This index
is 1 of 37 integer identifiers defined in WINDOWS.H. GetSystemMetrics
returns an integer, usually the size of the item specified in the parameter.

Let's write a program that displays all the information available from the
GetSystemMetrics call in a simple one-line-per-item format. Working with
this information is easier if we create a header file that defines an array
of structures containing both the WINDOWS.H identifiers for the
GetSystemMetrics index and the text we want to display for each value
returned from the call. This header file is called SYSMETS.H and is shown in
Figure 2-4.

 SYSMETS.H

/*-----------------------------------------------
   SYSMETS.H -- System metrics display structure
  -----------------------------------------------*/

#define NUMLINES (sizeof sysmetrics / sizeof sysmetrics [0])

struct
     {
     int  nIndex ;
     char *szLabel ;
     char *szDesc ;
     }
     sysmetrics [] =
     {
     SM_CXSCREEN,      "SM_CXSCREEN",      "Screen width in pixels",
     SM_CYSCREEN,      "SM_CYSCREEN",      "Screen height in pixels",
     SM_CXVSCROLL,     "SM_CXVSCROLL",     "Vertical scroll arrow width",
     SM_CYHSCROLL,     "SM_CYHSCROLL",     "Horizontal scroll arrow height",
     SM_CYCAPTION,     "SM_CYCAPTION",     "Caption bar height",
     SM_CXBORDER,      "SM_CXBORDER",      "Border width",
     SM_CYBORDER,      "SM_CYBORDER",      "Border height",
     SM_CXDLGFRAME,    "SM_CXDLGFRAME",    "Dialog window frame width",
     SM_CYDLGFRAME,    "SM_CYDLGFRAME",    "Dialog window frame height",
     SM_CYVTHUMB,      "SM_CYVTHUMB",      "Vertical scroll thumb height",
     SM_CXHTHUMB,      "SM_CXHTHUMB",      "Horizontal scroll thumb width",
     SM_CXICON,        "SM_CXICON",        "Icon width",
     SM_CYICON,        "SM_CYICON",        "Icon height",
     SM_CXCURSOR,      "SM_CXCURSOR",      "Cursor width",
     SM_CYCURSOR,      "SM_CYCURSOR",      "Cursor height",
     SM_CYMENU,        "SM_CYMENU",        "Menu bar height",
     SM_CXFULLSCREEN,  "SM_CXFULLSCREEN",  "Full-screen client window
width",
     SM_CYFULLSCREEN,  "SM_CYFULLSCREEN",  "Full-screen client window
height",
     SM_CYKANJIWINDOW, "SM_CYKANJIWINDOW", "Kanji window height",
     SM_MOUSEPRESENT,  "SM_MOUSEPRESENT",  "Mouse present flag",
     SM_CYVSCROLL,     "SM_CYVSCROLL",     "Vertical scroll arrow height",
     SM_CXHSCROLL,     "SM_CXHSCROLL",     "Horizontal scroll arrow width",
     SM_DEBUG,         "SM_DEBUG",         "Debug version flag",
     SM_SWAPBUTTON,    "SM_SWAPBUTTON",    "Mouse buttons swapped flag",
     SM_RESERVED1,     "SM_RESERVED1",     "Reserved",
     SM_RESERVED2,     "SM_RESERVED2",     "Reserved",
     SM_RESERVED3,     "SM_RESERVED3",     "Reserved",
     SM_RESERVED4,     "SM_RESERVED4",     "Reserved",
     SM_CXMIN,         "SM_CXMIN",         "Minimum window width",
     SM_CYMIN,         "SM_CYMIN",         "Minimum window height",
     SM_CXSIZE,        "SM_CXSIZE",        "Minimize/Maximize icon width",
     SM_CYSIZE,        "SM_CYSIZE",        "Minimize/Maximize icon height",



     SM_CXFRAME,       "SM_CXFRAME",       "Window frame width",
     SM_CYFRAME,       "SM_CYFRAME",       "Window frame height",
     SM_CXMINTRACK,    "SM_CXMINTRACK",    "Minimum tracking width of
window",
     SM_CYMINTRACK,    "SM_CYMINTRACK",    "Minimum tracking height of
window",
     SM_CMETRICS,      "SM_CMETRICS",      "Number of system metrics"
     } ;

The program that displays this information is called SYSMETS1. The files
required to create SYSMETS1.EXE (make file, C source code, and module
definition file) are shown in Figure 2-5. Most of the code should look
familiar by now. With the exception of the program name, the make file,
resource script, and DEF file are identical to those for HELLOWIN. In
SYSMETS1.C, WinMain is virtually identical to HELLOWIN.

 SYSMETS1.MAK

#------------------------
# SYSMETS1.MAK make file
#------------------------

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

sysmets1.obj : sysmets1.c sysmets.h
     cl -c -Gsw -Ow -W2 -Zp sysmets1.c

 SYSMETS1.C

/*----------------------------------------------------
   SYSMETS1.C -- System Metrics Display Program No. 1
                 (c) Charles Petzold, 1990
  ----------------------------------------------------*/

#include 
#include "sysmets.h"

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

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)




     {
     static char szAppName[] = "SysMets1" ;
     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, "Get System Metrics No. 1",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     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 short cxChar, cxCaps, cyChar ;
     char         szBuffer[10] ;
     HDC          hdc ;
     short        i ;
     PAINTSTRUCT  ps ;
     TEXTMETRIC   tm ;
     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;

               ReleaseDC (hwnd, hdc) ;
               return 0 ;

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

               for (i = 0 ; i < NUMLINES ; i++)
                    {
                    TextOut (hdc, cxChar, cyChar * (1 + i),
                             sysmetrics[i].szLabel,
                             lstrlen (sysmetrics[i].szLabel)) ;

                    TextOut (hdc, cxChar + 18 * cxCaps, cyChar * (1 + i),
                             sysmetrics[i].szDesc,
                             lstrlen (sysmetrics[i].szDesc)) ;

                    SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

                    TextOut (hdc, cxChar + 18 * cxCaps + 40 * cxChar,
                             cyChar * (1 + i), szBuffer,
                             wsprintf (szBuffer, "%5d",
                                  GetSystemMetrics (sysmetrics[i].nIndex)))
;

                    SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }

     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 SYSMETS1.DEF

;-------------------------------------
; SYSMETS1.DEF module definition file
;-------------------------------------

NAME           SYSMETS1

DESCRIPTION    'System Metrics Display 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

Figure 2-6 shows SYSMETS1 running on a VGA. As you can see from the
program's window, the screen width is 640 pixels and the screen height is
480 pixels. These two values, as well as many of the other values shown by
the program, will be different for different types of video displays.

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


The SYSMETS1.C Window Procedure

The WndProc window procedure in the SYSMETS1.C program processes three
messages: WM_CREATE, WM_PAINT, and WM_DESTROY. The WM_DESTROY message is
processed in the same way as the HELLOWIN program in Chapter 1.

The WM_CREATE message is the first message the window procedure receives. It
is generated by Windows when the CreateWindow function creates the window.
During the WM_CREATE message, SYSMETS1 obtains a device context for the
window by calling GetDC, and gets the text metrics for the default system
font by calling GetTextMetrics. SYSMETS1 saves the average character width
in cxChar and the total height of the characters including external leading
in cyChar.

SYSMETS1 also saves an average width of uppercase letters in the static
variable cxCaps. For a fixed-pitch font, cxCaps would equal cxChar. For a
variable-width font, cxCaps is about 150% of cxChar. The low bit of the
tmPitchAndFamily field of the TEXTMETRIC structure is 1 for a variable-width
font and 0 for a fixed-pitch font. SYSMETS1 uses this bit value to calculate
cxCaps from cxChar:

cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;

SYSMETS1 does all window painting during the WM_PAINT message. As normal,
the window procedure first obtains a handle to the device context by calling
BeginPaint. A for statement loops through all the lines of the sysmetrics
structure defined in SYSMETS.H. The three columns of text are displayed with
three TextOut functions. In each case, the third parameter to TextOut is set
to:

cyChar * (1 + i)

This parameter indicates the pixel position of the top of the character
string relative to the top of the client area. Thus, the program leaves a
margin at the top equal to cyChar. The first line of text (when i equals 0)
begins cyChar pixels below the top of the client area.

The first TextOut statement displays the uppercase identifiers in the first
of the three columns. The second parameter to TextOut is cxChar. This leaves
a one-character margin between the left edge of the client area and the text
string. The text is obtained from the szLabel field of the sysmetrics
structure. I use the Windows function lstrlen (which is similar to strlen)
to obtain the length of the string, which is required as the last parameter
to TextOut.

The second TextOut statement displays the description of the system metrics
value. These descriptions are stored in the szDesc field of the sysmetrics
structure. In this case, the second parameter to TextOut is set to:

cxChar + 18 * cxCaps

The longest uppercase identifier displayed in the first column is 16
characters, so the second column must begin at least 16 x cxCaps to the
right of the beginning of the first column of text.

The third TextOut statement displays the numeric values obtained from the
GetSystemMetrics function. The variable-width font makes formatting a column
of right-justified numbers a little tricky. All the digits from 0 through 9
have the same width, but this width is greater than the width of a space.
Numbers can be one or more digits wide, so different numbers can begin at
different horizontal positions.

Wouldn't it be easier if we could display a column of right-justified
numbers by specifying the pixel position where the number ends rather than
where it begins? This is what the SetTextAlign function lets us do. After
SYSMETS1 calls

SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

then the coordinates passed to subsequent TextOut functions will specify the
top-right corner of the text string rather than the top-left corner.

The TextOut function to display the column of numbers has a second parameter
set to:

cxChar + 18 * cxCaps + 40 * cxChar

The 40 x cxChar value accommodates the width of the second column and the
width of the third column. Following the TextOut function, another call to
SetTextAlign sets things back to normal for the next time through the loop.


Not Enough Room!

One little nasty problem exists with the SYSMETS1 program: Unless you have a
big-screen high-resolution video adapter, you can't see the last few lines
of the system metrics list. If you make the window narrower, you can't see
even the values.

SYSMETS1 doesn't know how large its client area is. It begins the text at
the top of the window and relies on Windows to clip everything that drifts
beyond the edges of the client area. Our first job is to determine how much
of the program's output can actually fit within the client area.


The Size of the Client Area

If you experiment with existing Windows applications, you'll find that
window sizes can vary widely. At the most (assuming the window does not have
a menu or scroll bars), the window can be maximized, and the client area
will occupy the entire screen except for the caption bar. The minimum size
of the window can be quite small, sometimes almost nonexistent, eliminating
the client area.

One common method for determining the size of a window's client area is to
process the WM_SIZE message within your window procedure. Windows sends a
WM_SIZE message to a window procedure whenever the size of the window
changes. The lParam variable passed to the window procedure contains the
width of the client area in the low word and the height in the high word.
The code to process this message looks like this:

static short cxClient, cyClient ;
[other program lines]
case WM_SIZE :
     cxClient = LOWORD (lParam) ;
     cyClient = HIWORD (lParam) ;
     break ;

The LOWORD and HIWORD macros are defined in WINDOWS.H. Like cxChar and
cyChar, the cxClient and cyClient variables are defined as static inside the
window procedure because they are used later when processing other messages.

The WM_SIZE message will eventually be followed by a WM_PAINT message. Why?
Because when we define the window class, we specify that the class style is:

CS_HREDRAW | CS_VREDRAW

This class style tells Windows to force a repaint if either the horizontal
or vertical size changes.

You can calculate the number of full lines of text displayable within the
client area with the formula:

cyClient / cyChar

This may be 0 if the height of the client area is too small to display a
full character. Similarly, the approximate number of lowercase characters
you can display horizontally within the client area is equal to:

cxClient / cxChar

If you determine cxChar and cyChar during a WM_CREATE message, don't worry
about dividing by 0 in these calculations. Your window procedure receives a
WM_CREATE message when WinMain calls CreateWindow. The first WM_SIZE message
comes a little later when WinMain calls ShowWindow, at which point cxChar
and cyChar have already been assigned positive values.

Knowing the size of the window's client area is the first step in providing
a way for the user to move the text within the client area if the client
area is not large enough to hold everything. If you're familiar with other
Windows applications that have similar requirements, you probably know what
we need: This is a job for scroll bars.



SCROLL BARS

Scroll bars are one of the best features of a graphics and mouse interface.
They are easy to use and provide good visual feedback. You can use scroll
bars whenever you need to display anything--text, graphics, a spreadsheet,
database records, pictures--that requires more space than the available
client area of the window.

Scroll bars are positioned either vertically (for up and down movement) or
horizontally (for left and right movement). You can click with the mouse on
the arrows at each end of a scroll bar or on the area between the arrows. A
"scroll box," or "thumb," travels the length of the scroll bar to indicate
the approximate location of the material shown on the display in relation to
the entire document. You can also drag the thumb with the mouse to move to a
particular location. Figure 2-7 shows the recommended use of a vertical
scroll bar for text.

Programmers sometimes have problems with scrolling terminology because their
perspective is different from the user's: A user who scrolls down wants to
bring a lower part of the document into view. However, the program actually
moves the document up in relation to the display window. The Windows
documentation and the WINDOWS.H identifiers are based on the user's
perspective: Scrolling up means moving toward the beginning of the document;
scrolling down means moving toward the end.

It is very easy to include a horizontal or vertical scroll bar in your
application window. All you need to do is include the identifier WS_VSCROLL
(vertical scroll) or WS_HSCROLL (horizontal scroll) or both to the window
style in the CreateWindow statement. These scroll bars are always placed
against the right side or bottom of the window and extend for the full
length or width of the client area. The client area does not include the
space occupied by the scroll bar. The width of a vertical window scroll bar
and the height of a horizontal window scroll bar are constant for a
particular display driver. If you need these values, you can obtain them (as
you may have observed) from the GetSystemMetrics call.

Windows takes care of all mouse logic for the scroll bars. However, window
scroll bars do not have an automatic keyboard interface. If you want the
cursor keys to duplicate some of the window scroll bars' functions, you must
explicitly provide logic for that (as we'll do in the next chapter).

Scroll Bar Range and Position

Scroll bars have a "range" and a current "position." The range is defined by
minimum and maximum integer values. When the thumb is at the top (or left)
of the scroll bar, the position of the thumb is the minimum value of the
range. At the bottom (or right) of the scroll bar, the thumb position is the
maximum value of the range.

The position of the thumb is always a discrete integral value. For instance,
a scroll bar with a range from 0 through 4 has five thumb positions, as
shown in Figure 2-8. By default, the range of a scroll bar is 0 (top or
left) through 100 (bottom or right), but it's easy to change the range to
something that is more convenient for the program:

SetScrollRange (hwnd, nBar, nMin, nMax, bRedraw) ;

The nBar parameter is either SB_VERT or SB_HORZ, nMin and nMax are the
minimum and maximum positions of the range, and bRedraw is set to TRUE if
you want Windows to redraw the scroll bar based on the new range.

You can use SetScrollPos to set a new thumb position within the range:

SetScrollPos (hwnd, nBar, nPos, nRedraw) ;

The nPos parameter is the new position and must be within the range of nMin
through nMax. Windows provides similar functions (GetScrollRange and
GetScrollPos) to obtain the current range and position of a scroll bar.

When you use scroll bars within your program, you share responsibility with
Windows for maintaining the scroll bars and updating the position of the
scroll bar thumb. These are Windows' responsibilities for scroll bars:

  þ   Handle all scroll bar mouse logic

  þ   Provide a "reverse video" flash when the user clicks on the scroll bar

  þ   Display a "ghost" box when the user drags the thumb within the scroll
      bar

  þ   Send scroll bar messages to the window procedure for the window
      containing the scroll bar

These are your program's responsibilities:

  þ   Initialize the range of the scroll bar

  þ   Process the scroll bar messages

  þ   Update the position of the scroll bar thumb

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


Scroll Bar Messages

Windows sends the window procedure WM_VSCROLL and WM_HSCROLL messages when
the scroll bar is clicked with the mouse or the thumb is dragged. Each mouse
action on the scroll bar generates at least two messages, one when the mouse
button is pressed and another when it is released.

The value of wParam that accompanies the WM_VSCROLL and WM_HSCROLL messages
describes what the mouse is doing to the scroll bar. These values of wParam
have WINDOWS.H identifiers that begin with SB, which stands for "scroll
bar." Although some of these identifiers use the words "UP" and "DOWN," they
apply to horizontal as well as vertical scroll bars, as you see in Figure
2-9. Your window procedure can receive multiple SB_LINEUP, SB_PAGEUP,
SB_LINEDOWN, or SB_PAGEDOWN messages if the mouse button is held down while
positioned on the scroll bar. The SB_ENDSCROLL message signals that the
mouse button has been released. You can generally ignore SB_ENDSCROLL
messages.

When wParam is SB_THUMBTRACK or SB_THUMBPOSITION, the low word of lParam is
the current position of the dragged scroll bar. This position is within the
minimum and maximum values of the scroll bar range. For other values of
wParam, the low word of lParam should be ignored. You can also ignore the
high word of lParam.

The Windows documentation indicates that the wParam value can also be SB_TOP
and SB_BOTTOM, indicating that the scroll bar has been moved to its minimum
or maximum position. However, you will never receive these values for a
scroll bar created as part of your application window.

Handling the SB_THUMBTRACK and SB_THUMBPOSITION messages is problematic. If
you set a large scroll bar range and the user quickly drags the thumb inside
the scroll bar, Windows sends your window function a barrage of
SB_THUMBTRACK messages.

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

Your program may have problems keeping up with these messages. For this
reason, most Windows applications ignore these messages and take action only
on receipt of SB_THUMBPOSITION, which means that the thumb is again at rest.

However, if you can update your display quickly, you may want to include
SB_THUMBTRACK processing in your program. But be aware that users who
discover that your program scrolls as they move the scroll bar thumb will
undoubtedly try to move it as quickly as possible to see if your program can
keep up. They will get an inordinate amount of satisfaction if it cannot.


Scrolling SYSMETS

Enough explanation. It's time to put this stuff into practice. But let's
start simply. We'll begin with vertical scrolling because that's what we
desperately need. The horizontal scrolling can wait. SYSMETS2 is shown in
Figure 2-10.

The new CreateWindow call adds a vertical scroll bar to the window; the
scroll bar has this window style:

WS_OVERLAPPEDWINDOW | WS_VSCROLL

 SYSMETS2.MAK

#------------------------
# SYSMETS2.MAK make file
#------------------------

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

sysmets2.obj : sysmets2.c sysmets.h
     cl -c -Gsw -Ow -W2 -Zp sysmets2.c

 SYSMETS2.C

/*----------------------------------------------------
   SYSMETS2.C -- System Metrics Display Program No. 2
                 (c) Charles Petzold, 1990
  ----------------------------------------------------*/

#include 
#include "sysmets.h"

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



int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "SysMets2" ;
     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, "Get System Metrics No. 2",
                          WS_OVERLAPPEDWINDOW | WS_VSCROLL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     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 short cxChar, cxCaps, cyChar, cxClient, cyClient, nVscrollPos ;
     char         szBuffer[10] ;
     HDC          hdc ;
     short        i, y ;
     PAINTSTRUCT  ps ;
     TEXTMETRIC   tm ;
     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;

               ReleaseDC (hwnd, hdc) ;

               SetScrollRange (hwnd, SB_VERT, 0, NUMLINES, FALSE) ;
               SetScrollPos   (hwnd, SB_VERT, nVscrollPos, TRUE) ;
               return 0 ;

          case WM_SIZE :
               cyClient = HIWORD (lParam) ;
               cxClient = LOWORD (lParam) ;
               return 0 ;

          case WM_VSCROLL :
               switch (wParam)
                    {
                    case SB_LINEUP :
                         nVscrollPos -= 1 ;
                         break ;

                    case SB_LINEDOWN :
                         nVscrollPos += 1 ;
                         break ;

                    case SB_PAGEUP :
                         nVscrollPos -= cyClient / cyChar ;
                         break ;

                    case SB_PAGEDOWN :
                         nVscrollPos += cyClient / cyChar ;
                         break ;

                    case SB_THUMBPOSITION :
                         nVscrollPos = LOWORD (lParam) ;
                         break ;

                    default :
                         break ;
                    }
               nVscrollPos = max (0, min (nVscrollPos, NUMLINES)) ;

               if (nVscrollPos != GetScrollPos (hwnd, SB_VERT))
                    {
                    SetScrollPos (hwnd, SB_VERT, nVscrollPos, TRUE) ;
                    InvalidateRect (hwnd, NULL, TRUE) ;
                    }
               return 0 ;

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

               for (i = 0 ; i < NUMLINES ; i++)
                    {
                    y = cyChar * (1 - nVscrollPos + i) ;

                    TextOut (hdc, cxChar, y,
                             sysmetrics[i].szLabel,
                             lstrlen (sysmetrics[i].szLabel)) ;

                    TextOut (hdc, cxChar + 18 * cxCaps, y,
                             sysmetrics[i].szDesc,
                             lstrlen (sysmetrics[i].szDesc)) ;

                    SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

                    TextOut (hdc, cxChar + 18 * cxCaps + 40 * cxChar, y,
                             szBuffer,
                             wsprintf (szBuffer, "%5d",
                                  GetSystemMetrics (sysmetrics[i].nIndex)))
;

                    SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }

     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 SYSMET2.DEF

;-------------------------------------
; SYSMETS2.DEF module definition file
;-------------------------------------

NAME           SYSMETS2

DESCRIPTION    'System Metrics Display 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

The WndProc window procedure has two additional lines to set the range and
position of the vertical scroll bar during processing of the WM_CREATE
message:

SetScrollRange (hwnd, SB_VERT, 0, NUMLINES, FALSE) ;
SetScrollPos   (hwnd, SB_VERT, nVscrollPos, TRUE) ;

The sysmetrics structure has NUMLINES lines of text, so the scroll bar range
is set from 0 through NUMLINES. Each position of the scroll bar corresponds
to a line of text displayed at the top of the client area. If the scroll bar
thumb is at position 0, a blank line is left at the top of the screen for a
margin. As you increase the position of the scroll bar by scrolling down,
the text should move up. When the scroll bar position is at the bottom, the
last line of the structure is at the top.

To help with processing of the WM_VSCROLL messages, a static variable called
nVscrollPos is defined within the WndProc window procedure. This variable is
the current position of the scroll bar thumb. For SB_LINEUP and SB_LINEDOWN,
all we need to do is adjust the scroll position by 1. For SB_PAGEUP and
SB_PAGEDOWN, we want to move the text by the contents of one screen, or
cyClient divided by cyChar. For SB_THUMBPOSITION, the new thumb position is
the low word of lParam. SB_ENDSCROLL and SB_THUMBTRACK messages are ignored.

The nVscrollPos is then adjusted using the min and max macros (defined in
WINDOWS.H) to ensure that it is between the minimum and maximum range
values. If the scroll position has changed, then it is updated using
SetScrollPos, and the entire window is invalidated by an InvalidateRect
call.

The InvalidateRect call generates a WM_PAINT message. When the original
SYSMETS1 processed WM_PAINT messages, the y-coordinate of each line was
calculated as:

cyChar * (1 + i)

In SYSMETS2, the formula is:

cyChar * (1 - nVscrollPos + i)

The loop still displays NUMLINES lines of text, but for values of
nVscrollPos of 2 and above, the loop begins displaying lines above the
client area. Windows merely ignores these lines.

I told you we'd start simply. This is rather wasteful and inefficient code.
We'll fix it shortly, but first consider how we update the client area after
a WM_VSCROLL message.


Structuring Your Program for Painting

The window procedure in SYSMETS2 does not repaint the client area after
processing a scroll bar message. Instead, it calls InvalidateRect to
invalidate the client area. This causes Windows to place a WM_PAINT message
in the message queue.

It is best to structure your Windows programs so that you do all client-area
painting in response to a WM_PAINT message. Because your program should be
able to repaint the entire client area of the window at any time on receipt
of a WM_PAINT message, you will probably duplicate code if you also paint in
other parts of the program.

At first, you may rebel at this dictum because it is so different from
normal PC programming. I won't deny that, on occasion, painting in response
to messages other than WM_PAINT is much more convenient. (The KEYLOOK
program in the next chapter is an example of such a program.) But in many
cases it's simply unnecessary, and after you master the discipline of
accumulating all the information you need to paint in response to a WM_PAINT
message, you'll be pleased with the results. However, your program will
often determine that it must repaint a particular area of the display when
processing a message other than WM_PAINT. This is where InvalidateRect comes
in handy. You can use it to invalidate specific rectangles of the client
area or the entire client area.

Simply marking areas of the window as invalid to generate WM_PAINT messages
may not be entirely satisfactory in some applications. After you make an
InvalidateRect call, Windows places a WM_PAINT message in the message queue,
and the window procedure eventually processes it. However, Windows treats
WM_PAINT messages as low priority. If your message queue contains only a
WM_PAINT message and another application has other messages waiting, Windows
switches to the other application when you make a GetMessage call.

If you prefer to update the invalid area immediately, you can call
UpdateWindow after you call InvalidateRect:

UpdateWindow (hwnd) ;

UpdateWindow causes the window procedure to be called immediately with a
WM_PAINT message if any part of the client area is invalid. (It will not
call the window procedure if the entire client area is valid.) This WM_PAINT
message bypasses the message  queue. The window procedure is called directly
from Windows. When the window procedure has finished repainting, it exits
and Windows returns control to the program at the statement following the
UpdateWindow call.

You'll note that UpdateWindow is the same function used in WinMain to
generate the first WM_PAINT message. When a window is first created, the
entire client area is invalid. UpdateWindow directs the window procedure to
paint it.


Building a Better Scroll

Because SYSMETS2 is too inefficient a model to be imitated in other
programs, let's clean it up. SYSMETS3--our final version of the SYSMETS
program in this chapter--is shown in Figure 2-11. This version adds a
horizontal scroll bar for left and right scrolling and repaints the client
area more efficiently.

 SYSMETS3.MAK

#------------------------
# SYSMETS3.MAK make file
#------------------------

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

sysmets3.obj : sysmets3.c sysmets.h
     cl -c -Gsw -Ow -W2 -Zp sysmets3.c

 SYSMETS3.C

/*----------------------------------------------------
   SYSMETS3.C -- System Metrics Display Program No. 3
                 (c) Charles Petzold, 1990
  ----------------------------------------------------*/

#include 
#include "sysmets.h"

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

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)



     {
     static char szAppName[] = "SysMets3" ;
     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, "Get System Metrics No. 3",
                          WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     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 short cxChar, cxCaps, cyChar, cxClient, cyClient, nMaxWidth,
                  nVscrollPos, nVscrollMax, nHscrollPos, nHscrollMax ;
     char         szBuffer[10] ;
     HDC          hdc ;
     short        i, x, y, nPaintBeg, nPaintEnd, nVscrollInc, nHscrollInc ;
     PAINTSTRUCT  ps ;
     TEXTMETRIC   tm ;

     switch (message)

          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;

               ReleaseDC (hwnd, hdc) ;

               nMaxWidth = 40 * cxChar + 18 * cxCaps ;
               return 0 ;

          case WM_SIZE :
               cyClient = HIWORD (lParam) ;
               cxClient = LOWORD (lParam) ;

               nVscrollMax = max (0, NUMLINES + 2 - cyClient / cyChar) ;
               nVscrollPos = min (nVscrollPos, nVscrollMax) ;

               SetScrollRange (hwnd, SB_VERT, 0, nVscrollMax, FALSE) ;
               SetScrollPos   (hwnd, SB_VERT, nVscrollPos, TRUE) ;

               nHscrollMax = max (0, 2 + (nMaxWidth - cxClient) / cxChar) ;
               nHscrollPos = min (nHscrollPos, nHscrollMax) ;

               SetScrollRange (hwnd, SB_HORZ, 0, nHscrollMax, FALSE) ;
               SetScrollPos   (hwnd, SB_HORZ, nHscrollPos, TRUE) ;
               return 0 ;

          case WM_VSCROLL :
               switch (wParam)
                    {
                    case SB_TOP :
                         nVscrollInc = -nVscrollPos ;
                         break ;

                    case SB_BOTTOM :
                         nVscrollInc = nVscrollMax - nVscrollPos ;
                         break ;

                    case SB_LINEUP :
                         nVscrollInc = -1 ;
                         break ;

                    case SB_LINEDOWN :
                         nVscrollInc = 1 ;
                         break ;
                    case SB_PAGEUP :
                         nVscrollInc = min (-1, -cyClient / cyChar) ;
                         break ;

                    case SB_PAGEDOWN :
                         nVscrollInc = max (1, cyClient / cyChar) ;
                         break ;

                    case SB_THUMBTRACK :
                         nVscrollInc = LOWORD (lParam) - nVscrollPos ;
                         break ;

                    default :
                         nVscrollInc = 0 ;
                    }
               if (nVscrollInc = max (-nVscrollPos,
                         min (nVscrollInc, nVscrollMax - nVscrollPos)))
                    {
                    nVscrollPos += nVscrollInc ;
                    ScrollWindow (hwnd, 0, -cyChar * nVscrollInc, NULL,
NULL) ;
                    SetScrollPos (hwnd, SB_VERT, nVscrollPos, TRUE) ;
                    UpdateWindow (hwnd) ;
                    }
               return 0 ;

          case WM_HSCROLL :
               switch (wParam)
                    {
                    case SB_LINEUP :
                         nHscrollInc = -1 ;
                         break ;

                    case SB_LINEDOWN :
                         nHscrollInc = 1 ;
                         break ;

                    case SB_PAGEUP :
                         nHscrollInc = -8 ;
                         break ;

                    case SB_PAGEDOWN :
                         nHscrollInc = 8 ;
                         break ;

                    case SB_THUMBPOSITION :
                         nHscrollInc = LOWORD (lParam) - nHscrollPos ;
                         break ;

                    default :
                         nHscrollInc = 0 ;
                    }

               if (nHscrollInc = max (-nHscrollPos,
                         min (nHscrollInc, nHscrollMax - nHscrollPos)))
                    {
                    nHscrollPos += nHscrollInc ;
                    ScrollWindow (hwnd, -cxChar * nHscrollInc, 0, NULL,
NULL) ;
                    SetScrollPos (hwnd, SB_HORZ, nHscrollPos, TRUE) ;
                    }
               return 0 ;

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

               nPaintBeg = max (0, nVscrollPos + ps.rcPaint.top / cyChar -
1) ;
               nPaintEnd = min (NUMLINES,
                                nVscrollPos + ps.rcPaint.bottom / cyChar) ;

               for (i = nPaintBeg ; i < nPaintEnd ; i++)
                    {
                    x = cxChar * (1 - nHscrollPos) ;
                    y = cyChar * (1 - nVscrollPos + i) ;

                    TextOut (hdc, x, y,
                             sysmetrics[i].szLabel,
                             lstrlen (sysmetrics[i].szLabel)) ;

                    TextOut (hdc, x + 18 * cxCaps, y,
                             sysmetrics[i].szDesc,
                             lstrlen (sysmetrics[i].szDesc)) ;

                    SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

                    TextOut (hdc, x + 18 * cxCaps + 40 * cxChar, y,
                             szBuffer,
                             wsprintf (szBuffer, "%5d",
                                  GetSystemMetrics (sysmetrics[i].nIndex)))
;

                    SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }

     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 SYSMETS3.DEF

;-------------------------------------
; SYSMETS3.DEF module definition file
;-------------------------------------

NAME           SYSMETS3

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

These are the improvements in SYSMETS3 and how they are implemented in the
program:

  þ   You can no longer scroll the display so that the last line appears at
      the top of the client area. You can scroll only far enough to see the
      last line at the bottom of the client area. This requires that the
      program calculate a new scroll bar range (and possibly a new thumb
      position) when it processes a WM_SIZE message. The WM_SIZE logic
      calculates the scroll bar range based on the number of lines of text,
      the width of the text, and the size of the client area. This approach
      results in a smaller range--only that necessary to bring into view the
      text that falls outside the client area.

      This offers an interesting dividend. Suppose that the client area of
      the window is large enough to display the entire text with top and
      bottom margins. In this case, both the minimum position and maximum
      position of the scroll bar range will equal zero. What will Windows do
      with this information? It will remove the scroll bar from the window!
      It's no longer needed. Similarly, if the client area is wide enough to
      show the full 60-column width of the text, no horizontal scroll bar is
      displayed in the window.

  þ   The WM_VSCROLL and WM_HSCROLL messages are processed by first
      calculating an increment of the scroll bar position for each value of
      wParam. This value is then used to scroll the existing contents of the
      window using the Windows ScrollWindow call. This function has the
      following format:

      ScrollWindow (hwnd, xInc, yInc, lpRect, lpClipRect) ;

      The xInc and yInc values specify an amount to scroll in pixels. In
      SYSMETS3, the lpRect and lpClipRect values are set to NULL to specify
      that the entire client area should be scrolled. Windows invalidates
      the rectangle in the client area "uncovered" by the scrolling
      operation. This generates a WM_PAINT message. InvalidateRect is no
      longer needed. (Note that ScrollWindow is not a GDI procedure because
      it does not require a handle to a device context. It is one of the few
      non-GDI Windows functions that changes the appearance of the client
      area of a window.)

  þ   The WM_PAINT processing now determines which lines are within the
      invalid rectangle and rewrites only those lines. It does this by
      analyzing the top and bottom coordinates of the invalid rectangle
      stored in the PAINTSTRUCT structure. The program paints only those
      text lines within the invalid rectangle. The code is more complex, but
      it is much faster.

  þ   Because WM_PAINT was speeded up, I decided to let SYSMETS3 process
      SB_THUMBTRACK operations for WM_VSCROLL messages. Previously, the
      program would ignore SB_THUMBTRACK messages (which occur as the user
      drags the scroll bar thumb) and would act only on SB- _THUMBPOSITION
      messages, which occur when the user stops dragging the thumb. The
      WM_VSCROLL code also calls UpdateWindow to update the client area
      immediately. When you move the thumb on the vertical scroll bar,
      SYSMETS3 will continually scroll and update the client area. I'll let
      you decide whether SYSMETS3 (and Windows) is fast enough to justify
      this change.


But I Don't Like to Use the Mouse

If you don't have a mouse on your PC, you can't scroll SYSMETS3 at all.
Scroll bars created as part of your application window do not have an
automatic keyboard interface. Because Windows can be installed without a
mouse, it is highly recommended that you write programs that do not require
the mouse.

In the next chapter you'll learn how to use the keyboard and how to add a
keyboard interface to SYSMETS. You'll notice that SYSMETS3 seems to process
WM_VSCROLL messages where wParam equals SB_TOP and SB_BOTTOM. I mentioned
earlier that a window procedure doesn't receive these messages for scroll
bars, so right now this is superfluous code. When we come back to this
program in the next chapter, you'll see the reason for including this code.