Get a site

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

Modal Dialog Boxes

Dialog boxes are either "modal" or "modeless." The modal dialog box is the most common. When your program displays a modal dialog box, the user cannot switch between the dialog box and another window in your program. The user must explicitly end the dialog box, usually by clicking a push button marked either OK or Cancel. The user can, however, switch to another program while the dialog box is still displayed. Some dialog boxes (called "system modal") do not allow even this. System modal dialog boxes must be ended before the user can do anything else in Windows.

Creating an "About" Dialog Box

Even if a Windows program requires no user input, it will often have a dialog box that is invoked by an About option on the menu. This dialog box displays the name and icon of the program, a copyright notice, a push button labeled OK, and perhaps some other information. (Perhaps a telephone number for technical support?) The first program we'll look at does nothing except display an About dialog box. The ABOUT1 program is shown in Figure 11-1.

Figure 11-1. The ABOUT1 program.

ABOUT1.C

/*------------------------------------------ ABOUT1.C -- About Box Demo Program No. 1 (c) Charles Petzold, 1998 ------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK AboutDlgProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("About1") ; MSG msg ; HWND hwnd ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("About Box Demo Program"), 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 HINSTANCE hInstance ; switch (message) { case WM_CREATE : hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; return 0 ; case WM_COMMAND : switch (LOWORD (wParam)) { case IDM_APP_ABOUT : DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc) ; break ; } return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG : return TRUE ; case WM_COMMAND : switch (LOWORD (wParam)) { case IDOK : case IDCANCEL : EndDialog (hDlg, 0) ; return TRUE ; } break ; } return FALSE ; }

ABOUT1.RC (excerpts)

//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
// Dialog

ABOUTBOX DIALOG DISCARDABLE  32, 32, 180, 100
STYLE DS_MODALFRAME | WS_POPUP
FONT 8, "MS Sans Serif"
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,66,80,50,14
    ICON            "ABOUT1",IDC_STATIC,7,7,21,20
    CTEXT           "About1",IDC_STATIC,40,12,100,8
    CTEXT           "About Box Demo Program",IDC_STATIC,7,40,166,8
    CTEXT           "(c) Charles Petzold, 1998",IDC_STATIC,7,52,166,8
END

/////////////////////////////////////////////////////////////////////////////
// Menu

ABOUT1 MENU DISCARDABLE 
BEGIN
    POPUP "&Help"
    BEGIN
        MENUITEM "&About About1...",            IDM_APP_ABOUT
    END
END

/////////////////////////////////////////////////////////////////////////////
// Icon

ABOUT1                  ICON    DISCARDABLE     "About1.ico"

RESOURCE.H (excerpts)

// Microsoft Developer Studio generated include file.
// Used by About1.rc

#define IDM_APP_ABOUT                   40001
#define IDC_STATIC                      -1

ABOUT1.ICO

You create the icon and the menu in this program the same way as described in the last chapter. Both the icon and the menu have text ID names of "About1." The menu has one option, which generates a WM_COMMAND message with an ID of IDM_APP_ABOUT. This causes the program to display the dialog box shown in Figure 11-2.

Click to view at full size.

Figure 11-2. The ABOUT1 program's dialog box.

The Dialog Box and Its Template

To add a dialog box to an application in the Visual C++ Developer Studio, you begin by selecting Resource from the Insert menu and choosing Dialog Box. You are then presented with a dialog box with a title bar and caption ("Dialog") and OK and Cancel buttons. A Controls toolbar allows you to insert various controls in the dialog box.

Developer Studio gives the dialog box a standard ID of IDD_DIALOG1. You can right-click this name (or the dialog box itself) and select Properties from the menu. For this program, change the ID to "AboutBox" (with quotation marks). To be consistent with the dialog box I created, change the X Pos and Y Pos fields to 32. This is to indicate where the dialog box is displayed relative to the upper left corner of the client area of the program's window. (I'll discuss dialog box coordinates in more detail shortly.)

Now, still in the Properties dialog, select the Styles tab. Unclick the Title Bar check box because this dialog box does not have a title bar. Click the close button on the Properties dialog.

Now it's time to actually design the dialog box. We won't be needing the Cancel button, so click that button and press the Delete key on your keyboard. Click the OK button, and move it to the bottom of the dialog. At the bottom of the Developer Studio window will be a small bitmap on a toolbar that lets you center the control horizontally in the window. Press that button.

We want the program's icon to appear in the dialog box. To do so, press the Pictures button on the floating Controls toolbar. Move the mouse to the surface of the dialog box, press the left button, and drag a square. This is where the icon will appear. Press the right mouse button on this square, and select Properties from the menu. Leave the ID as IDC_STATIC. This identifier will be defined in RESOURCE.H as -1, which is used for all IDs that the C program does not refer to. Change the Type to Icon. You should be able to type the name of the program's icon in the Image field, or, if you've already created the icon, you can select the name ("About1") from the combo box.

For the three static text strings in the dialog box, select Static Text from the Controls toolbar and position the text in the dialog window. Right-click the control, and select Properties from the menu. You'll type the text you want to appear in the Caption field of the Properties box. Select the Styles tab to select Center from the Align Text field.

As you add these text strings, you may want to make the dialog box larger. Select it and drag the outline. You can also select and size controls. It's often easier to use the keyboard cursor movement keys for this. The arrow keys by themselves move the controls; the arrow keys with Shift depressed let you change the controls' sizes. The coordinates and sizes of the selected control are shown in the lower right corner of the Developer Studio window.

If you build the application and later look at the ABOUT1.RC resource script file, you'll see the dialog box template that Developer Studio generated. The dialog box that I designed has a template that looks like this:

ABOUTBOX DIALOG DISCARDABLE  32, 32, 180, 100
STYLE DS_MODALFRAME | WS_POPUP
FONT 8, "MS Sans Serif"

BEGIN
    DEFPUSHBUTTON   "OK",IDOK,66,80,50,14
    ICON            "ABOUT1",IDC_STATIC,7,7,21,20
    CTEXT           "About1",IDC_STATIC,40,12,100,8
    CTEXT           "About Box Demo Program",IDC_STATIC,7,40,166,8
    CTEXT           "(c) Charles Petzold, 1998",IDC_STATIC,7,52,166,8
END

The first line gives the dialog box a name (in this case, ABOUTBOX). As is the case for other resources, you can use a number instead. The name is followed by the keywords DIALOG and DISCARDABLE, and four numbers. The first two numbers are the x and y coordinates of the upper left corner of the dialog box, relative to the client area of its parent when the dialog box is invoked by the program. The second two numbers are the width and height of the dialog box.

These coordinates and sizes are not in units of pixels. They are instead based on a special coordinate system used only for dialog box templates. The numbers are based on the size of the font used for the dialog box (in this case, an 8-point MS Sans Serif font): x-coordinates and width are expressed in units of 1/4 of an average character width; y-coordinates and height are expressed in units of 1/8 of the character height. Thus, for this particular dialog box, the upper left corner of the dialog box is 5 characters from the left edge of the main window's client area and 2-1/2 characters from the top edge. The dialog itself is 40 characters wide and 10 characters high.

This coordinate system allows you to use coordinates and sizes that will retain the general dimensions and look of the dialog box regardless of the resolution of the video display and the font you've selected. Because font characters are often approximately twice as high as they are wide, the dimensions on both the x-axis and the y-axis are nearly the same.

The STYLE statement in the template is similar to the style field of a CreateWindow call. WS_POPUP and DS_MODALFRAME are normally used for modal dialog boxes, but we'll explore some alternatives later on.

Within the BEGIN and END statements (or left and right brackets, if you'd prefer, when designing dialog box templates by hand), you define the child window controls that will appear in the dialog box. This dialog box uses three types of child window controls: DEFPUSHBUTTON (a default push button), ICON (an icon), and CTEXT (centered text). The format of these statements is

control-type "text" id, xPos, yPos, xWidth, yHeight, iStyle

The iStyle value at the end is optional; it specifies additional window styles using identifiers defined in the Windows header files.

These DEFPUSHBUTTON, ICON, and CTEXT identifiers are used in dialog boxes only. They are shorthand for a particular window class and window style. For example, CTEXT indicates that the class of the child window control is "static" and that the style is

WS_CHILD ¦ SS_CENTER ¦ WS_VISIBLE ¦ WS_GROUP

Although this is the first time we've encountered the WS_GROUP identifier, we used the WS_CHILD, SS_CENTER, and WS_VISIBLE window styles when creating static child window text controls in the COLORS1 program in Chapter 9.

For the icon, the text field is the name of the program's icon resource, which is also defined in the ABOUT1 resource script. For the push button, the text field is the text that appears inside the push button. This text is equivalent to the text specified as the second argument in a CreateWindow call when you create a child window control in a program.

The id field is a value that the child window uses to identify itself when sending messages (usually WM_COMMMAND messages) to its parent. The parent window of these child window controls is the dialog box window itself, which sends these messages to a window procedure in Windows. However, this window procedure also sends these messages to the dialog box procedure that you'll include in your program. The ID values are equivalent to the child window IDs used in the CreateWindow function when we created child windows in Chapter 9. Because the text and icon controls do not send messages back to the parent window, these values are set to IDC_STATIC, which is defined in RESOURCE.H as -1. The ID value for the push button is IDOK, which is defined in WINUSER.H as 1.

The next four numbers set the position of the child window control (relative to the upper left corner of the dialog box's client area) and the size. The position and size are expressed in units of 1/4 of the average width and 1/8 of the height of a font character. The width and height values are ignored for the ICON statement.

The DEFPUSHBUTTON statement in the dialog box template includes the window style WS_GROUP in addition to the window style implied by the DEFPUSHBUTTON keyword. I'll have more to say about WS_GROUP (and the related WS_TABSTOP style) when discussing the second version of this program, ABOUT2, a bit later.

The Dialog Box Procedure

The dialog box procedure within your program handles messages to the dialog box. Although it looks very much like a window procedure, it is not a true window procedure. The window procedure for the dialog box is within Windows. That window procedure calls your dialog box procedure with many of the messages that it receives. Here's the dialog box procedure for ABOUT1:

BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, 
                            WPARAM wParam, LPARAM lParam)
{
     switch (message)
     {
     case WM_INITDIALOG :
          return TRUE ;
          
     case WM_COMMAND :
          switch (LOWORD (wParam))
          {
          case IDOK :
          case IDCANCEL :
               EndDialog (hDlg, 0) ;
               return TRUE ;
          }
          break ;
     }
     return FALSE ;
}

The parameters to this function are the same as those for a normal window procedure; as with a window procedure, the dialog box procedure must be defined as a CALLBACK function. Although I've used hDlg for the handle to the dialog box window, you can use hwnd instead if you like. Let's note first the differences between this function and a window procedure:

The WM_INITDIALOG message is the first message the dialog box procedure receives. This message is sent only to dialog box procedures. If the dialog box procedure returns TRUE, Windows sets the input focus to the first child window control in the dialog box that has a WS_TABSTOP style (which I'll explain in the discussion of ABOUT2). In this dialog box, the first child window control that has a WS_TABSTOP style is the push button. Alternatively, during the processing of WM_INITDIALOG, the dialog box procedure can use SetFocus to set the focus to one of the child window controls in the dialog box and then return FALSE.

The only other message this dialog box processes is WM_COMMAND. This is the message the push-button control sends to its parent window either when the button is clicked with the mouse or when the Spacebar is pressed while the button has the input focus. The ID of the control (which we set to IDOK in the dialog box template) is in the low word of wParam. For this message, the dialog box procedure calls EndDialog, which tells Windows to destroy the dialog box. For all other messages, the dialog box procedure returns FALSE to tell the dialog box window procedure within Windows that our dialog box procedure did not process the message.

The messages for a modal dialog box don't go through your program's message queue, so you needn't worry about the effect of keyboard accelerators within the dialog box.

Invoking the Dialog Box

During the processing of WM_CREATE in WndProc, ABOUT1 obtains the program's instance handle and stores it in a static variable:

hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;

ABOUT1 checks for WM_COMMAND messages where the low word of wParam is equal to IDM_APP_ABOUT. When it gets one, the program calls DialogBox:

DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc) ;

This function requires the instance handle (saved during WM_CREATE), the name of the dialog box (as defined in the resource script), the parent of the dialog box (which is the program's main window), and the address of the dialog procedure. If you use a numeric identifier rather than a name for the dialog box template, you can convert it to a string using the MAKEINTRESOURCE macro.

Selecting About About1 from the menu displays the dialog box, as shown in Figure 11-2. You can end this dialog box by clicking the OK button with the mouse, by pressing the Spacebar, or by pressing Enter. For any dialog box that contains a default push button, Windows sends a WM_COMMAND message to the dialog box, with the low word of wParam equal to the ID of the default push button when Enter or the Spacebar is pressed. That ID is IDOK. You can also end the dialog box by pressing Escape. In that case Windows sends a WM_COMMAND message with an ID equal to IDCANCEL.

The DialogBox function you call to display the dialog box will not return control to WndProc until the dialog box is ended. The value returned from DialogBox is the second parameter to the EndDialog function called within the dialog box procedure. (This value is not used in ABOUT1 but is used in ABOUT2.) WndProc can then return control to Windows.

Even when the dialog box is displayed, however, WndProc can continue to receive messages. In fact, you can send messages to WndProc from within the dialog box procedure. ABOUT1's main window is the parent of the dialog box popup window, so the SendMessage call in AboutDlgProc would start off like this:

SendMessage (GetParent (hDlg),  . . . ) ;

Variations on a Theme

Although the dialog editor and other resource editors in the Visual C++ Developer Studio seemingly make it unnecessary to even look at resource scripts, it is still helpful to learn resource script syntax. Particularly for dialog templates, knowing the syntax allows you to have a better feel for the scope and limitations of dialog boxes. You may even want to create a dialog box template manually if there's something you need to do that can't be done otherwise (such as in the HEXCALC program later in this chapter). The resource compiler and resource script syntax is documented in /Platform SDK/Windows Programming Guidelines/Platform SDK Tools/Compiling/Using the Resource Compiler.

The window style of the dialog box is specified in the Properties dialog in the Developer Studio, which is translated into the STYLE line of the dialog box template. For ABOUT1, we used a style that is most common for modal dialog boxes:

STYLE WS_POPUP ¦ DS_MODALFRAME

However, you can also experiment with other styles. Some dialog boxes have a caption bar that identifies the dialog's purpose and lets the user move the dialog box around the display using the mouse. This is the style WS_CAPTION. When you use WS_CAPTION, the x and y coordinates specified in the DIALOG statement are the coordinates of the dialog box's client area, relative to the upper left corner of the parent window's client area. The caption bar will be shown above the y-coordinate.

If you have a caption bar, you can put text in it using the CAPTION statement, following the STYLE statement, in the dialog box template:

CAPTION "Dialog Box Caption"

Or while processing the WM_INITDIALOG message in the dialog procedure, you can use

SetWindowText (hDlg, TEXT ("Dialog Box Caption")) ;

If you use the WS_CAPTION style, you can also add a system menu box with the WS_SYSMENU style. This style allows the user to select Move or Close from the system menu.

Selecting Resizing from the Border list box of the Properties dialog (equivalent to the style WS_THICKFRAME) allows the user to resize the dialog box, although this is unusual. If you don't mind being even more unusual, you can also try adding a maximize box to the dialog box style.

You can even add a menu to a dialog box. The dialog box template will include the statement

MENU menu-name

The argument is either the name or the number of a menu in the resource script. Menus are highly uncommon for modal dialog boxes. If you use one, be sure that all the ID numbers in the menu and the dialog box controls are unique, or if they're not, that they duplicate the same commands.

The FONT statement lets you set something other than the system font for use with dialog box text. This was once uncommon in dialog boxes but is now quite normal. Indeed, Developer Studio selects the 8-point MS Sans Serif font by default in any dialog box you create. A Windows program can achieve a unique look by shipping a special font with a program that is used solely by the program for dialog boxes and other text output.

Although the dialog box window procedure is normally within Windows, you can use one of your own window procedures to process dialog box messages. To do so, specify a window class name in the dialog box template:

CLASS "class-name"

There are some other considerations involved, but I'll demonstrate this approach in the HEXCALC program shown later in this chapter.

When you call DialogBox, specifying the name of a dialog box template, Windows has almost everything it needs to create a popup window by calling the normal CreateWindow function. Windows obtains the coordinates and size of the window, the window style, the caption, and the menu from the dialog box template. Windows gets the instance handle and the parent window handle from the arguments to DialogBox. The only other piece of information it needs is a window class (assuming the dialog box template does not specify one). Windows registers a special window class for dialog boxes. The window procedure for this window class has access to the address of your dialog box procedure (which you provide in the DialogBox call), so it can keep your program informed of messages that this popup window receives. Of course, you can create and maintain your own dialog box by creating the popup window yourself. Using DialogBox is simply an easier approach.

You may want the benefit of using the Windows dialog manager, but you may not want to (or be able to) define the dialog template in a resource script. Perhaps you want the program to create a dialog box dynamically as it's running. The function to look at is DialogBoxIndirect, which uses data structures to define the template.

In the dialog box template in ABOUT1.RC, the shorthand notation CTEXT, ICON, and DEFPUSHBUTTON is used to define the three types of child window controls we want in the dialog box. There are several others that you can use. Each type implies a particular predefined window class and a window style. The following table shows the equivalent window class and window style for some common control types:
Control Type Window Class Window Style
PUSHBUTTON button BS_PUSHBUTTON ¦ WS_TABSTOP
DEFPUSHBUTTON button BS_DEFPUSHBUTTON ¦ WS_TABSTOP
CHECKBOX button BS_CHECKBOX ¦ WS_TABSTOP
RADIOBUTTON button BS_RADIOBUTTON ¦ WS_TABSTOP
GROUPBOX button BS_GROUPBOX ¦ WS_TABSTOP
LTEXT static SS_LEFT ¦ WS_GROUP
CTEXT static SS_CENTER ¦ WS_GROUP
RTEXT static SS_RIGHT ¦ WS_GROUP
ICON static SS_ICON
EDITTEXT edit ES_LEFT ¦ WS_BORDER ¦ WS_TABSTOP
SCROLLBAR scrollbar SBS_HORZ
LISTBOX listbox LBS_NOTIFY ¦ WS_BORDER ¦ WS_VSCROLL
COMBOBOX combobox CBS_SIMPLE ¦ WS_TABSTOP

The resource compiler is the only program that understands this shorthand notation. In addition to the window styles shown above, each of these controls has the style

WS_CHILD ¦ WS_VISIBLE

For all these control types except EDITTEXT, SCROLLBAR, LISTBOX, and COMBOBOX, the format of the control statement is

control-type "text", id, xPos, yPos, xWidth, yHeight, iStyle

For EDITTEXT, SCROLLBAR, LISTBOX, and COMBOBOX, the format is

control-type id, xPos, yPos, xWidth, yHeight, iStyle

which excludes the text field. In both statements, the iStyle parameter is optional.

In Chapter 9, I discussed rules for determining the width and height of predefined child window controls. You might want to refer back to that chapter for these rules, keeping in mind that sizes specified in dialog box templates are always in terms of 1/4 of the average character width and 1/8 of the character height.

The "style" field of the control statements is optional. It allows you to include other window style identifiers. For instance, if you wanted to create a check box consisting of text to the left of a square box, you could use

CHECKBOX "text", id, xPos, yPos, xWidth, yHeight, BS_LEFTTEXT

Notice that the control type EDITTEXT automatically has a border. If you want to create a child window edit control without a border, you can use

EDITTEXT id, xPos, yPos, xWidth, yHeight, NOT WS_BORDER

The resource compiler also recognizes a generalized control statement that looks like

CONTROL "text", id, "class", iStyle, xPos, yPos, xWidth, yHeight

This statement allows you to create any type of child window control by specifying the window class and the complete window style. For example, instead of using

PUSHBUTTON "OK", IDOK, 10, 20, 32, 14

you can use

CONTROL "OK", IDOK, "button", WS_CHILD ¦ WS_VISIBLE ¦
          BS_PUSHBUTTON ¦ WS_TABSTOP, 10, 20, 32, 14

When the resource script is compiled, these two statements are encoded identically in the .RES file and the .EXE file. In Developer Studio, you create a statement like this using the Custom Control option from the Controls toolbar. In the ABOUT3 program, shown in Figure 11-5, I show how you can use this to create a control whose window class is defined in your program.

When you use CONTROL statements in a dialog box template, you don't need to include the WS_CHILD and WS_VISIBLE styles. Windows includes these in the window style when creating the child windows. The format of the CONTROL statement also clarifies what the Windows dialog manager does when it creates a dialog box. First, as I described earlier, it creates a popup window whose parent is the window handle that was provided in the DialogBox function. Then, for each control in the dialog template, the dialog box manager creates a child window. The parent of each of these controls is the popup dialog box. The CONTROL statement shown above is translated into a CreateWindow call that looks like

hCtrl = CreateWindow (TEXT ("button"), TEXT ("OK"),
                      WS_CHILD ¦ WS_VISIBLE ¦ WS_TABSTOP ¦ BS_PUSHBUTTON,
                      10 * cxChar / 4, 20 * cyChar / 8,
                      32 * cxChar / 4, 14 * cyChar / 8,
                      hDlg, IDOK, hInstance, NULL) ;

where cxChar and cyChar are the width and height of the dialog box font character in pixels. The hDlg parameter is returned from the CreateWindow call that creates the dialog box window. The hInstance parameter is obtained from the original DialogBox call.

A More Complex Dialog Box

The simple dialog box in ABOUT1 demonstrates the basics of getting a dialog box up and running; now let's try something a little more complex. The ABOUT2 program, shown in Figure 11-3, demonstrates how to manage controls (in this case, radio buttons) within a dialog box procedure and also how to paint on the client area of the dialog box.

Figure 11-3. The ABOUT2 program.

ABOUT2.C

/*------------------------------------------
   ABOUT2.C -- About Box Demo Program No. 2
               (c) Charles Petzold, 1998
  ------------------------------------------*/

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc      (HWND, UINT, WPARAM, LPARAM) ;
BOOL    CALLBACK AboutDlgProc (HWND, UINT, WPARAM, LPARAM) ;
     
