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

The Button Class

We'll begin our exploration of the button window class with a program named BTNLOOK ("button look"), which is shown in Figure 9-1. BTNLOOK creates 10 child window button controls, one for each of the 10 standard styles of buttons.

Figure 9-1. The BTNLOOK program.

BTNLOOK.C

/*----------------------------------------
   BTNLOOK.C -- Button Look Program
                (c) Charles Petzold, 1998
  ----------------------------------------*/

#include <windows.h>

struct
{
     int     iStyle ;
     TCHAR * szText ;
}
button[] =
{
     BS_PUSHBUTTON,      TEXT ("PUSHBUTTON"),
     BS_DEFPUSHBUTTON,   TEXT ("DEFPUSHBUTTON"),
     BS_CHECKBOX,        TEXT ("CHECKBOX"), 
     BS_AUTOCHECKBOX,    TEXT ("AUTOCHECKBOX"),
     BS_RADIOBUTTON,     TEXT ("RADIOBUTTON"),
     BS_3STATE,          TEXT ("3STATE"),
     BS_AUTO3STATE,      TEXT ("AUTO3STATE"),
     BS_GROUPBOX,        TEXT ("GROUPBOX"),
     BS_AUTORADIOBUTTON, TEXT ("AUTORADIO"),
     BS_OWNERDRAW,       TEXT ("OWNERDRAW")
} ;

