Building a Better Scroll

Because SYSMETS2 is too inefficient a model to be imitated in other programs, let's clean it up. SYSMETS3—our final version of the SYSMETS program in this chapter—is shown in Figure 2-11. This version adds a horizontal scroll bar for left and right scrolling and repaints the client area more efficiently.

SYSMETS3.MAK

#------------------------

# SYSMETS3.MAK make file

#------------------------

sysmets3.exe : sysmets3.obj sysmets3.def

link sysmets3, /align:16, NUL, /nod slibcew libw, sysmets3

rc sysmets3.exe

sysmets3.obj : sysmets3.c sysmets.h

cl -c -Gsw -Ow -W2 -Zp sysmets3.c

SYSMETS3.C

/*----------------------------------------------------

SYSMETS3.C -- System Metrics Display Program No. 3

(c) Charles Petzold, 1990

----------------------------------------------------*/

#include <windows.h>

#include "sysmets.h"

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,

LPSTR lpszCmdLine, int nCmdShow)

{

static char szAppName[] = "SysMets3" ;

HWND hwnd ;

MSG msg ;

WNDCLASS wndclass ;

if (!hPrevInstance)

{

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 = GetStockObject (WHITE_BRUSH) ;

wndclass.lpszMenuName = NULL ;

wndclass.lpszClassName = szAppName ;

RegisterClass (&wndclass) ;

}

hwnd = CreateWindow (szAppName, "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, nCmdShow) ;

UpdateWindow (hwnd) ;

while (GetMessage (&msg, NULL, 0, 0))

{

TranslateMessage (&msg) ;

DispatchMessage (&msg) ;

}

return msg.wParam ;

}

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)

{

static short cxChar, cxCaps, cyChar, cxClient, cyClient, nMaxWidth,

nVscrollPos, nVscrollMax, nHscrollPos, nHscrollMax ;

char szBuffer[10] ;

HDC hdc ;

short i, x, y, nPaintBeg, nPaintEnd, nVscrollInc, nHscrollInc ;

PAINTSTRUCT ps ;

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) ;

nMaxWidth = 40 * cxChar + 18 * cxCaps ;

return 0 ;

case WM_SIZE :

cyClient = HIWORD (lParam) ;

cxClient = LOWORD (lParam) ;

nVscrollMax = max (0, NUMLINES + 2 - cyClient / cyChar) ;

nVscrollPos = min (nVscrollPos, nVscrollMax) ;

SetScrollRange (hwnd, SB_VERT, 0, nVscrollMax, FALSE) ;

SetScrollPos (hwnd, SB_VERT, nVscrollPos, TRUE) ;

nHscrollMax = max (0, 2 + (nMaxWidth - cxClient) / cxChar) ;

nHscrollPos = min (nHscrollPos, nHscrollMax) ;

SetScrollRange (hwnd, SB_HORZ, 0, nHscrollMax, FALSE) ;

SetScrollPos (hwnd, SB_HORZ, nHscrollPos, TRUE) ;

return 0 ;

case WM_VSCROLL :

switch (wParam)

{

case SB_TOP :

nVscrollInc = -nVscrollPos ;

break ;

case SB_BOTTOM :

nVscrollInc = nVscrollMax - nVscrollPos ;

break ;

case SB_LINEUP :

nVscrollInc = -1 ;

break ;

case SB_LINEDOWN :

nVscrollInc = 1 ;

break ;

case SB_PAGEUP :

nVscrollInc = min (-1, -cyClient / cyChar) ;

break ;

case SB_PAGEDOWN :

nVscrollInc = max (1, cyClient / cyChar) ;

break ;

case SB_THUMBTRACK :

nVscrollInc = LOWORD (lParam) - nVscrollPos ;

break ;

default :

nVscrollInc = 0 ;

}

if (nVscrollInc = max (-nVscrollPos,

min (nVscrollInc, nVscrollMax - nVscrollPos)))

{

nVscrollPos += nVscrollInc ;

ScrollWindow (hwnd, 0, -cyChar * nVscrollInc, NULL, NULL) ;

SetScrollPos (hwnd, SB_VERT, nVscrollPos, TRUE) ;

UpdateWindow (hwnd) ;

}

return 0 ;

case WM_HSCROLL :

switch (wParam)

{

case SB_LINEUP :

nHscrollInc = -1 ;

break ;

case SB_LINEDOWN :

nHscrollInc = 1 ;

break ;

case SB_PAGEUP :

nHscrollInc = -8 ;

break ;

case SB_PAGEDOWN

nHscrollInc = 8 ;

break ;

case SB_THUMBPOSITION :

nHscrollInc = LOWORD (lParam) - nHscrollPos ;

break ;

default :

nHscrollInc = 0 ;

}