int iCurrentColor  = IDC_BLACK,
    iCurrentFigure = IDC_RECT ;

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

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

void PaintWindow (HWND hwnd, int iColor, int iFigure)
{
     static COLORREF crColor[8] = { RGB (  0,   0, 0), RGB (  0,   0, 255),
                                    RGB (  0, 255, 0), RGB (  0, 255, 255),
                                    RGB (255,   0, 0), RGB (255,   0, 255),
                                    RGB (255, 255, 0), RGB (255, 255, 255) } ;

     HBRUSH          hBrush ;
     HDC             hdc ;
     RECT            rect ;
     
     hdc = GetDC (hwnd) ;
     GetClientRect (hwnd, &rect) ;
     hBrush = CreateSolidBrush (crColor[iColor - IDC_BLACK]) ;
     hBrush = (HBRUSH) SelectObject (hdc, hBrush) ;
     
     if (iFigure == IDC_RECT)
          Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ;
     else
          Ellipse   (hdc, rect.left, rect.top, rect.right, rect.bottom) ;
     
     DeleteObject (SelectObject (hdc, hBrush)) ;
     ReleaseDC (hwnd, hdc) ;
}

void PaintTheBlock (HWND hCtrl, int iColor, int iFigure)
{
     InvalidateRect (hCtrl, NULL, TRUE) ;
     UpdateWindow (hCtrl) ;
     PaintWindow (hCtrl, iColor, iFigure) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static HINSTANCE hInstance ;
     PAINTSTRUCT      ps ;
     
     switch (message)
     {
     case WM_CREATE:
          hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;
          return 0 ;
          
     case WM_COMMAND:
          switch (LOWORD (wParam))
          {
          case IDM_APP_ABOUT:
               if (DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc))
                    InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;
          }
          break ;
          
     case WM_PAINT:
          BeginPaint (hwnd, &ps) ;
          EndPaint (hwnd, &ps) ;
               
          PaintWindow (hwnd, iCurrentColor, iCurrentFigure) ;
          return 0 ;
               
     case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, 
                            WPARAM wParam, LPARAM lParam)
{
     static HWND hCtrlBlock ;
     static int  iColor, iFigure ;
     
     switch (message)
     {
     case WM_INITDIALOG:
          iColor  = iCurrentColor ;
          iFigure = iCurrentFigure ;
          
          CheckRadioButton (hDlg, IDC_BLACK, IDC_WHITE,   iColor) ;
          CheckRadioButton (hDlg, IDC_RECT,  IDC_ELLIPSE, iFigure) ;
          
          hCtrlBlock = GetDlgItem (hDlg, IDC_PAINT) ;
          
          SetFocus (GetDlgItem (hDlg, iColor)) ;
          return FALSE ;
          
     case WM_COMMAND:
          switch (LOWORD (wParam))
          {
          case IDOK:
               iCurrentColor  = iColor ;
               iCurrentFigure = iFigure ;
               EndDialog (hDlg, TRUE) ;
               return TRUE ;
               
          case IDCANCEL:
               EndDialog (hDlg, FALSE) ;
               return TRUE ;
               
          case IDC_BLACK:
          case IDC_RED:
          case IDC_GREEN:
          case IDC_YELLOW:
          case IDC_BLUE:
          case IDC_MAGENTA:
          case IDC_CYAN:
          case IDC_WHITE:
               iColor = LOWORD (wParam) ;
               CheckRadioButton (hDlg, IDC_BLACK, IDC_WHITE, LOWORD (wParam)) ;
               PaintTheBlock (hCtrlBlock, iColor, iFigure) ;
               return TRUE ;
               
          case IDC_RECT:
          case IDC_ELLIPSE:
               iFigure = LOWORD (wParam) ;
               CheckRadioButton (hDlg, IDC_RECT, IDC_ELLIPSE, LOWORD (wParam)) ;
               PaintTheBlock (hCtrlBlock, iColor, iFigure) ;
               return TRUE ;
          }

          break ;
          
     case WM_PAINT:
          PaintTheBlock (hCtrlBlock, iColor, iFigure) ;
          break ;
     }
     return FALSE ;
}

