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

Building a Better Scroll

SYSMETS2 works well, but it's too inefficient a model to be imitated in other programs. Soon I'll present a new version that corrects its deficiencies. Most interesting, perhaps, is that this new version will not use any of the four scroll bar functions discussed so far. Instead, it will use new functions unique to the Win32 API.

The Scroll Bar Information Functions

The scroll bar documentation (in /Platform SDK/User Interface Services/Controls/Scroll Bars) indicates that the SetScrollRange, SetScrollPos, GetScrollRange, and GetScrollPos functions are "obsolete." This is not entirely accurate. While these functions have been around since Windows 1.0, they were upgraded to handle 32-bit arguments in the Win32 API. They are still perfectly functional and are likely to remain functional. Moreover, they are simple enough not to overwhelm a newcomer to Windows programming at the outset, which is why I continue to use them in this book.

The two scroll bar functions introduced in the Win32 API are called SetScrollInfo and GetScrollInfo. These functions do everything the earlier functions do and add two new important features.

The first feature involves the size of the scroll bar thumb. As you may have noticed, the size of the thumb was constant in the SYSMETS2 program. However, in some Windows applications you may have used, the size of the thumb is proportional to the amount of the document displayed in the window. This displayed amount is known as the "page size." In arithmetic terms,
Thumb size Page size Amount of document displayed

=
=
Scroll length Range Total size of document

You can use SetScrollInfo to set the page size (and hence the size of the thumb), as we'll see in the SYSMETS3 program coming up shortly.

The GetScrollInfo function adds a second important feature, or rather it corrects a deficiency in the current API. Suppose you want to use a range that is 65,536 or more units. Back in the days of 16-bit Windows, this was not possible. In Win32, of course, the functions are defined as accepting 32-bit arguments, and indeed they do. (Keep in mind that if you do use a range this large, the number of actual physical positions of the thumb is still limited by the pixel size of the scroll bar.) However, when you get a WM_VSCROLL or WM_HSCROLL message with a notification code of SB_THUMBTRACK or SB_THUMBPOSITION, only 16 bits are provided to indicate the current position of the thumb. The GetScrollInfo function lets you obtain the actual 32-bit value.

The syntax of the SetScrollInfo and GetScrollInfo functions is

SetScrollInfo (hwnd, iBar, &si, bRedraw) ;
GetScrollInfo (hwnd, iBar, &si) ;

The iBar argument is either SB_VERT or SB_HORZ, as in the other scroll bar functions. As with those functions also, it can be SB_CTL for a scroll bar control. The last argument for SetScrollInfo can be TRUE or FALSE to indicate if you want Windows to redraw the scroll bar taking into account the new information.

The third argument to both functions is a SCROLLINFO structure, which is defined like so:

typedef struct tagSCROLLINFO
{
     UINT cbSize ;     // set to sizeof (SCROLLINFO)
     UINT fMask ;      // values to set or get
     int  nMin ;       // minimum range value
     int  nMax ;       // maximum range value
     UINT nPage ;      // page size
     int  nPos ;       // current position
     int  nTrackPos ;  // current tracking position
}
SCROLLINFO, * PSCROLLINFO ;

In your program, you can define a structure of type SCROLLINFO like this:

SCROLLINFO si ;

Before calling SetScrollInfo or GetScrollInfo, you must set the cbSize field to the size of the structure:

si.cbSize = sizeof (si) ;

or

si.cbSize = sizeof (SCROLLINFO) ;

As you get acquainted with Windows, you'll find several other structures that have a first field like this one to indicate the size of the structure. This field allows for a future version of Windows to expand the structure and add new features while still being compatible with previously compiled programs.

You set the fMask field to one or more flags beginning with the SIF prefix. You can combine these flags with the C bitwise OR function (|).

When you use the SIF_RANGE flag with the SetScrollInfo function, you must set the nMin and nMax fields to the desired scroll bar range. When you use the SIF_RANGE flag with the GetScrollInfo function, the nMin and nMax fields will be set to the current range on return from the function.

