VC++ 6.0 ebook chapter index
Free counters!

The Scroll Bar Class

When the subject of scroll bars first came up in Chapter 4, I discussed some of the differences between "window scroll bars" and "scroll bar controls." The SYSMETS programs use window scroll bars, which appear at the right and bottom of the window. You add window scroll bars to a window by including the identifier WS_VSCROLL or WS_HSCROLL or both in the window style when creating the window. Now we're ready to make some scroll bar controls, which are child windows that can appear anywhere in the client area of the parent window. You create child window scroll bar controls by using the predefined window class "scrollbar" and one of the two scroll bar styles SBS_VERT and SBS_HORZ.

Unlike the button controls (and the edit and list box controls to be discussed later), scroll bar controls do not send WM_COMMAND messages to the parent window. Instead, they send WM_VSCROLL and WM_HSCROLL messages, just like window scroll bars. When processing the scroll bar messages, you can differentiate between window scroll bars and scroll bar controls by the lParam parameter. It will be 0 for window scroll bars and the scroll bar window handle for scroll bar controls. The high and low words of the wParam parameter have the same meaning for window scroll bars and scroll bar controls.

Although window scroll bars have a fixed width, Windows uses the full rectangle dimensions given in the CreateWindow call (or later in the MoveWindow call) to size the scroll bar controls. You can make long, thin scroll bar controls or short, pudgy scroll bar controls.

If you want to create scroll bar controls that have the same dimensions as window scroll bars, you can use GetSystemMetrics to obtain the height of a horizontal scroll bar:

GetSystemMetrics (SM_CYHSCROLL) ;

or the width of a vertical scroll bar:

GetSystemMetrics (SM_CXVSCROLL) ;

The scroll bar window style identifiers SBS_LEFTALIGN, SBS_RIGHTALIGN, SBS_TOP ALIGN, and SBS_BOTTOMALIGN are documented to give standard dimensions to scroll bars. However, these styles work only for scroll bars in dialog boxes.

You can set the range and position of a scroll bar control with the same calls used for window scroll bars:

SetScrollRange (hwndScroll, SB_CTL, iMin, iMax, bRedraw) ;
SetScrollPos (hwndScroll, SB_CTL, iPos, bRedraw) ;
SetScrollInfo (hwndScroll, SB_CTL, &si, bRedraw) ;

The difference is that window scroll bars use a handle to the main window as the first parameter and SB_VERT or SB_HORZ as the second parameter.

Amazingly enough, the system color named COLOR_SCROLLBAR is no longer used for scroll bars. The end buttons and thumb are based on COLOR_BTNFACE, COLOR_BTNHILIGHT, COLOR_BTNSHADOW, COLOR_BTNTEXT (for the little arrows), COLOR_DKSHADOW, and COLOR_BTNLIGHT. The large area between the two end buttons is based on a combination of COLOR_BTNFACE and COLOR_BTNHIGHLIGHT.

If you trap WM_CTLCOLORSCROLLBAR messages, you can return a brush from the message to override the color used for this area. Let's do it.

The COLORS1 Program

To see some uses of scroll bars and static child windows—and also to explore color in more depth—we'll use the COLORS1 program, shown in Figure 9-5. COLORS1 displays three scroll bars in the left half of the client area labeled "Red," "Green," and "Blue." As you scroll the scroll bars, the right half of the client area changes to the composite color indicated by the mix of the three primary colors. The numeric values of the three primary colors are displayed under the three scroll bars.

Figure 9-5. The COLORS1 program.


   COLORS1.C -- Colors Using Scroll Bars
                (c) Charles Petzold, 1998

#include <windows.h>