ABOUT2.RC (excerpts)

//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
// Dialog

ABOUTBOX DIALOG DISCARDABLE  32, 32, 200, 234
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION
FONT 8, "MS Sans Serif"
BEGIN
    ICON            "ABOUT2",IDC_STATIC,7,7,20,20
    CTEXT           "About2",IDC_STATIC,57,12,86,8
    CTEXT           "About Box Demo Program",IDC_STATIC,7,40,186,8
    LTEXT           "",IDC_PAINT,114,67,74,72
    GROUPBOX        "&Color",IDC_STATIC,7,60,84,143
    RADIOBUTTON     "&Black",IDC_BLACK,16,76,64,8,WS_GROUP | WS_TABSTOP
    RADIOBUTTON     "B&lue",IDC_BLUE,16,92,64,8
    RADIOBUTTON     "&Green",IDC_GREEN,16,108,64,8
    RADIOBUTTON     "Cya&n",IDC_CYAN,16,124,64,8
    RADIOBUTTON     "&Red",IDC_RED,16,140,64,8
    RADIOBUTTON     "&Magenta",IDC_MAGENTA,16,156,64,8
    RADIOBUTTON     "&Yellow",IDC_YELLOW,16,172,64,8
    RADIOBUTTON     "&White",IDC_WHITE,16,188,64,8
    GROUPBOX        "&Figure",IDC_STATIC,109,156,84,46,WS_GROUP
    RADIOBUTTON     "Rec&tangle",IDC_RECT,116,172,65,8,WS_GROUP | WS_TABSTOP
    RADIOBUTTON     "&Ellipse",IDC_ELLIPSE,116,188,64,8
    DEFPUSHBUTTON   "OK",IDOK,35,212,50,14,WS_GROUP
    PUSHBUTTON      "Cancel",IDCANCEL,113,212,50,14,WS_GROUP