The SIF_POS flag is similar. When used with the SetScrollInfo function, you must set the nPos field of the structure to the desired position. You use the SIF_POS flag with GetScrollInfo to obtain the current position.

The SIF_PAGE flag lets you set and obtain the page size. You set nPage to the desired page size with the SetScrollInfo function. GetScrollInfo with the SIF_PAGE flag lets you obtain the current page size. Don't use this flag if you don't want a proportional scroll bar thumb.

You use the SIF_TRACKPOS flag only with GetScrollInfo while processing a WM_VSCROLL or WM_HSCROLL message with a notification code of SB_THUMBTRACK or SB_THUMBPOSITION. On return from the function, the nTrackPos field of the SCROLLINFO structure will indicate the current 32-bit thumb position.

You use the SIF_DISABLENOSCROLL flag only with the SetScrollInfo function. If this flag is specified and the new scroll bar arguments would normally render the scroll bar invisible, this scroll renders the scroll bar disabled instead. (I'll explain this more shortly.)

The SIF_ALL flag is a combination of SIF_RANGE, SIF_POS, SIF_PAGE, and SIF_TRACKPOS. This is handy when setting the scroll bar arguments during a WM_SIZE message. (The SIF_TRACKPOS flag is ignored when specified in a SetScrollInfo function.) It's also handy when processing a scroll bar message.

How Low Can You Scroll?

In SYSMETS2, the scrolling range is set to a minimum of 0 and a maximum of NUMLINES - 1. When the scroll bar position is 0, the first line of information is at the top of the client area; when the scroll bar position is NUMLINES - 1, the last line is at the top of the client area and no other lines are visible.

You could say that SYSMETS2 scrolls too far. It really only needs to scroll far enough so that the last line of information appears at the bottom of the client area rather than at the top. We could make some changes to SYSMETS2 to accomplish this. Rather than set the scroll bar range when we process the WM_CREATE message, we could wait until we receive the WM_SIZE message:

iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ;
SetScrollRange (hwnd, SB_VERT, 0, iVscrollMax, TRUE) ;

Suppose NUMLINES equals 75, and suppose for a particular window size that cyClient divided by cyChar equals 50. In other words, we have 75 lines of information but only 50 can fit in the client area at any time. Using the two lines of code shown above, the range is set to a minimum of 0 and a maximum of 25. When the scroll bar position equals 0, the program displays lines 0 through 49. When the scroll bar position equals 1, the program displays lines 1 through 50; and when the scroll bar position equals 25 (the maximum), the program displays lines 25 through 74. Obviously we'd have to make changes to other parts of the program, but this is entirely doable.

One nice feature of the new scroll bar functions is that when you use a scroll bar page size, much of this logic is done for you. Using the SCROLLINFO structure and SetScrollInfo, you'd have code that looked something like this:

si.cbSize = sizeof (SCROLLINFO) ;
si.cbMask = SIF_RANGE | SIF_PAGE ;
si.nMin   = 0 ;
si.nMax   = NUMLINES - 1 ;
si.nPage  = cyClient / cyChar ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;

When you do this, Windows limits the maximum scroll bar position not to si.nMax but to si.nMax - si.nPage + 1. Let's make the same assumptions as earlier: NUMLINES equals 75 (so si.nMax equals 74), and si.nPage equals 50. This means that the maximum scroll bar position is limited to 74 - 50 + 1, or 25. This is exactly what we want.

What happens when the page size is as large as the scroll bar range? That is, in this example, what if nPage is 75 or above? Windows conveniently hides the scroll bar because it's no longer needed. If you don't want the scroll bar to be hidden, use SIF_DISABLENOSCROLL when calling SetScrollInfo and Windows will merely disable the scroll bar rather than hide it.

The New SYSMETS

SYSMETS3—our final version of the SYSMETS program in this chapter—is shown in Figure 4-11. This version uses the SetScrollInfo and GetScrollInfo functions, adds a horizontal scroll bar for left and right scrolling, and repaints the client area more efficiently.

Figure 4-11. The SYSMETS3 program.

SYSMETS3.C

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

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

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

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

     hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 3"),
                          WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, 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, cxClient, cyClient, iMaxWidth ;
     HDC         hdc ;
     int         i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ;
     PAINTSTRUCT ps ;
     SCROLLINFO  si ;
     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) ;

               // Save the width of the three columns
          
          iMaxWidth = 40 * cxChar + 22 * cxCaps ;
          return 0 ;

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

               // Set vertical scroll bar range and page size

          si.cbSize = sizeof (si) ;
          si.fMask  = SIF_RANGE | SIF_PAGE ;
          si.nMin   = 0 ;
          si.nMax   = NUMLINES - 1 ;
          si.nPage  = cyClient / cyChar ;
          SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;

               // Set horizontal scroll bar range and page size

          si.cbSize = sizeof (si) ;
          si.fMask  = SIF_RANGE | SIF_PAGE ;
          si.nMin   = 0 ;
          si.nMax   = 2 + iMaxWidth / cxChar ;
          si.nPage  = cxClient / cxChar ;
          SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ;
          return 0 ;
          
     case WM_VSCROLL:
               // Get all the vertical scroll bar information

          si.cbSize = sizeof (si) ;
          si.fMask  = SIF_ALL ;
          GetScrollInfo (hwnd, SB_VERT, &si) ;

               // Save the position for comparison later on

          iVertPos = si.nPos ;

          switch (LOWORD (wParam))
          {
          case SB_TOP:
               si.nPos = si.nMin ;
               break ;
               
          case SB_BOTTOM:
               si.nPos = si.nMax ;
               break ;
               
          case SB_LINEUP:
               si.nPos -= 1 ;
               break ;
               
          case SB_LINEDOWN:
               si.nPos += 1 ;
               break ;
               
          case SB_PAGEUP:
               si.nPos -= si.nPage ;
               break ;

          case SB_PAGEDOWN:
               si.nPos += si.nPage ;
               break ;
               
          case SB_THUMBTRACK:
               si.nPos = si.nTrackPos ;
               break ;
               
          default:
               break ;         
          }
               // Set the position and then retrieve it.  Due to adjustments
               //   by Windows it may not be the same as the value set.

          si.fMask = SIF_POS ;
          SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
          GetScrollInfo (hwnd, SB_VERT, &si) ;

               // If the position has changed, scroll the window and update it

          if (si.nPos != iVertPos)
          {                    
               ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos), 
                                   NULL, NULL) ;
               UpdateWindow (hwnd) ;
          }
          return 0 ;
          
     case WM_HSCROLL:
               // Get all the vertical scroll bar information

          si.cbSize = sizeof (si) ;
          si.fMask  = SIF_ALL ;

               // Save the position for comparison later on

          GetScrollInfo (hwnd, SB_HORZ, &si) ;
          iHorzPos = si.nPos ;

          switch (LOWORD (wParam))
          {
          case SB_LINELEFT:
               si.nPos -= 1 ;
               break ;
               
          case SB_LINERIGHT:
               si.nPos += 1 ;
               break ;
               
          case SB_PAGELEFT:
               si.nPos -= si.nPage ;
               break ;
               
          case SB_PAGERIGHT:
               si.nPos += si.nPage ;
               break ;
               
          case SB_THUMBPOSITION:
               si.nPos = si.nTrackPos ;
               break ;
               
          default :
               break ;
          }
               // Set the position and then retrieve it.  Due to adjustments
               //   by Windows it may not be the same as the value set.

          si.fMask = SIF_POS ;
          SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ;
          GetScrollInfo (hwnd, SB_HORZ, &si) ;
          
               // If the position has changed, scroll the window 

          if (si.nPos != iHorzPos)
          {
               ScrollWindow (hwnd, cxChar * (iHorzPos - si.nPos), 0, 
                             NULL, NULL) ;
          }
          return 0 ;

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

               // Get vertical scroll bar position

          si.cbSize = sizeof (si) ;
          si.fMask  = SIF_POS ;
          GetScrollInfo (hwnd, SB_VERT, &si) ;
          iVertPos = si.nPos ;

               // Get horizontal scroll bar position
          GetScrollInfo (hwnd, SB_HORZ, &si) ;
          iHorzPos = si.nPos ;

               // Find painting limits

          iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ;
          iPaintEnd = min (NUMLINES - 1,
                           iVertPos + ps.rcPaint.bottom / cyChar) ;
          
          for (i = iPaintBeg ; i <= iPaintEnd ; i++)
          {
               x = cxChar * (1 - iHorzPos) ;
               y = cyChar * (i - iVertPos) ;
               
               TextOut (hdc, x, y,
                        sysmetrics[i].szLabel,
                        lstrlen (sysmetrics[i].szLabel)) ;
               
               TextOut (hdc, x + 22 * cxCaps, y,
                        sysmetrics[i].szDesc,
                        lstrlen (sysmetrics[i].szDesc)) ;
               
               SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
               
               TextOut (hdc, x + 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) ;
}