if (nHscrollInc = max (-nHscrollPos,

min (nHscrollInc, nHscrollMax - nHscrollPos)))

{

nHscrollPos += nHscrollInc ;

ScrollWindow (hwnd, -cxChar * nHscrollInc, 0, NULL, NULL) ;

SetScrollPos (hwnd, SB_HORZ, nHscrollPos, TRUE) ;

}

return 0 ;

case WM_PAINT :

hdc = BeginPaint (hwnd, &ps) ;

nPaintBeg = max (0, nVscrollPos + ps.rcPaint.top / cyChar - 1) ;

nPaintEnd = min (NUMLINES,

nVscrollPos + ps.rcPaint.bottom / cyChar) ;

for (i = nPaintBeg ; i < nPaintEnd ; i++)

{

x = cxChar * (1 - nHscrollPos) ;

y = cyChar * (1 - nVscrollPos + i) ;

TextOut (hdc, x, y,

sysmetrics[i].szLabel,

lstrlen (sysmetrics[i].szLabel)) ;

TextOut (hdc, x + 18 * cxCaps, y,

sysmetrics[i].szDesc,

lstrlen (sysmetrics[i].szDesc)) ;

SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

TextOut (hdc, x + 18 * cxCaps + 40 * cxChar, y,

szBuffer,

wsprintf (szBuffer, "%5d",

GetSystemMetrics (sysmetrics[i].nIndex))) ;

SetTextAlign (hdc, TA_LEFT | TA_TOP) ;

}

EndPaint (hwnd, &ps) ;

return 0 ;

case WM_DESTROY :

PostQuitMessage (0) ;

return 0 ;

}

return DefWindowProc (hwnd, message, wParam, lParam) ;

}

SYSMETS3.DEF

;-------------------------------------

; SYSMETS3.DEF module definition file

;-------------------------------------

NAME SYSMETS3

DESCRIPTION 'System Metrics Display No. 3 (c) Charles Petzold, 1990'

EXETYPE WINDOWS

STUB 'WINSTUB.EXE'

CODE PRELOAD MOVEABLE DISCARDABLE

DATA PRELOAD MOVEABLE MULTIPLE

HEAPSIZE 1024

STACKSIZE 8192

EXPORTS WndProc

These are the improvements in SYSMETS3 and how they are implemented in the program:

You can no longer scroll the display so that the last line appears at the top of the client area. You can scroll only far enough to see the last line at the bottom of the client area. This requires that the program calculate a new scroll bar range (and possibly a new thumb position) when it processes a WM_SIZE message. The WM_SIZE logic calculates the scroll bar range based on the number of lines of text, the width of the text, and the size of the client area. This approach results in a smaller range—only that necessary to bring into view the text that falls outside the client area.

This offers an interesting dividend. Suppose that the client area of the window is large enough to display the entire text with top and bottom margins. In this case, both the minimum position and maximum position of the scroll bar range will equal zero. What will Windows do with this information? It will remove the scroll bar from the window! It's no longer needed. Similarly, if the client area is wide enough to show the full 60-column width of the text, no horizontal scroll bar is displayed in the window.

The WM_VSCROLL and WM_HSCROLL messages are processed by first calculating an increment of the scroll bar position for each value of wParam. This value is then used to scroll the existing contents of the window using the Windows ScrollWindow call. This function has the following format:

ScrollWindow (hwnd, xInc, yInc, lpRect, lpClipRect) ;

The xInc and yInc values specify an amount to scroll in pixels. In SYSMETS3, the lpRect and lpClipRect values are set to NULL to specify that the entire client area should be scrolled. Windows 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 procedure 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.)

The WM_PAINT processing now determines which lines are within the invalid rectangle and rewrites only those lines. It does this by analyzing the top and bottom coordinates of the invalid rectangle stored in the PAINTSTRUCT structure. The program paints only those text lines within the invalid rectangle. The code is more complex, but it is much faster.

Because WM_PAINT was speeded up, I decided to let SYSMETS3 process SB_THUMBTRACK operations for WM_VSCROLL messages. Previously, the program would ignore SB_THUMBTRACK messages (which occur as the user drags the scroll bar thumb) and would act only on SB- THUMBPOSITION messages, which occur when the user stops dragging the thumb. The WM_VSCROLL code also calls UpdateWindow to update the client area immediately. When you move the thumb on the vertical scroll bar, SYSMETS3 will continually scroll and update the client area. I'll let you decide whether SYSMETS3 (and Windows) is fast enough to justify this change.