END

/////////////////////////////////////////////////////////////////////////////
// Icon
ABOUT2                  ICON    DISCARDABLE     "About2.ico"

/////////////////////////////////////////////////////////////////////////////
// Menu

ABOUT2 MENU DISCARDABLE 
BEGIN
    POPUP "&Help"
    BEGIN
        MENUITEM "&About",                      IDM_APP_ABOUT
    END
END

RESOURCE.H (excerpts)

// Microsoft Developer Studio generated include file.
// Used by About2.rc

#define IDC_BLACK                       1000
#define IDC_BLUE                        1001
#define IDC_GREEN                       1002
#define IDC_CYAN                        1003
#define IDC_RED                         1004
#define IDC_MAGENTA                     1005
#define IDC_YELLOW                      1006
#define IDC_WHITE                       1007
#define IDC_RECT                        1008
#define IDC_ELLIPSE                     1009
#define IDC_PAINT                       1010
#define IDM_APP_ABOUT                   40001
#define IDC_STATIC                      -1
ABOUT2.ICO

The About box in ABOUT2 has two groups of radio buttons. One group is used to select a color, and the other group is used to select either a rectangle or an ellipse. The rectangle or ellipse is shown in the dialog box with the interior colored with the current color selection. If you press the OK button, the dialog box is ended, and the program's window procedure draws the selected figure in its own client area. If you press Cancel, the client area of the main window remains the same. The dialog box is shown in Figure 11-4. Although the ABOUT2 dialog box uses the predefined identifiers IDOK and IDCANCEL for the two push buttons, each of the radio buttons has its own identifier beginning with the letters IDC ("ID for a control"). These identifiers are defined in RESOURCE.H.