This version of the program relies on Windows to maintain the scroll bar information and do a lot of the bounds checking. At the beginning of WM_VSCROLL and WM_HSCROLL processing, it obtains all the scroll bar information, adjusts the position based on the notification code, and then sets the position by calling SetScrollInfo. The program then calls GetScrollInfo. If the position was out of range in the SetScrollInfo call, the position is corrected by Windows and the correct value is returned in the GetScrollInfo call.

SYSMETS3 uses the ScrollWindow function to scroll information in the window's client area rather than repaint it. Although the function is rather complex (and has been superseded in recent versions of Windows by the even more complex ScrollWindowEx), SYSMETS3 uses it in a fairly simple way. The second argument to the function gives an amount to scroll the client area horizontally in pixels, and the third argument is an amount to scroll the client area vertically.

The last two arguments to ScrollWindow are set to NULL. This indicates that the entire client area is to be scrolled. Windows automatically invalidates the rectangle in the client area "uncovered" by the scrolling operation. This generates a WM_PAINT message. InvalidateRect is no longer needed. Note that ScrollWindow is not a GDI function because it does not require a handle to a device context. It is one of the few non-GDI Windows functions that changes the appearance of the client area of a window. Rather peculiarly but conveniently, it is documented along with the scroll bar functions.

The WM_HSCROLL processing traps the SB_THUMBPOSITION notification code and ignores SB_THUMBTRACK. Thus, if the user drags the thumb on the horizontal scroll bar, the program will not scroll the contents of the window horizontally until the user releases the mouse button.