#define NUM (sizeof button / sizeof button[0])

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("BtnLook") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Button Look"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

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

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static HWND  hwndButton[NUM] ;
     static RECT  rect ;
     static TCHAR szTop[]    = TEXT ("message            wParam       lParam"),
                  szUnd[]    = TEXT ("_______            ______       ______"),
                  szFormat[] = TEXT ("%-16s%04X-%04X    %04X-%04X"),
                  szBuffer[50] ;
     static int   cxChar, cyChar ;
     HDC          hdc ;
     PAINTSTRUCT  ps ;
     int          i ;
     
     switch (message)
     {
     case WM_CREATE :
          cxChar = LOWORD (GetDialogBaseUnits ()) ;
          cyChar = HIWORD (GetDialogBaseUnits ()) ;
          
          for (i = 0 ; i < NUM ; i++)
               hwndButton[i] = CreateWindow ( TEXT("button"), 
                                   button[i].szText,
                                   WS_CHILD | WS_VISIBLE | button[i].iStyle,
                                   cxChar, cyChar * (1 + 2 * i),
                                   20 * cxChar, 7 * cyChar / 4,
                                   hwnd, (HMENU) i,
                                   ((LPCREATESTRUCT) lParam)->hInstance, NULL) ;
          return 0 ;

     case WM_SIZE :
          rect.left   = 24 * cxChar ;
          rect.top    =  2 * cyChar ;
          rect.right  = LOWORD (lParam) ;
          rect.bottom = HIWORD (lParam) ;
          return 0 ;

     case WM_PAINT :
          InvalidateRect (hwnd, &rect, TRUE) ;
          
          hdc = BeginPaint (hwnd, &ps) ;
          SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
          SetBkMode (hdc, TRANSPARENT) ;
          
          TextOut (hdc, 24 * cxChar, cyChar, szTop, lstrlen (szTop)) ;
          TextOut (hdc, 24 * cxChar, cyChar, szUnd, lstrlen (szUnd)) ;
          
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_DRAWITEM :
     case WM_COMMAND :
          ScrollWindow (hwnd, 0, -cyChar, &rect, &rect) ;
          
          hdc = GetDC (hwnd) ;
          SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
          
          TextOut (hdc, 24 * cxChar, cyChar * (rect.bottom / cyChar - 1),
                   szBuffer,
                   wsprintf (szBuffer, szFormat,
                         message == WM_DRAWITEM ? TEXT ("WM_DRAWITEM") : 
                                                  TEXT ("WM_COMMAND"),
                         HIWORD (wParam), LOWORD (wParam),
                         HIWORD (lParam), LOWORD (lParam))) ;
          
          ReleaseDC (hwnd, hdc) ;
          ValidateRect (hwnd, &rect) ;
          break ;
          
     case WM_DESTROY :
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

As you click on each button, the button sends a WM_COMMAND message to the parent window procedure, which is the familiar WndProc. BTNLOOK's WndProc displays the wParam and lParam parameters of this message in the right half of the client area, as shown in Figure 9-2.

The button with the style BS_OWNERDRAW is displayed on this window only with a background shading because this is a style of button that the program is responsible for drawing. The button indicates it needs drawing by WM_DRAWITEM messages containing an lParam message parameter that is a pointer to a structure of type DRAWITEMSTRUCT. These messages are also displayed in BTNLOOK. I'll discuss owner-draw buttons in more detail later in this chapter.

Click to view at full size.

Figure 9-2. The BTNLOOK display.

Creating the Child Windows

BTNLOOK defines a structure called button that contains button window styles and descriptive text strings for each of the 10 types of buttons. The button window styles all begin with the letters BS, which stand for "button style." The 10 button child windows are created in a for loop during WM_CREATE message processing in WndProc. The CreateWindow call uses the following parameters:

Class name
TEXT ("button")
Window text
button[i].szText
Window style
WS_CHILD ¦ WS_VISIBLE ¦ button[i].iStyle
x position
cxChar
y position
cyChar * (1 + 2 * i)
Width
20 * xChar
Height
7 * yChar / 4
Parent window
hwnd
Child window ID
(HMENU) i
Instance handle
((LPCREATESTRUCT) lParam) -> hInstance
Extra parameters
NULL

The class name parameter is the predefined name. The window style uses WS_CHILD, WS_VISIBLE, and one of the 10 button styles (BS_PUSHBUTTON, BS_DEFPUSHBUTTON, and so forth) that are defined in the button structure. The window text parameter (which for a normal window is the text that appears in the caption bar) is text that will be displayed with each button. I've simply used text that identifies the button style.

The x position and y position parameters indicate the placement of the upper left corner of the child window relative to the upper left corner of the parent window's client area. The width and height parameters specify the width and height of each child window. Notice that I'm using a function named GetDialogBaseUnits to obtain the width and height of the characters in the default font. This is the function that dialog boxes use to obtain text dimensions. The function returns a 32-bit value comprising a width in the low word and a height in the high word. While GetDialogBaseUnits returns roughly the same values as can be obtained from the GetTextMetrics function, it's somewhat easier to use and will ensure more consistency with controls in dialog boxes.

The child window ID parameter should be unique for each child window. This ID helps your window procedure identify the child window when processing WM_COMMAND messages from it. Notice that the child window ID is passed in the CreateWindow parameter normally used to specify the program's menu, so it must be cast to an HMENU.

The instance handle parameter of the CreateWindow call looks a little strange, but we're taking advantage of the fact that during a WM_CREATE message lParam is actually a pointer to a structure of type CREATESTRUCT ("creation structure") that has a member hInstance. So we cast lParam into a pointer to a CREATESTRUCT structure and get hInstance out.

(Some Windows programs use a global variable named hInst to give window procedures access to the instance handle available in WinMain. In WinMain, you need to simply set

hInst = hInstance ;

before creating the main window. In the CHECKER3 program in Chapter 7, we used GetWindowLong to obtain this instance handle:

GetWindowLong (hwnd, GWL_HINSTANCE)

Any of these methods is fine.)

After the CreateWindow call, we needn't do anything more with these child windows. The button window procedure within Windows maintains the buttons for us and handles all repainting jobs. (The exception is the button with the BS_OWNERDRAW style; as I'll discuss later, this button style requires the program to draw the button.) At the program's termination, Windows destroys these child windows when the parent window is destroyed.

The Child Talks to Its Parent

When you run BTNLOOK, you see the different button types displayed on the left side of the client area. As I mentioned earlier, when you click a button with the mouse, the child window control sends a WM_COMMAND message to its parent window. BTNLOOK traps the WM_COMMAND message and displays the values of wParam and lParam. Here's what they mean:

LOWORD (wParam) Child window ID
HIWORD (wParam) Notification code
lParam Child window handle

If you're converting programs written for the 16-bit versions of Windows, be aware that these message parameters have been altered to accommodate 32-bit handles.

The child window ID is the value passed to CreateWindow when the child window is created. In BTNLOOK, these IDs are 0 through 9 for the 10 buttons displayed in the client area. The child window handle is the value that Windows returns from the CreateWindow call.