Click to view at full size.

Figure 11-4. The ABOUT2 program's dialog box.

When you create the radio buttons in the ABOUT2 dialog box, create them in the order shown. This ensures that Developer Studio defines sequentially valued identifiers, which is assumed by the program. Also, uncheck the Auto option for each radio button. The Auto Radio Button requires less code but is initially more mysterious. Give them the identifiers shown above in ABOUT2.RC.

Check the Group option in the Properties dialog for the OK and Cancel buttons, and for the Figure group box, and for the first radio buttons (Black and Rectangle) in each group. Check the Tab Stop check box for these two radio buttons.

When you have all the controls in the dialog box approximately positioned and sized, choose the Tab Order option from the Layout menu. Click each control in the order shown in the ABOUT2.RC resource script.

Working with Dialog Box Controls

In Chapter 9, you discovered that most child window controls send WM_COMMAND messages to the parent window. (The exception is scroll bar controls.) You also saw that the parent window can alter child window controls (for instance, checking or unchecking radio buttons or check boxes) by sending messages to the controls. You can similarly alter controls in a dialog box procedure. If you have a series of radio buttons, for example, you can check and uncheck the buttons by sending them messages. However, Windows also provides several shortcuts when working with controls in dialog boxes. Let's look at the way in which the dialog box procedure and the child window controls communicate.

The dialog box template for ABOUT2 is shown in the ABOUT2.RC resource script in Figure 11-3. The GROUPBOX control is simply a frame with a title (either Color or Figure) that surrounds each of the two groups of radio buttons. The eight radio buttons in the first group are mutually exclusive, as are the two radio buttons in the second group.

When one of the radio buttons is clicked with the mouse (or when the Spacebar is pressed while the radio button has the input focus), the child window sends its parent a WM_COMMAND message with the low word of wParam set to the ID of the control. The high word of wParam is a notification code, and lParam is the window handle of the control. For a radio button, this notification code is always BN_CLICKED, which equals 0. The dialog box window procedure in Windows then passes this WM_COMMAND message to the dialog box procedure within ABOUT2.C. When the dialog box procedure receives a WM_COMMAND message for one of the radio buttons, it turns on the check mark for that button and turns off the check marks for all the other buttons in the group.

You might recall from Chapter 9 that checking and unchecking a button requires that you send the child window control a BM_CHECK message. To turn on a button check mark, you use

SendMessage (hwndCtrl, BM_SETCHECK, 1, 0) ;

To turn off the check mark, you use

SendMessage (hwndCtrl, BM_SETCHECK, 0, 0) ;

The hwndCtrl parameter is the window handle of the child window button control.

But this method presents a little problem in the dialog box procedure, because you don't know the window handles of all the radio buttons. You know only the one from which you're getting the message. Fortunately, Windows provides you with a function to obtain the window handle of a dialog box control using the dialog box window handle and the control ID:

hwndCtrl = GetDlgItem (hDlg, id) ;

(You can also obtain the ID value of a control from the window handle by using

id = GetWindowLong (hwndCtrl, GWL_ID) ;

but this is rarely necessary.)

You'll notice in the ABOUT2.H header file shown in Figure 11-3 that the ID values for the eight colors are sequential from IDC_BLACK to IDC_WHITE. This arrangement helps in processing the WM_COMMAND messages from the radio buttons. For a first attempt at checking and unchecking the radio buttons, you might try something like the following in the dialog box procedure:

static int iColor ;
[other program lines]
case WM_COMMAND:
     switch (LOWORD (wParam))
     {
     [other program lines]
     case IDC_BLACK:
     case IDC_RED:
     case IDC_GREEN:
     case IDC_YELLOW:
     case IDC_BLUE:
     case IDC_MAGENTA:
     case IDC_CYAN:
     case IDC_WHITE:
          iColor = LOWORD (wParam) ;
 
          for (i = IDC_BLACK, i <= IDC_WHITE, i++)
               SendMessage (GetDlgItem (hDlg, i),
                            BM_SETCHECK, i == LOWORD (wParam), 0) ;
          return TRUE ;
     [other program lines]

This approach works satisfactorily. You've saved the new color value in iColor, and you've also set up a loop that cycles through all the ID values for the eight colors. You obtain the window handle of each of these eight radio button controls and use SendMessage to send each handle a BM_SETCHECK message. The wParam value of this message is set to 1 only for the button that sent the WM_COMMAND message to the dialog box window procedure.

The first shortcut is the special dialog box procedure SendDlgItemMessage:

SendDlgItemMessage (hDlg, id, iMsg, wParam, lParam) ;

It is equivalent to

SendMessage (GetDlgItem (hDlg, id), id, wParam, lParam) ;

Now the loop would look like this:

for (i = IDC_BLACK, i <= IDC_WHITE, i++)
     SendDlgItemMessage (hDlg, i, BM_SETCHECK, i == LWORD (wParam), 0) ;

That's a little better. But the real breakthrough comes when you discover the CheckRadioButton function:

CheckRadioButton (hDlg, idFirst, idLast, idCheck) ;

This function turns off the check marks for all radio button controls with IDs from idFirst to idLast except for the radio button with an ID of idCheck, which is checked. The IDs must be sequential. Now we can get rid of the loop entirely and use:

CheckRadioButton (hDlg, IDC_BLACK, IDC_WHITE, LOWORD (wParam)) ;

That's how it's done in the dialog box procedure in ABOUT2.

A similar shortcut function is provided for working with check boxes. If you create a CHECKBOX dialog window control, you can turn the check mark on and off using the function

CheckDlgButton (hDlg, idCheckbox, iCheck) ;

If iCheck is set to 1, the button is checked; if it's set to 0, the button is unchecked. You can obtain the status of a check box in a dialog box by using

iCheck = IsDlgButtonChecked (hDlg, idCheckbox) ;

You can either retain the current status of the check mark as a static variable within the dialog box procedure or do something like this to toggle the button on a WM_COMMAND message:

CheckDlgButton (hDlg, idCheckbox,
     !IsDlgButtonChecked (hDlg, idCheckbox)) ;

If you define a BS_AUTOCHECKBOX control, you don't need to process the WM_COMMAND message at all. You can simply obtain the current status of the button by using IsDlgButtonChecked before terminating the dialog box. However, if you use the BS_AUTORADIOBUTTON style, IsDlgButtonChecked is not quite satisfactory because you'd need to call it for each radio button until the function returned TRUE. Instead, you'd still trap WM_COMMAND messages to keep track of which button gets pressed.

The OK and Cancel Buttons

ABOUT2 has two push buttons, labeled OK and Cancel. In the dialog box template in ABOUT2.RC, the OK button has an ID of IDOK (defined in WINUSER.H as 1) and the Cancel button has an ID of IDCANCEL (defined as 2). The OK button is the default:

    DEFPUSHBUTTON   "OK",IDOK,35,212,50,14
    PUSHBUTTON      "Cancel",IDCANCEL,113,212,50,14

This arrangement is normal for OK and Cancel buttons in dialog boxes; having the OK button as the default helps out with the keyboard interface. Here's how: Normally, you would end the dialog box by clicking one of these buttons with the mouse or pressing the Spacebar when the desired button has the input focus. However, the dialog box window procedure also generates a WM_COMMAND message when the user presses Enter, regardless of which control has the input focus. The LOWORD of wParam is set to the ID value of the default push button in the dialog box unless another push button has the input focus. In that case, the LOWORD of wParam is set to the ID of the push button with the input focus. If no push button in the dialog box is the default push button, Windows sends the dialog box procedure a WM_COMMAND message with the LOWORD of wParam equal to IDOK. If the user presses the Esc key or Ctrl-Break, Windows sends the dialog box procedure a WM_COMMAND message with the LOWORD of wParam equal to IDCANCEL. So you don't have to add separate keyboard logic to the dialog box procedure, because the keystrokes that normally terminate a dialog box are translated by Windows into WM_COMMAND messages for these two push buttons.

The AboutDlgProc function handles these two WM_COMMAND messages by calling EndDialog:

switch (LWORD (wParam))
{
case IDOK:
     iCurrentColor  = iColor ;
     iCurrentFigure = iFigure ;
     EndDialog (hDlg, TRUE) ;
     return TRUE ;

case IDCANCEL :
     EndDialog (hDlg, FALSE) ;
     return TRUE ;

ABOUT2's window procedure uses the global variables iCurrentColor and iCurrentFigure when drawing the rectangle or ellipse in the program's client area. AboutDlgProc uses the static local variables iColor and iFigure when drawing the figure within the dialog box.

Notice the different value in the second parameter of EndDialog. This is the value that is passed back as the return value from the original DialogBox function in WndProc:

case IDM_ABOUT:
     if (DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc))
          InvalidateRect (hwnd, NULL, TRUE) ;
     return 0 ;

If DialogBox returns TRUE (nonzero), meaning that the OK button was pressed, then the WndProc client area needs to be updated with the new figure and color. These were saved in the global variables iCurrentColor and iCurrentFigure by AboutDlgProc when it received a WM_COMMAND message with the low word of wParam equal to IDOK. If DialogBox returns FALSE, the main window continues to use the original settings of iCurrentColor and iCurrentFigure.

TRUE and FALSE are commonly used in EndDialog calls to signal to the main window procedure whether the user ended the dialog box with OK or Cancel. However, the argument to EndDialog is actually an int, and DialogBox returns an int, so it's possible to return more information in this way than simply TRUE or FALSE.

Avoiding Global Variables

The use of global variables in ABOUT2 may or may not be disturbing to you. Some programmers (myself included) prefer to keep the use of global variables to a bare minimum. The iCurrentColor and iCurrentFigure variables in ABOUT2 certainly seem to qualify as legitimate candidates for global definitions because they must be used in both the window procedure and the dialog procedure. However, a program that has many dialog boxes, each of which can alter the values of several variables, could easily have a confusing proliferation of global variables.

You might prefer to conceive of each dialog box within a program as being associated with a data structure containing all the variables that can be altered by the dialog box. You would define these structures in typedef statements. For example, in ABOUT2 you might define a structure associated with the About box like so:

typedef struct
{
     int iColor, iFigure ;
}
ABOUTBOX_DATA ;

In WndProc, you define and initialize a static variable based on this structure:

static ABOUTBOX_DATA ad = { IDC_BLACK, IDC_RECT } ;

Also in WndProc, replace all occurrences of iCurrentColor and iCurrentFigure with ad.iColor and ad.iFigure. When you invoke the dialog box, use DialogBoxParam rather than DialogBox. This function has a fifth argument that can be any 32-bit value you'd like. Generally, it is set to a pointer to a structure, in this case the ABOUTBOX_DATA structure in WndProc:

case IDM_ABOUT:
     if (DialogBoxParam (hInstance, TEXT ("AboutBox"), 
                         hwnd, AboutDlgProc, &ad))
          InvalidateRect (hwnd, NULL, TRUE) ;
     return 0 ;

Here's the key: the last argument to DialogBoxParam is passed to the dialog procedure as lParam in the WM_INITDIALOG message.

The dialog procedure would have two static variables (a structure and a pointer to a structure) based on the ABOUTBOX_DATA structure:

static ABOUTBOX_DATA ad, * pad ;

In AboutDlgProc this definition replaces the definitions of iColor and iFigure. At the outset of the WM_INITDIALOG message, the dialog procedure sets the values of these two variables from lParam:

pad = (ABOUTBOX_DATA *) lParam ;
ad = * pad ;

In the first statement, pad is set to the lParam pointer. That is, pad actually points to the ABOUTBOX_DATA structure defined in WndProc. The second statement performs a field-by-field structure copy from the structure in WndProc to the local structure in DlgProc.

Now, throughout AboutDlgProc, replace iFigure and iColor with ad.iColor and ad.iFigure except in the code for when the user presses the OK button. In that case, copy the contents of the local structure back to the structure in WndProc:

case IDOK:
     * pad = ad ;
     EndDialog (hDlg, TRUE) ;
     return TRUE ;

Tab Stops and Groups

In Chapter 9, we used window subclassing to add a facility to COLORS1 that let us move from one scroll bar to another by pressing the Tab key. In a dialog box, window subclassing is unnecessary: Windows does all the logic for moving the input focus from one control to another. However, you have to help out by using the WS_TABSTOP and WS_GROUP window styles in the dialog box template. For all controls that you want to access using the Tab key, specify WS_TABSTOP in the window style.

If you refer back to the table, you'll notice that many of the controls include WS_TABSTOP as a default, while others do not. Generally the controls that do not include the WS_TABSTOP style (particularly the static controls) should not get the input focus because they can't do anything with it. Unless you set the input focus to a specific control in a dialog box during processing of the WM_INITDIALOG message and return FALSE from the message, Windows sets the input focus to the first control in the dialog box that has the WS_TABSTOP style.

The second keyboard interface that Windows adds to a dialog box involves the cursor movement keys. This interface is of particular importance with radio buttons. After you use the Tab key to move to the currently checked radio button within a group, you need to use the cursor movement keys to change the input focus from that radio button to other radio buttons within the group. You accomplish this by using the WS_GROUP window style. For a particular series of controls in the dialog box template, Windows will use the cursor movement keys to shift the input focus from the first control that has the WS_GROUP style up to, but not including, the next control that has the WS_GROUP style. Windows will cycle from the last control in a dialog box to the first control, if necessary, to find the end of the group.

By default, the controls LTEXT, CTEXT, RTEXT, and ICON include the WS_GROUP style, which conveniently marks the end of a group. You often have to add WS_GROUP styles to other types of controls.

Look at the dialog box template in ABOUT2.RC. The four controls that have the WS_TABSTOP style are the first radio buttons of each group (explicitly included) and the two push buttons (by default). When you first invoke the dialog box, these are the four controls you can move among using the Tab key.

Within each group of radio buttons, you use the cursor movement keys to change the input focus and the check mark. For example, the first radio button (Black) in the Color group box and the Figure group box have the WS_GROUP style. This means that you can use the cursor movement keys to move the focus from the Black radio button up to, but not including, the Figure group box. Similarly, the first radio button (Rectangle) in the Figure group box and DEFPUSHBUTTON have the WS_GROUP style, so you can use the cursor movement keys to move between the two radio buttons in this group: Rectangle and Ellipse. Both push buttons get the WS_GROUP style to prevent the cursor movement keys from doing anything when the push buttons have the input focus.

When using ABOUT2, the dialog box manager in Windows performs some magic in the two groups of radio buttons. As expected, the cursor movement keys within a group of radio buttons shift the input focus and send a WM_COMMAND message to the dialog box procedure. But when you change the checked radio button within the group, Windows also assigns the newly checked radio button the WS_TABSTOP style. The next time you tab to that group, Windows will set the input focus to the checked radio button.

An ampersand (&) in the text field causes the letter that follows to be underlined and adds another keyboard interface. You can move the input focus to any of the radio buttons by pressing the underlined letter. By pressing C (for the Color group box) or F (for the Figure group box), you can move the input focus to the currently checked radio button in that group.

Although programmers normally let the dialog box manager take care of all this, Windows includes two functions that let you search for the next or previous tab stop or group item. These functions are

hwndCtrl = GetNextDlgTabItem (hDlg, hwndCtrl, bPrevious) ;

and

hwndCtrl = GetNextDlgGroupItem (hDlg, hwndCtrl, bPrevious) ;

If bPrevious is TRUE, the functions return the previous tab stop or group item; if FALSE, they return the next tab stop or group item.

Painting on the Dialog Box

ABOUT2 also does something relatively unusual: it paints on the dialog box. Let's see how this works. Within the dialog box template in ABOUT2.RC, a blank text control is defined with a position and size for the area we want to paint:

LTEXT  ""  IDC_PAINT, 114, 67, 72, 72

This area is 18 characters wide and 9 characters high. Because this control has no text, all that the window procedure for the "static" class does is erase the background when the child window control has to be repainted.

When the current color or figure selection changes or when the dialog box itself gets a WM_PAINT message, the dialog box procedure calls PaintTheBlock, which is a function in ABOUT2.C:

PaintTheBlock (hCtrlBlock, iColor, iFigure) ;

In AboutDlgProc, the window handle hCtrlBlock had been set during the processing of the WM_INITDIALOG message:

hCtrlBlock = GetDlgItem (hDlg, IDD_PAINT) ;

Here's the PaintTheBlock function:

void PaintTheBlock (HWND hCtrl, int iColor, int iFigure)
{
     InvalidateRect (hCtrl, NULL, TRUE) ;
     UpdateWindow (hCtrl) ;
     PaintWindow (hCtrl, iColor, iFigure) ;
}

This invalidates the child window control, generates a WM_PAINT message to the control window procedure, and then calls another function in ABOUT2 called PaintWindow.

The PaintWindow function obtains a device context handle for hCtrl and draws the selected figure, filling it with a colored brush based on the selected color. The size of the child window control is obtained from GetClientRect. Although the dialog box template defines the size of the control in terms of characters, GetClientRect obtains the dimensions in pixels. You can also use the function MapDialogRect to convert the character coordinates in the dialog box to pixel coordinates in the client area.

We're not really painting the dialog box's client area—we're actually painting the client area of the child window control. Whenever the dialog box gets a WM_PAINT message, the child window control is invalidated and then updated to make it believe that its client area is now valid. We then paint on top of it.

Using Other Functions with Dialog Boxes

Most functions that you can use with child windows you can also use with controls in a dialog box. For instance, if you're feeling devious, you can use MoveWindow to move the controls around the dialog box and force the user to chase them around with the mouse.

Sometimes you need to dynamically enable or disable certain controls in a dialog box, depending on the settings of other controls. This call,

EnableWindow (hwndCtrl, bEnable) ;

enables the control when bEnable is TRUE (nonzero) and disables it when bEnable is FALSE (0). When a control is disabled, it receives no keyboard or mouse input. Don't disable a control that has the input focus.

Defining Your Own Controls

Although Windows assumes much of the responsibility for maintaining the dialog box and child window controls, various methods let you slip some of your own code into this process. We've already seen a method that allows you to paint on the surface of a dialog box. You can also use window subclassing (discussed in Chapter 9) to alter the operation of child window controls.

You can also define your own child window controls and use them in a dialog box. For example, suppose you don't particularly care for the normal rectangular push buttons and would prefer to create elliptical push buttons. You can do this by registering a window class and using your own window procedure to process messages for your customized child window. You then specify this window class in Developer Studio in the Properties dialog box associated with a custom control. This translates into a CONTROL statement in the dialog box template. The ABOUT3 program, shown in Figure 11-5, does exactly that.

Figure 11-5. The ABOUT3 program.

ABOUT3.C

/*------------------------------------------
   ABOUT3.C -- About Box Demo Program No. 3
               (c) Charles Petzold, 1998
  ------------------------------------------*/

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
BOOL    CALLBACK AboutDlgProc (HWND, UINT, WPARAM, LPARAM) ;
LRESULT CALLBACK EllipPushWndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("About3") ;
     MSG          msg ;
     HWND         hwnd ;
     WNDCLASS     wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = szAppName ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = EllipPushWndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = NULL ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = TEXT ("EllipPush") ;

     RegisterClass (&wndclass) ;

     hwnd = CreateWindow (szAppName, TEXT ("About Box Demo Program"),
                          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 HINSTANCE hInstance ;
     
     switch (message)
     {
     case WM_CREATE :
          hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;
          return 0 ;
          
     case WM_COMMAND :
          switch (LOWORD (wParam))
          {
          case IDM_APP_ABOUT :
               DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc) ;
               return 0 ;
          }
          break ;
          
     case WM_DESTROY :
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, 
                            WPARAM wParam, LPARAM lParam)

{
     switch (message)
     {
     case WM_INITDIALOG :
          return TRUE ;
          
     case WM_COMMAND :
          switch (LOWORD (wParam))
          {
          case IDOK :
               EndDialog (hDlg, 0) ;
               return TRUE ;
          }
          break ;
     }
     return FALSE ;
}

LRESULT CALLBACK EllipPushWndProc (HWND hwnd, UINT message, 
                                   WPARAM wParam, LPARAM lParam)
{
     TCHAR       szText[40] ;
     HBRUSH      hBrush ;
     HDC         hdc ;
     PAINTSTRUCT ps ;
     RECT        rect ;
     
     switch (message)
     {
     case WM_PAINT :
          GetClientRect (hwnd, &rect) ;
          GetWindowText (hwnd, szText, sizeof (szText)) ;
          
          hdc = BeginPaint (hwnd, &ps) ;
          
          hBrush = CreateSolidBrush (GetSysColor (COLOR_WINDOW)) ;
          hBrush = (HBRUSH) SelectObject (hdc, hBrush) ;
          SetBkColor (hdc, GetSysColor (COLOR_WINDOW)) ;
          SetTextColor (hdc, GetSysColor (COLOR_WINDOWTEXT)) ;
          
          Ellipse (hdc, rect.left, rect.top, rect.right, rect.bottom) ;
          DrawText (hdc, szText, -1, &rect,
                    DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
          
          DeleteObject (SelectObject (hdc, hBrush)) ;
          
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_KEYUP :
          if (wParam != VK_SPACE)
               break ;
                                             // fall through
     case WM_LBUTTONUP :
          SendMessage (GetParent (hwnd), WM_COMMAND,
               GetWindowLong (hwnd, GWL_ID), (LPARAM) hwnd) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

ABOUT3.RC (excerpts)

//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
// Dialog

ABOUTBOX DIALOG DISCARDABLE  32, 32, 180, 100
STYLE DS_MODALFRAME | WS_POPUP
FONT 8, "MS Sans Serif"
BEGIN
    CONTROL         "OK",IDOK,"EllipPush",WS_GROUP | WS_TABSTOP,73,79,32,14
    ICON            "ABOUT3",IDC_STATIC,7,7,20,20
    CTEXT           "About3",IDC_STATIC,40,12,100,8
    CTEXT           "About Box Demo Program",IDC_STATIC,7,40,166,8
    CTEXT           "(c) Charles Petzold, 1998",IDC_STATIC,7,52,166,8
END

/////////////////////////////////////////////////////////////////////////////
// Menu

ABOUT3 MENU DISCARDABLE 
BEGIN
    POPUP "&Help"
    BEGIN
        MENUITEM "&About About3...",            IDM_APP_ABOUT
    END
END

/////////////////////////////////////////////////////////////////////////////
// Icon

ABOUT3                  ICON    DISCARDABLE     "icon1.ico"

RESOURCE.H (excerpts)

// Microsoft Developer Studio generated include file.
// Used by About3.rc

#define IDM_APP_ABOUT                   40001
#define IDC_STATIC                      -1

ABOUT3.ICO

The window class we'll be registering is called EllipPush ("elliptical push button"). In the dialog editor in Developer Studio, delete both the Cancel and OK buttons. To add a control based on this window class, select Custom Control from the Controls toolbar. In the Properties dialog for this control, type EllipPush in the Class field. Rather than a DEFPUSHBUTTON statement appearing in the dialog box template, you'll see a CONTROL statement that specifies this window class:

CONTROL "OK" IDOK, "EllipPush", TABGRP, 64, 60, 32, 14

The dialog box manager uses this window class in a CreateWindow call when creating the child window control in the dialog box.

The ABOUT3.C program registers the EllipPush window class in WinMain:

wndclass.style         = CS_HREDRAW ¦ CS_VREDRAW ;
wndclass.lpfnWndProc   = EllipPushWndProc ;
wndclass.cbClsExtra    = 0 ;
wndclass.cbWndExtra    = 0 ;
wndclass.hInstance     = hInstance ;
wndclass.hIcon         = NULL ;
wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1) ;
wndclass.lpszMenuName  = NULL ;
wndclass.lpszClassName = TEXT ("EllipPush") ;

RegisterClass (&wndclass) ;

The window class specifies that the window procedure is EllipPushWndProc, which is also in ABOUT3.C.

The EllipPushWndProc window procedure processes only three messages: WM_PAINT, WM_KEYUP, and WM_LBUTTONUP. During the WM_PAINT message, it obtains the size of its window from GetClientRect and obtains the text that appears in the push button from GetWindowText. It uses the Windows functions Ellipse and DrawText to draw the ellipse and the text.

The processing of the WM_KEYUP and WM_LBUTTONUP messages is simple:

case WM_KEYUP :
     if (wParam != VK_SPACE)
          break ;
                              // fall through
case WM_LBUTTONUP :
     SendMessage (GetParent (hwnd), WM_COMMAND,
          GetWindowLong (hwnd, GWL_ID), (LPARAM) hwnd) ;
     return 0 ;

The window procedure obtains the handle of its parent window (the dialog box) using GetParent and sends a WM_COMMAND message with wParam equal to the control's ID. The ID is obtained using GetWindowLong. The dialog box window procedure then passes this message on to the dialog box procedure within ABOUT3. The result is a customized push button, as shown in Figure 11-6. You can use this same method to create other customized controls for dialog boxes.

Click to view at full size.

Figure 11-6. A customized push button created by ABOUT3.

Is that all there is to it? Well, not really. EllipPushWndProc is a bare-bones version of the logic generally involved in maintaining a child window control. For instance, the button doesn't flash like normal push buttons. To invert the colors on the interior of the push button, the window procedure would have to process WM_KEYDOWN (from the Spacebar) and WM_LBUTTONDOWN messages. The window procedure should also capture the mouse on a WM_LBUTTONDOWN message and release the mouse (and return the button's interior color to normal) if the mouse is moved outside the child window's client area while the button is still depressed. Only if the button is released while the mouse is captured should the child window send a WM_COMMAND message back to its parent.

EllipPushWndProc also does not process WM_ENABLE messages. As mentioned above, a dialog box procedure can disable a window by using the EnableWindow function. The child window would then display gray rather than black text to indicate that it has been disabled and cannot receive messages.

If the window procedure for a child window control needs to store data that are different for each created window, it can do so by using a positive value of cbWndExtra in the window class structure. This reserves space in the internal window structure that can be accessed by using SetWindowLong and GetWindowLong.