int     idFocus ;
WNDPROC OldScroll[3] ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
     static TCHAR szAppName[] = TEXT ("Colors1") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;
          = 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 = CreateSolidBrush (0) ;
     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 ("Color Scroll"),
                          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 COLORREF crPrim[3] = { RGB (255, 0, 0), RGB (0, 255, 0),
                                   RGB (0, 0, 255) } ;
     static HBRUSH  hBrush[3], hBrushStatic ;
     static HWND    hwndScroll[3], hwndLabel[3], hwndValue[3], hwndRect ;
     static int     color[3], cyChar ;
     static RECT    rcColor ;
     static TCHAR * szColorLabel[] = { TEXT ("Red"), TEXT ("Green"), 
                                       TEXT ("Blue") } ;
     HINSTANCE      hInstance ;
     int            i, cxClient, cyClient ;
     TCHAR          szBuffer[10] ;
     switch (message)
     case WM_CREATE :
          hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ;
               // Create the white-rectangle window against which the 
               // scroll bars will be positioned. The child window ID is 9.
          hwndRect = CreateWindow (TEXT ("static"), NULL,
                                   WS_CHILD | WS_VISIBLE | SS_WHITERECT,
                                   0, 0, 0, 0,
                                   hwnd, (HMENU) 9, hInstance, NULL) ;
          for (i = 0 ; i < 3 ; i++)
                    // The three scroll bars have IDs 0, 1, and 2, with
                    // scroll bar ranges from 0 through 255.
               hwndScroll[i] = CreateWindow (TEXT ("scrollbar"), NULL,
                                             WS_CHILD | WS_VISIBLE | 
                                             WS_TABSTOP | SBS_VERT,
                                             0, 0, 0, 0, 
                                             hwnd, (HMENU) i, hInstance, NULL) ;
               SetScrollRange (hwndScroll[i], SB_CTL, 0, 255, FALSE) ;
               SetScrollPos   (hwndScroll[i], SB_CTL, 0, FALSE) ;
                    // The three color-name labels have IDs 3, 4, and 5, 
                    // and text strings "Red", "Green", and "Blue".
               hwndLabel [i] = CreateWindow (TEXT ("static"), szColorLabel[i],
                                             WS_CHILD | WS_VISIBLE | SS_CENTER,
                                             0, 0, 0, 0, 
                                             hwnd, (HMENU) (i + 3), 
                                             hInstance, NULL) ;
                    // The three color-value text fields have IDs 6, 7, 
                    // and 8, and initial text strings of "0".
               hwndValue [i] = CreateWindow (TEXT ("static"), TEXT ("0"),
                                             WS_CHILD | WS_VISIBLE | SS_CENTER,
                                             0, 0, 0, 0,
                                             hwnd, (HMENU) (i + 6), 
                                             hInstance, NULL) ;
               OldScroll[i] = (WNDPROC) SetWindowLong (hwndScroll[i], 
                                             GWL_WNDPROC, (LONG) ScrollProc) ;
               hBrush[i] = CreateSolidBrush (crPrim[i]) ;
          hBrushStatic = CreateSolidBrush (
                              GetSysColor (COLOR_BTNHIGHLIGHT)) ;
          cyChar = HIWORD (GetDialogBaseUnits ()) ;
          return 0 ;
     case WM_SIZE :
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;

          SetRect (&rcColor, cxClient / 2, 0, cxClient, cyClient) ;
          MoveWindow (hwndRect, 0, 0, cxClient / 2, cyClient, TRUE) ;
          for (i = 0 ; i < 3 ; i++)
               MoveWindow (hwndScroll[i],
                           (2 * i + 1) * cxClient / 14, 2 * cyChar,
                           cxClient / 14, cyClient - 4 * cyChar, TRUE) ;
               MoveWindow (hwndLabel[i],
                           (4 * i + 1) * cxClient / 28, cyChar / 2,
                           cxClient / 7, cyChar, TRUE) ;
               MoveWindow (hwndValue[i],
                           (4 * i + 1) * cxClient / 28, 
                           cyClient - 3 * cyChar / 2,
                           cxClient / 7, cyChar, TRUE) ;
          SetFocus (hwnd) ;
          return 0 ;
     case WM_SETFOCUS :
          SetFocus (hwndScroll[idFocus]) ;
          return 0 ;
     case WM_VSCROLL :
          i = GetWindowLong ((HWND) lParam, GWL_ID) ;
          switch (LOWORD (wParam))
          case SB_PAGEDOWN :
               color[i] += 15 ;
                                             // fall through
          case SB_LINEDOWN :
               color[i] = min (255, color[i] + 1) ;
               break ;
          case SB_PAGEUP :
               color[i] -= 15 ;
                                             // fall through
          case SB_LINEUP :
               color[i] = max (0, color[i] - 1) ;
               break ;
          case SB_TOP :
               color[i] = 0 ;
               break ;
          case SB_BOTTOM :
               color[i] = 255 ;
               break ;
          case SB_THUMBPOSITION :
          case SB_THUMBTRACK :
               color[i] = HIWORD (wParam) ;
               break ;
          default :
               break ;
          SetScrollPos  (hwndScroll[i], SB_CTL, color[i], TRUE) ;
          wsprintf (szBuffer, TEXT ("%i"), color[i]) ;
          SetWindowText (hwndValue[i], szBuffer) ;
          DeleteObject ((HBRUSH) 
               SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) 
                    CreateSolidBrush (RGB (color[0], color[1], color[2])))) ;
          InvalidateRect (hwnd, &rcColor, TRUE) ;
          return 0 ;
          i = GetWindowLong ((HWND) lParam, GWL_ID) ;
          return (LRESULT) hBrush[i] ;
          i = GetWindowLong ((HWND) lParam, GWL_ID) ;
          if (i >= 3 && i <= 8)    // static text controls
               SetTextColor ((HDC) wParam, crPrim[i % 3]) ;
               SetBkColor ((HDC) wParam, GetSysColor (COLOR_BTNHIGHLIGHT));
               return (LRESULT) hBrushStatic ;
          break ;
          DeleteObject (hBrushStatic) ;
          hBrushStatic = CreateSolidBrush (GetSysColor (COLOR_BTNHIGHLIGHT)) ;
          return 0 ;

     case WM_DESTROY :
          DeleteObject ((HBRUSH)
               SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) 
                    GetStockObject (WHITE_BRUSH))) ;
          for (i = 0 ; i < 3 ; i++)
               DeleteObject (hBrush[i]) ;
          DeleteObject (hBrushStatic) ;
          PostQuitMessage (0) ;
          return 0 ;
     return DefWindowProc (hwnd, message, wParam, lParam) ;