The notification code indicates in more detail what the message means. The possible values of button notification codes are defined in the Windows header files:

Button Notification Code Identifier Value
BN_CLICKED 0
BN_PAINT 1
BN_HILITE or BN_PUSHED 2
BN_UNHILITE or BN_UNPUSHED 3
BN_DISABLE 4
BN_DOUBLECLICKED or BN_DBLCLK 5
BN_SETFOCUS 6
BN_KILLFOCUS 7

In reality, you'll never see most of these button values. The notification codes 1 through 4 are for an obsolete button style called BS_USERBUTTON. (It's been replaced with BS_OWNERDRAW and a different notification mechanism.) The notification codes 6 and 7 are sent only if the button style includes the flag BS_NOTIFY. The notification code 5 is sent only for BS_RADIOBUTTON, BS_AUTORADIOBUTTON, and BS_OWNERDRAW buttons, or for other buttons if the button style includes BS_NOTIFY.

You'll notice that when you click a button with the mouse, a dashed line surrounds the text of the button. This indicates that the button has the input focus. All keyboard input now goes to the child window button control rather than to the main window. However, when the button control has the input focus, it ignores all keystrokes except the Spacebar, which now has the same effect as a mouse click.

The Parent Talks to Its Child

Although BTNLOOK does not demonstrate this fact, a window procedure can also send messages to the child window control. These messages include many of the window messages beginning with the prefix WM. In addition, eight button-specific messages are defined in WINUSER.H; each begins with the letters BM, which stand for "button message." These button messages are shown in the following table:

Button Message Value
BM_GETCHECK 0x00F0
BM_SETCHECK 0x00F1
BM_GETSTATE 0x00F2
BM_SETSTATE 0x00F3
BM_SETSTYLE 0x00F4
BM_CLICK 0x00F5
BM_GETIMAGE 0x00F6
BM_SETIMAGE 0x00F7

The BM_GETCHECK and BM_SETCHECK messages are sent by a parent window to a child window control to get and set the check mark of check boxes and radio buttons. The BM_GETSTATE and BM_SETSTATE messages refer to the normal, or pushed, state of a window when you click it with the mouse or press it with the Spacebar. We'll see how these messages work when we look at each type of button. The BM_SETSTYLE message lets you change the button style after the button is created.

Each child window has a window handle and an ID that is unique among its siblings. Knowing one of these items allows you to get the other. If you know the window handle of the child, you can obtain the ID using

id = GetWindowLong (hwndChild, GWL_ID) ;

This function (along with SetWindowLong) was used in the CHECKER3 program in Chapter 7 to maintain data in a special area reserved when the window class was registered. The area accessed with the GWL_ID identifier is reserved by Windows when the child window is created. You can also use

id = GetDlgCtrlID (hwndChild) ;

Even though the "Dlg" part of the function name refers to a dialog box, this is really a general-purpose function.

Knowing the ID and the parent window handle, you can get the child window handle:

hwndChild = GetDlgItem (hwndParent, id) ;

Push Buttons

The first two buttons shown in BTNLOOK are "push" buttons. A push button is a rectangle enclosing text specified in the window text parameter of the CreateWindow call. The rectangle takes up the full height and width of the dimensions given in the CreateWindow or MoveWindow call. The text is centered within the rectangle.

Push-button controls are used mostly to trigger an immediate action without retaining any type of on/off indication. The two types of push-button controls have window styles called BS_PUSHBUTTON and BS_DEFPUSHBUTTON. The "DEF" in BS_DEFPUSHBUTTON stands for "default." When used to design dialog boxes, BS_PUSHBUTTON controls and BS_DEFPUSHBUTTON controls function differently from one another. When used as child window controls, however, the two types of push buttons function the same way, although BS_DEFPUSHBUTTON has a heavier outline.

A push button looks best when its height is 7/4 times the height of a text character, which is what BTNLOOK uses. The push button's width must accommodate at least the width of the text, plus two additional characters.

