VC++ 6.0 ebook chapter index
Free counters!

Scroll Bars

Scroll bars are one of the best features of a graphical user interface. They are easy to use and provide excellent visual feedback. You can use scroll bars whenever you need to display anything—text, graphics, a spreadsheet, database records, pictures, Web pages—that requires more space than is available in the window's client area.

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

Click to view at full size.

Figure 4-7. The vertical scroll bar.

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

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

Windows takes care of processing all mouse messages to the scroll bars. However, scroll bars do not have an automatic keyboard interface. If you want the cursor keys to duplicate some of the functionality of the scroll bars, you must explicitly provide logic for that (as we'll do when we make another version of the SYSMETS program in the next chapter).

Scroll Bar Range and Position

Every scroll bar has an associated "range" and "position." The scroll bar range is a pair of integers representing a minimum and maximum value associated with the scroll bar. The position is the location of the thumb within the range. When the thumb is at the top (or left) of the scroll bar, the position of the thumb is the minimum value of the range. At the bottom (or right) of the scroll bar, the thumb position is the maximum value of the range.

By default, the range of a scroll bar is 0 (top or left) through 100 (bottom or right), but it's easy to change the range to something that is more convenient for the program:

SetScrollRange (hwnd, iBar, iMin, iMax, bRedraw) ;

The iBar argument is either SB_VERT or SB_HORZ, iMin and iMax are the new minimum and maximum positions of the range, and you set bRedraw to TRUE if you want Windows to redraw the scroll bar based on the new range. (If you will be calling other functions that affect the appearance of the scroll bar after you call SetScrollRange, you'll probably want to set bRedraw to FALSE to avoid excessive redrawing.)

The thumb position is always a discrete integral value. For instance, a scroll bar with a range of 0 through 4 has five thumb positions, as shown in Figure 4-8.

Click to view at full size.

Figure 4-8. Scroll bars with five thumb positions.

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

SetScrollPos (hwnd, iBar, iPos, bRedraw) ;

The iPos argument is the new position and must be within the range of iMin and iMax. Windows provides similar functions (GetScrollRange and GetScrollPos) to obtain the current range and position of a scroll bar.

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

These are the responsibilities of your program:

Like almost everything in life, this will make a lot more sense when we start looking at some code.

Scroll Bar Messages

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

Like all messages, WM_VSCROLL and WM_HSCROLL are accompanied by the wParam and lParam message parameters. For messages from scroll bars created as part of your window, you can ignore lParam; that's used only for scroll bars created as child windows, usually within dialog boxes.

The wParam message parameter is divided into a low word and a high word. The low word of wParam is a number that indicates what the mouse is doing to the scroll bar. This number is referred to as a "notification code." Notification codes have values defined by identifiers that begin with SB, which stands for "scroll bar." Here's how the notification codes are defined in WINUSER.H:

#define SB_LINEUP           0
#define SB_LINELEFT         0
#define SB_LINEDOWN         1
#define SB_LINERIGHT        1
#define SB_PAGEUP           2
#define SB_PAGELEFT         2
#define SB_PAGEDOWN         3
#define SB_PAGERIGHT        3
#define SB_THUMBTRACK       5
#define SB_TOP              6
#define SB_LEFT             6
#define SB_BOTTOM           7
#define SB_RIGHT            7
#define SB_ENDSCROLL        8

You use the identifiers containing the words LEFT and RIGHT for horizontal scroll bars, and the identifiers with UP, DOWN, TOP, and BOTTOM with vertical scroll bars. The notification codes associated with clicking the mouse on various areas of the scroll bar are shown in Figure 4-9.

Click to view at full size.

Figure 4-9. Identifiers for the wParam values of scroll bar messages.

If you hold down the mouse button on the various parts of the scroll bar, your program can receive multiple scroll bar messages. When the mouse button is released, you'll get a message with a notification code of SB_ENDSCROLL. You can generally ignore messages with the SB_ENDSCROLL notification code. Windows will not change the position of the scroll bar thumb. Your application does that by calling SetScrollPos.

When you position the mouse cursor over the scroll bar thumb and press the mouse button, you can move the thumb. This generates scroll bar messages with notification codes of SB_THUMBTRACK and SB_THUMBPOSITION. When the low word of wParam is SB_THUMBTRACK, the high word of wParam is the current position of the scroll bar thumb as the user is dragging it. This position is within the minimum and maximum values of the scroll bar range. When the low word of wParam is SB_THUMBPOSITION, the high word of wParam is the final position of the scroll bar thumb when the user released the mouse button. For other scroll bar actions, the high word of wParam should be ignored.

To provide feedback to the user, Windows will move the scroll bar thumb when you drag it with the mouse as your program is receiving SB_THUMBTRACK messages. However, unless you process SB_THUMBTRACK or SB_THUMBPOSITION messages by calling SetScrollPos, the thumb will snap back to its original position when the user releases the mouse button.

A program can process either the SB_THUMBTRACK or SB_THUMBPOSITION messages, but doesn't usually process both. If you process SB_THUMBTRACK messages, you'll move the contents of your client area as the user is dragging the thumb. If instead you process SB_THUMBPOSITION messages, you'll move the contents of the client area only when the user stops dragging the thumb. It's preferable (but more difficult) to process SB_THUMBTRACK messages; for some types of data your program may have a hard time keeping up with the messages.

As you'll note, the WINUSER.H header files includes notification codes of SB_TOP, SB_BOTTOM, SB_LEFT, and SB_RIGHT, indicating that the scroll bar has been moved to its minimum or maximum position. However, you will never receive these notification codes for a scroll bar created as part of your application window.

Although it's not common, using 32-bit values for the scroll bar range is perfectly valid. However, the high word of wParam, which is only a 16-bit value, cannot properly indicate the position for SB_THUMBTRACK and SB_THUMBPOSITION actions. In this case, you need to use the function GetScrollInfo (described later in this chapter) to get this information.

Scrolling SYSMETS

Enough explanation. It's time to put this stuff into practice. Let's start simply. We'll begin with vertical scrolling because that's what we desperately need. The horizontal scrolling can wait. SYSMET2 is shown in Figure 4-10. This program is probably the simplest implementation of a scroll bar you'll want in an application.

Figure 4-10. The SYSMETS2 program.


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

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


int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
     static TCHAR szAppName[] = TEXT ("SysMets2") ;
     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 = (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 ("Get System Metrics No. 2"),
                          WS_OVERLAPPEDWINDOW | WS_VSCROLL,
                          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 int  cxChar, cxCaps, cyChar, cyClient, iVscrollPos ;
     HDC         hdc ;
     int         i, y ;
     TCHAR       szBuffer[10] ;
     TEXTMETRIC  tm ;

     switch (message)
     case WM_CREATE:
          hdc = GetDC (hwnd) ;

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

          ReleaseDC (hwnd, hdc) ;

          SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ;
          SetScrollPos   (hwnd, SB_VERT, iVscrollPos, TRUE) ;
          return 0 ;

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

     case WM_VSCROLL:
          switch (LOWORD (wParam))
          case SB_LINEUP:
               iVscrollPos -= 1 ;
               break ;
          case SB_LINEDOWN:
               iVscrollPos += 1 ;
               break ;
          case SB_PAGEUP:
               iVscrollPos -= cyClient / cyChar ;
               break ;
          case SB_PAGEDOWN:
               iVscrollPos += cyClient / cyChar ;
               break ;
          case SB_THUMBPOSITION:
               iVscrollPos = HIWORD (wParam) ;
               break ;
          default :
               break ;

          iVscrollPos = max (0, min (iVscrollPos, NUMLINES - 1)) ;

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

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;
          for (i = 0 ; i < NUMLINES ; i++)
               y = cyChar * (i - iVscrollPos) ;
               TextOut (hdc, 0, y,
                        lstrlen (sysmetrics[i].szLabel)) ;
               TextOut (hdc, 22 * cxCaps, y,
                        lstrlen (sysmetrics[i].szDesc)) ;
               SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
               TextOut (hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer,
                        wsprintf (szBuffer, TEXT ("%5d"),
                                  GetSystemMetrics (sysmetrics[i].iIndex))) ;
               SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
          EndPaint (hwnd, &ps) ;
          return 0 ;

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

The new CreateWindow call adds a vertical scroll bar to the window by including the WS_VSCROLL window style in the third argument:


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

SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ;
SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ;

The sysmetrics structure array has NUMLINES lines of text, so the scroll bar range is set to 0 through NUMLINES - 1. Each position of the scroll bar corresponds to a line of text displayed at the top of the client area. If the scroll bar thumb is at position 0, the first line will be positioned at the top of the client area. For positions greater than zero, other lines appear at the top. When the position is NUMLINES - 1, the last line of text appears at the top of the client area.

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

After the program calculates a new value of iVscrollPos based on the type of WM_VSCROLL message it receives, it makes sure that it is still between the minimum and maximum range value of the scroll bar by using the min and max macros. The program then compares the value of iVscrollPos with the previous position, which is obtained by calling GetScrollPos. If the scroll position has changed, it is updated by calling SetScrollPos, and the entire window is invalidated by a call to InvalidateRect.

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

cyChar * i

In SYSMETS2, the formula is

cyChar * (i - iVscrollPos)

The loop still displays NUMLINES lines of text, but for nonzero values of iVscrollPos this value is negative. The program is actually displaying the early lines of text above and outside the client area. Windows, of course, doesn't allow these lines to appear on the screen, so everything looks all nice and neat.

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

Structuring Your Program for Painting

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

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

At first, you may rebel at this dictum because it seems such a roundabout way of doing things. In the early days of Windows, programmers found this concept difficult to master because it was so different from character-mode PC programming. And, as I mentioned earlier, there are frequently times when your program will respond to some keyboard or mouse logic by drawing something immediately. This is done for both convenience and efficiency. But in many cases it's simply unnecessary. After you master the discipline of accumulating all the information you need to paint in response to a WM_PAINT message, you'll be pleased with the results.

As SYSMETS2 demonstrates, a program will often determine that it must repaint a particular area of the display while processing a message other than WM_PAINT. This is where InvalidateRect comes in handy. You can use it to invalidate specific rectangles of the client area or the entire client area.

Simply marking areas of the window as invalid to generate WM_PAINT messages might not be entirely satisfactory in some applications. After you make an InvalidateRect call, Windows places a WM_PAINT message in the message queue and the window procedure eventually processes it. However, Windows treats WM_PAINT messages as low priority, so if a lot of other activity is occurring in the system, it may be awhile before your window procedure receives the WM_PAINT message. Everyone has seen blank, white "holes" in Windows after a dialog box is removed and the program is still waiting to refresh its window.

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

UpdateWindow (hwnd) ;

UpdateWindow causes the window procedure to be called immediately with a WM_PAINT message if any part of the client area is invalid. (UpdateWindow will not call the window procedure if the entire client area is valid.) In this case, the WM_PAINT message bypasses the message queue. The window procedure is called directly from Windows. When the window procedure has finished repainting, it exits and the UpdateWindow function returns control to the code that called it.

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