The WM_VSCROLL strategy is different: here, the program traps SB_THUMBTRACK messages and ignores SB_THUMBPOSITION. Thus, the program scrolls its contents vertically in direct response to the user dragging the thumb on the vertical scroll bar. This is considered preferable, but watch out: It is well known that when users find out a program scrolls in direct response to dragging the scroll bar thumb, they will frenetically jerk the thumb back and forth trying to bring the program to its knees. Fortunately, today's fast PCs are much more likely to survive this torture test. But try your code out on a slow machine, and perhaps think about using the SB_SLOWMACHINE argument to GetSystemMetrics for alternative processing for slow machines.

One way to speed up WM_PAINT processing is illustrated by SYSMETS3: The WM_PAINT code determines which lines are within the invalid rectangle and rewrites only those lines. The code is more complex, of course, but it is faster.

But I Don't Like to Use the Mouse

In the early days of Windows, a significant number of users didn't care for using the mouse, and indeed, Windows itself (and many Windows programs) did not require a mouse. Although mouseless PCs have now generally gone the way of monochrome displays and dot-matrix printers, it is still recommended that you write programs that duplicate mouse operations with the keyboard. This is particularly true for something as fundamental as scroll bars, because our keyboards have a whole array of cursor movement keys that should offer alternatives to the mouse.

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