LRESULT CALLBACK ScrollProc (HWND hwnd, UINT message, 
                             WPARAM wParam, LPARAM lParam)
     int id = GetWindowLong (hwnd, GWL_ID) ;
     switch (message)
     case WM_KEYDOWN :
          if (wParam == VK_TAB)
               SetFocus (GetDlgItem (GetParent (hwnd), 
                    (id + (GetKeyState (VK_SHIFT) < 0 ? 2 : 1)) % 3)) ;
          break ;
     case WM_SETFOCUS :
          idFocus = id ;
          break ;
     return CallWindowProc (OldScroll[id], hwnd, message, wParam, lParam) ;

COLORS1 puts its children to work. The program uses 10 child window controls: 3 scroll bars, 6 windows of static text, and 1 static rectangle. COLORS1 traps WM_CTLCOLORSCROLLBAR messages to color the interior sections of the three scroll bars red, green, and blue and traps WM_CTLCOLORSTATIC messages to color the static text.

You can scroll the scroll bars using either the mouse or the keyboard. You can use COLORS1 as a development tool in experimenting with color and choosing attractive (or, if you prefer, ugly) colors for your own Windows programs. The COLORS1 display is shown in Figure 9-6, unfortunately reduced to gray shades for the printed page.

Click to view at full size.

Figure 9-6. The COLORS1 display.

COLORS1 doesn't process WM_PAINT messages. Virtually all of the work in COLORS1 is done by the child windows.

The color shown in the right half of the client area is actually the window's background color. A static child window with style SS_WHITERECT blocks out the left half of the client area. The three scroll bars are child window controls with the style SBS_VERT. These scroll bars are positioned on top of the SS_WHITERECT child. Six more static child windows of style SS_CENTER (centered text) provide the labels and the color values. COLORS1 creates its normal overlapped window and the 10 child windows within the WinMain function using CreateWindow. The SS_WHITERECT and SS_CENTER static windows use the window class "static"; the three scroll bars use the window class "scrollbar."