When the mouse cursor is inside the push button, pressing the mouse button causes the button to repaint itself using 3D-style shading to appear as if it's been depressed. Releasing the mouse button restores the original appearance and sends a WM_COMMAND message to the parent window with the notification code BN_CLICKED. As with the other button types, when a push button has the input focus, a dashed line surrounds the text and pressing and releasing the Spacebar has the same effect as pressing and releasing the mouse button.

You can simulate a push-button flash by sending the window a BM_SETSTATE message. This causes the button to be depressed:

SendMessage (hwndButton, BM_SETSTATE, 1, 0) ;

This call causes the button to return to normal:

SendMessage (hwndButton, BM_SETSTATE, 0, 0) ;

The hwndButton window handle is the value returned from the CreateWindow call.

You can also send a BM_GETSTATE message to a push button. The child window control returns the current state of the button: TRUE if the button is depressed and FALSE if it isn't depressed. Most applications do not require this information, however. And because push buttons do not retain any on/off information, the BM_SETCHECK and BM_GETCHECK messages are not used.

Check Boxes

A check box is a square box with text; the text usually appears to the right of the check box. (If you include the BS_LEFTTEXT style when creating the button, the text appears to the left; you'll probably want to combine this style with BS_RIGHT to right-justify the text.) Check boxes are usually incorporated in an application to allow a user to select options. The check box commonly functions as a toggle switch: clicking the box once causes a check mark to appear; clicking again toggles the check mark off.

The two most common styles for a check box are BS_CHECKBOX and BS_AUTOCHECKBOX. When you use the BS_CHECKBOX style, you must set the check mark yourself by sending the control a BM_SETCHECK message. The wParam parameter is set to 1 to create a check mark and to 0 to remove it. You can obtain the current check state of the box by sending the control a BM_GETCHECK message. You might use code like this to toggle the X mark when processing a WM_COMMAND message from the control:

SendMessage ((HWND) lParam, BM_SETCHECK, (WPARAM)
          !SendMessage ((HWND) lParam, BM_GETCHECK, 0, 0), 0) ;

Notice the ! operator in front of the second SendMessage call. The lParam value is the child window handle that is passed to your window procedure in the WM_COMMAND message. When you later need to know the state of the button, send it another BM_GETCHECK message. Or you can retain the current check state in a static variable in your window procedure. You can also initialize a BS_CHECKBOX check box with a check mark by sending it a BM_SETCHECK message:

SendMessage (hwndButton, BM_SETCHECK, 1, 0) ;

For the BS_AUTOCHECKBOX style, the button control itself toggles the check mark on and off. Your window procedure can ignore WM_COMMAND messages. When you need the current state of the button, send the control a BM_GETCHECK message:

iCheck = (int) SendMessage (hwndButton, BM_GETCHECK, 0, 0) ;

The value of iCheck is TRUE or nonzero if the button is checked and FALSE or 0 if not.

The other two check box styles are BS_3STATE and BS_AUTO3STATE. As their names indicate, these styles can display a third state as well—a gray color within the check box—which occurs when you send the control a WM_SETCHECK message with wParam equal to 2. The gray color indicates to the user that the selection is indeterminate or irrelevant.

The check box is aligned with the rectangle's left edge and is centered within the top and bottom dimensions of the rectangle that were specified during the CreateWindow call. Clicking anywhere within the rectangle causes a WM_COMMAND message to be sent to the parent. The minimum height for a check box is one character height. The minimum width is the number of characters in the text, plus two.

Radio Buttons

A radio button is named after the row of buttons that were once quite common on car radios. Each button on a car radio is set for a different radio station, and only one button can be pressed at a time. In dialog boxes, groups of radio buttons are conventionally used to indicate mutually exclusive options. Unlike check boxes, radio buttons do not work as toggles—that is, when you click a radio button a second time, its state remains unchanged.

The radio button looks very much like a check box except that it contains a little circle rather than a box. A heavy dot within the circle indicates that the radio button has been checked. The radio button has the window style BS_RADIOBUTTON or BS_AUTORADIOBUTTON, but the latter is used only in dialog boxes.

When you receive a WM_COMMAND message from a radio button, you should display its check by sending it a BM_SETCHECK message with wParam equal to 1:

SendMessage (hwndButton, BM_SETCHECK, 1, 0) ;

For all other radio buttons in the same group, you can turn off the checks by sending them BM_SETCHECK messages with wParam equal to 0:

SendMessage (hwndButton, BM_SETCHECK, 0, 0) ;

Group Boxes

The group box, which has the BS_GROUPBOX style, is an oddity in the button class. It neither processes mouse or keyboard input nor sends WM_COMMAND messages to its parent. The group box is a rectangular outline with its window text at the top. Group boxes are often used to enclose other button controls.

Changing the Button Text

You can change the text in a button (or in any other window) by calling SetWindowText:

SetWindowText (hwnd, pszString) ;

where hwnd is a handle to the window whose text is being changed and pszString is a pointer to a null-terminated string. For a normal window, this text is the text of the caption bar. For a button control, it's the text displayed with the button.

You can also obtain the current text of a window:

iLength = GetWindowText (hwnd, pszBuffer, iMaxLength) ;

The iMaxLength parameter specifies the maximum number of characters to copy into the buffer pointed to by pszBuffer. The function returns the string length copied. You can prepare your program for a particular text length by first calling

iLength = GetWindowTextLength (hwnd) ;

Visible and Enabled Buttons

To receive mouse and keyboard input, a child window must be both visible (displayed) and enabled. When a child window is visible but not enabled, Windows displays the text in gray rather than black.

If you don't include WS_VISIBLE in the window class when creating the child window, the child window will not be displayed until you make a call to ShowWindow:

ShowWindow (hwndChild, SW_SHOWNORMAL) ;

But if you include WS_VISIBLE in the window class, you don't need to call ShowWindow. However, you can hide the child window by this call to ShowWindow:

ShowWindow (hwndChild, SW_HIDE) ;

You can determine if a child window is visible by a call to

IsWindowVisible (hwndChild) ;

You can also enable and disable a child window. By default, a window is enabled. You can disable it by calling

EnableWindow (hwndChild, FALSE) ;

For button controls, this call has the effect of graying the button text string. The button no longer responds to mouse or keyboard input. This is the best method for indicating that a button option is currently unavailable.

You can reenable a child window by calling

EnableWindow (hwndChild, TRUE) ;

You can determine whether a child window is enabled by calling

IsWindowEnabled (hwndChild) ;

Buttons and Input Focus

As I noted earlier in this chapter, push buttons, check boxes, radio buttons, and owner-draw buttons receive the input focus when they are clicked with the mouse. The control indicates it has the input focus with a dashed line that surrounds the text. When the child window control gets the input focus, the parent window loses it; all keyboard input then goes to the control rather than to the parent window. However, the child window control responds only to the Spacebar, which now functions like the mouse. This situation presents an obvious problem: your program has lost control of keyboard processing. Let's see what we can do about it.

As I discussed in Chapter 6, when Windows switches the input focus from one window (such as a parent) to another (such as a child window control), it first sends a WM_KILLFOCUS message to the window losing the input focus. The wParam parameter is the handle of the window that is to receive the input focus. Windows then sends a WM_SETFOCUS message to the window receiving the input focus, with wParam specifying the handle of the window losing the input focus. (In both cases, wParam might be NULL, which indicates that no window has or is receiving the input focus.)

A parent window can prevent a child window control from getting the input focus by processing WM_KILLFOCUS messages. Assume that the array hwndChild contains the window handles of all child windows. (These were saved in the array during the CreateWindow calls that created the windows.) NUM is the number of child windows.

case WM_KILLFOCUS :

     for (i = 0 ; i < NUM ; i++)
          if (hwndChild [i] == (HWND) wParam)
          {
               SetFocus (hwnd) ;
               break ;
          }
     return 0 ;

In this code, when the parent window detects that it's losing the input focus to one of its child window controls, it calls SetFocus to restore the input focus to itself.

Here's a simpler (but less obvious) way of doing it:

case WM_KILLFOCUS :
     if (hwnd == GetParent ((HWND) wParam))
          SetFocus (hwnd) ;
     return 0 ;

Both these methods have a shortcoming, however: they prevent the button from responding to the Spacebar, because the button never gets the input focus. A better approach would be to let the button get the input focus but also to include the facility for the user to move from button to button using the Tab key. At first this sounds impossible, but I'll show you how to accomplish it with a technique called "window subclassing" in the COLORS1 program shown later in this chapter.