The x position, y position, width, and height parameters of the CreateWindow calls are initially set to 0 because the position and sizing depend on the size of the client area, which is not yet known. COLORS1's window procedure resizes all 10 child windows using MoveWindow when it receives a WM_SIZE message. So whenever you resize the COLORS1 window, the size of the scroll bars changes proportionally.

When the WndProc window procedure receives a WM_VSCROLL message, the high word of the lParam parameter is the handle to the child window. We can use GetWindowWord to get the window ID number:

i = GetWindowLong ((HWND) lParam, GWL_ID) ;

For the three scroll bars, we have conveniently set the ID numbers to 0, 1, and 2, so WndProc can tell which scroll bar is generating the message.

Because the handles to the child windows were saved in arrays when the windows were created, WndProc can process the scroll bar message and set the new value of the appropriate scroll bar using the SetScrollPos call:

SetScrollPos (hwndScroll[i], SB_CTL, color[i], TRUE) ;

WndProc also changes the text of the child window at the bottom of the scroll bar:

wsprintf (szBuffer, TEXT ("%i"), color[I]) ;
SetWindowText (hwndValue[i], szBuffer) ;

The Automatic Keyboard Interface

Scroll bar controls can also process keystrokes, but only if they have the input focus. The following table shows how keyboard cursor keys translate into scroll bar messages:

Cursor Key Scroll Bar Message wParam Value
Left or Up SB_LINEUP
Right or Down SB_LINEDOWN

In fact, the SB_TOP and SB_BOTTOM scroll bar messages can be generated only by using the keyboard. If you want a scroll bar control to obtain the input focus when the scroll bar is clicked with the mouse, you must include the WS_TABSTOP identifier in the window class parameter of the CreateWindow call. When a scroll bar has the input focus, a blinking gray block is displayed on the scroll bar thumb.

To provide a full keyboard interface to the scroll bars, however, more work is necessary. First the WndProc window procedure must specifically give a scroll bar the input focus. It does this by processing the WM_SETFOCUS message, which the parent window receives when it obtains the input focus. WndProc simply sets the input focus to one of the scroll bars:

SetFocus (hwndScroll[idFocus]) ;

where idFocus is a global variable.

But you also need some way to get from one scroll bar to another by using the keyboard, preferably by using the Tab key. This is more difficult, because once a scroll bar has the input focus it processes all keystrokes. But the scroll bar cares only about the cursor keys; it ignores the Tab key. The way out of this dilemma lies in a technique called "window subclassing." We'll use it to add a facility to COLORS1 to jump from one scroll bar to another using the Tab key.

Window Subclassing

The window procedure for the scroll bar controls is somewhere inside Windows. However, you can obtain the address of this window procedure by a call to GetWindowLong using the GWL_WNDPROC identifier as a parameter. Moreover, you can set a new window procedure for the scroll bars by calling SetWindowLong. This technique, which is called "window subclassing," is very powerful. It lets you hook into existing window procedures, process some messages within your own program, and pass all other messages to the old window procedure.

The window procedure that does preliminary scroll bar message processing in COLORS1 is named ScrollProc; it is toward the end of the COLORS1.C listing. Because ScrollProc is a function within COLORS1 that is called by Windows, it must be defined as a CALLBACK.

For each of the three scroll bars, COLORS1 uses SetWindowLong to set the address of the new scroll bar window procedure and also obtain the address of the existing scroll bar window procedure:

OldScroll[i] = (WNDPROC) SetWindowLong (hwndScroll[i], GWL_WNDPROC,
                                       (LONG) ScrollProc)) ;

Now the function ScrollProc gets all messages that Windows sends to the scroll bar window procedure for the three scroll bars in COLORS1 (but not, of course, for scroll bars in other programs). The ScrollProc window procedure simply changes the input focus to the next (or previous) scroll bar when it receives a Tab or Shift-Tab keystroke. It calls the old scroll bar window procedure using CallWindowProc.

Coloring the Background

When COLORS1 defines its window class, it gives the background of its client area a solid black brush:

wndclass.hbrBackground = CreateSolidBrush (0) ;

When you change the settings of COLORS1's scroll bars, the program must create a new brush and put the new brush handle in the window class structure. Just as we were able to get and set the scroll bar window procedure using GetWindowLong and SetWindowLong, we can get and set the handle to this brush using GetClassWord and SetClassWord.

You can create the new brush and insert the handle in the window class structure and then delete the old brush:

DeleteObject ((HBRUSH)
     SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG)
          CreateSolidBrush (RGB (color[0], color[1], color[2])))) ;

The next time Windows recolors the background of the window, Windows will use this new brush. To force Windows to erase the background, we invalidate the right half of the client area:

InvalidateRect (hwnd, &rcColor, TRUE) ;

The TRUE (nonzero) value as the third parameter indicates that we want the background erased before repainting.

InvalidateRect causes Windows to put a WM_PAINT message in the message queue of the window procedure. Because WM_PAINT messages are low priority, this message will not be processed immediately if you are still moving the scroll bar with the mouse or the cursor keys. Alternatively, if you want the window to be updated immediately after the color is changed, you can add the statement

UpdateWindow (hwnd) ;

after the InvalidateRect call. But this might slow down keyboard and mouse processing.

COLORS1's WndProc function doesn't process the WM_PAINT message but passes it to DefWindowProc. Windows' default processing of WM_PAINT messages simply involves calling BeginPaint and EndPaint to validate the window. Because we specified in the InvalidateRect call that the background should be erased, the BeginPaint call causes Windows to generate a WM_ERASEBKGND (erase background) message. WndProc ignores this message also. Windows processes it by erasing the background of the client area using the brush specified in the window class.

It's always a good idea to clean up before termination, so during processing of the WM_DESTROY message, DeleteObject is called once more:

DeleteObject ((HBRUSH)
     SetClassLong (hwnd, GCL_HBRBACKGROUND,
          (LONG) GetStockObject (WHITE_BRUSH))) ;

Coloring the Scroll Bars and Static Text

In COLORS1, the interiors of the three scroll bars and the text in the six text fields are colored red, green, and blue. The coloring of the scroll bars is accomplished by processing WM_CTLCOLORSCROLLBAR messages.

In WndProc we define a static array of three handles to brushes:

static HBRUSH hBrush [3] ;

During processing of WM_CREATE, we create the three brushes:

for (I = 0 ; I < 3 ; I++)
     hBrush[0] = CreateSolidBrush (crPrim [I]) ;

where the crPrim array contains the RGB values of the three primary colors. During the WM_CTLCOLORSCROLLBAR processing, the window procedure returns one of these three brushes:

     i = GetWindowLong ((HWND) lParam, GWL_ID) ;
     return (LRESULT) hBrush [i] ;

These brushes must be destroyed during processing of the WM_DESTROY message:

for (i = 0 ; i < 3 ; i++)
     DeleteObject (hBrush [i])) ;

The text in the static text fields is colored similarly by processing the WM_CTLCOLORSTATIC message and calling SetTextColor. The text background is set using SetBkColor with the system color COLOR_BTNHIGHLIGHT. This causes the text background to be the same color as the static rectangle control behind the scrollbars and text displays. For static text controls, this text background color applies only to the rectangle behind each character in the string and not to the entire width of the control window. To accomplish this, the window procedure must also return a handle to a brush of the COLOR_BTNHIGHLIGHT color. This brush is named hBrushStatic; it is created during the WM_CREATE message and destroyed during the WM_DESTROY message.

By creating a brush based on the COLOR_BTNHIGHLIGHT color during the WM_CREATE message and using it through the duration of the program, we've exposed ourselves to a little problem. If the COLOR_BTNHIGHLIGHT color is changed while the program is running, the color of the static rectangle will change and the text background color will change but the whole background of the text window controls will remain the old COLOR_BTNHIGHLIGHT color.

To fix this problem, COLORS1 also processes the WM_SYSCOLORCHANGE message by simply recreating hBrushStatic using the new color.