Seeing What You've Drawn
Unfortunately, there is one small problem with Ruler's output: Unless you're running the program on a very high resolution video adapter, you can't see everything it draws. Even on a 1,280-pixel by 1,204-pixel screen, the window can't be stretched wide enough to make all the output visible. What doesn't fit inside the window's client area is clipped by the GDI. You could modify the sample program to make the ruler shorter, but that still wouldn't do much for someone running Windows on a 640-by-480 screen. No, there's a better solution, one that's entirely independent of the screen resolution. That solution is a scroll bar.
Adding a Scroll Bar to a Window
A scroll bar is a window with an arrow at each end and a traveling "thumb" in between that can be dragged with the mouse. Scroll bars can be oriented horizontally or vertically, but never at an angle. When the user clicks one of the scroll bar arrows, moves the thumb, or clicks the scroll bar shaft, the scroll bar informs the window it's attached to by sending it a message. It's up to the window to decide what, if anything, to do with that message because a scroll bar does very little on its own. It doesn't, for example, magically scroll the window's contents. What it does do is provide a very intuitive and universally recognized mechanism for scrolling backward and forward over a virtual landscape that's too large to fit within the physical confines of a window.
Adding a scroll bar to a window is one of the easiest things you'll ever do in a Windows program. To add a vertical scroll bar, create the window with the WS_VSCROLL style. To add a horizontal scroll bar, use the WS_HSCROLL style. To add horizontal and vertical scroll bars, use both WS_VSCROLL and WS_HSCROLL. Recall from Chapter 1 that the third parameter passed to CFrameWnd::Create is the window style, and that the default is WS_OVERLAPPEDWINDOW. An application that creates a conventional frame window with the statement
Create (NULL, _T ("My Application"));
can create a frame window containing a vertical scroll bar with the statement
Create (NULL, _T ("My Application"), WS_OVERLAPPEDWINDOW ¦ WS_VSCROLL);
Accordingly, Windows provides a scroll bar that extends the height of the window's client area from top to bottom on the right side. If you'd rather have the scroll bar appear on the left, include a WS_EX_LEFTSCROLLBAR flag in Create's optional dwExStyle (seventh) parameter.
Setting a Scroll Bar's Range, Position, and Page Size
After you create a scroll bar, you should initialize it with a range, position, and page size. The range is a pair of integers that define the upper and lower limits of the scroll bar's travel. The position is an integer value that specifies the current location within that range; its value is reflected in the position of the scroll bar thumb. The page size sets the size of the thumb to provide a visual representation of the relationship between the size of the window and the size of the scrollable view. For example, if the scroll bar range is 0 to 100 and the page size is 50, the thumb size is half the scroll bar length. If you don't set the page size, Windows picks a default, nonproportional thumb size for you.
One way to set a scroll bar's range and position is with the CWnd::SetScrollRange and CWnd::SetScrollPos functions. The statement
SetScrollRange (SB_VERT, 0, 100, TRUE);
sets a vertical scroll bar's range to 0 through 100, while the statement
SetScrollPos (SB_VERT, 50, TRUE);
sets the current position to 50 and consequently moves the thumb to the middle of the scroll bar. (For horizontal scroll bars, use SB_HORZ instead of SB_VERT.) A scroll bar maintains a record of its current range and position internally. You can query for those values at any time with CWnd::GetScrollRange and CWnd::GetScrollPos.
The TRUE parameter passed to SetScrollRange and SetScrollPos specifies that the scroll bar should be redrawn to reflect the change. You can prevent redraws by specifying FALSE. If you specify neither TRUE nor FALSE, both SetScrollRange and SetScrollPos default to TRUE. You generally want a scroll bar to redraw itself after one of these functions is called, but not if both are called in quick succession. Redrawing a scroll bar twice in a very short period of time produces an undesirable flashing effect. If you're setting the range and the position together, do it like this:
SetScrollRange (SB_VERT, 0, 100, FALSE);
SetScrollPos (SB_VERT, 50, TRUE);
SetScrollPos and SetScrollRange date back to the very first version of Windows. In today's versions, the preferred way to set a scroll bar's range and position is with the CWnd::SetScrollInfo function. In addition to allowing the range and the position to be set with a single function call, SetScrollInfo also provides a means—the only means, as it turns out—for setting the page size. SetScrollInfo accepts three parameters:
An SB_VERT or SB_HORZ parameter that specifies whether the scroll bar is vertical or horizontal (or SB_BOTH if you want to initialize two scroll bars at once)
A pointer to a SCROLLINFO structure
A BOOL value (TRUE or FALSE) that specifies whether the scroll bar should be redrawn
SCROLLINFO is defined as follows in Winuser.h:
typedef struct tagSCROLLINFO
{
UINT cbSize;
UINT fMask;
int nMin;
int nMax;
UINT nPage;
int nPos;
int nTrackPos;
} SCROLLINFO, FAR *LPSCROLLINFO;
cbSize specifies the size of the structure, nMin and nMax specify the scroll bar range, nPage specifies the page size, and nPos specifies the position. nTrackPos is not used in calls to SetScrollInfo, but it returns the scroll bar's thumb position when the complementary GetScrollInfo function is called to retrieve information about the scroll bar while the thumb is being dragged. The fMask field holds a combination of one or more of the following bit flags:
SIF_DISABLENOSCROLL, which disables the scroll bar
SIF_PAGE, which indicates that nPage holds the page size
SIF_POS, which indicates that nPos holds the scroll bar position
SIF_RANGE, which indicates that nMin and nMax hold the scroll bar range
SIF_ALL, which is equivalent to SIF_PAGE ¦ SIF_POS ¦ SIF_RANGE.
SetScrollInfo ignores fields for which bit flags are not specified. The statements
SCROLLINFO si;
si.fMask = SIF_POS;
si.nPos = 50;
SetScrollInfo (SB_VERT, &si, TRUE);
set the position while leaving the range and page size unaffected, and
SCROLLINFO si;
si.fMask = SIF_RANGE ¦ SIF_POS ¦ SIF_PAGE; // Or SIF_ALL
si.nMin = 0;
si.nMax = 99;
si.nPage = 25;
si.nPos = 50;
SetScrollInfo (SB_VERT, &si, TRUE);
sets the range, page size, and position in one operation. You don't need to initialize cbSize before calling SetScrollInfo or GetScrollInfo because MFC initializes it for you.
You can make a scroll bar disappear by setting the upper limit of its range equal to the lower limit. The scroll bar doesn't go away entirely; it's still there, even though you can't see it, and—more important—you can bring it back by making the range upper and lower limits different again. This turns out to be quite a useful trick if you want to hide the scroll bar because the window has been enlarged to the point that a scroll bar is no longer required. SetScrollInfo's SIF_DISABLENOSCROLL flag prevents a scroll bar from accepting further input, but it doesn't make the scroll bar disappear. Having a disabled scroll bar visible inside a window can be confusing to users, who are apt to wonder why the scroll bar is there if it can't be used.
When you set a scroll bar's range, page size, and position, here's a convenient model to keep in mind. Suppose your window's client area is 100 units high and the workspace you want to cover with a vertical scroll bar is 400 units high. Set the scroll bar range to 0-399 and the page size to 100. Accordingly, Windows will draw the scroll bar thumb so that it is one-fourth the height of the scroll bar. When the scroll bar position is 0, the thumb is positioned at the top of the scroll bar. As the thumb is scrolled down, scroll the contents of your window up an amount proportional to the distance the thumb was moved. If you limit the scroll bar's maximum position to 300 (the difference between the magnitude of the scroll bar range and the page size), the bottom of the thumb will reach the bottom of the scroll bar at the same time that the bottom of the workspace scrolls into view at the bottom of the window.
Synchronizing the Thumb Size and the Window Size
Since a scroll bar's thumb size reflects the relative size of the window compared to the width or the height of the virtual workspace, you should update the thumb size when the window size changes. It's easy to do: Just call SetScrollInfo with an SIF_PAGE flag each time your window receives a WM_SIZE message. The first WM_SIZE message comes when a window is created. Subsequent WM_SIZE messages arrive whenever the window's size changes. In MFC, an ON_WM_SIZE entry in a class's message map directs WM_SIZE messages to a handler named OnSize. The handler is prototyped as follows:
afx_msg void OnSize (UINT nType, int cx, int cy)
The nType parameter informs the window whether it has been minimized, maximized, or simply resized by using the code SIZE_MINIMIZED, SIZE_MAXIMIZED, or SIZE_RESTORED, respectively. cx and cy are the client area's new width and height in pixels. If you know the dimensions of your application's virtual workspace, you can set the thumb size accordingly.
Processing Scroll Bar Messages
A scroll bar notifies its owner (the window to which it is attached) of scroll bar events by sending messages. A horizontal scroll bar sends WM_HSCROLL messages, and a vertical scroll bar sends WM_VSCROLL messages. In MFC, these messages are directed to a window's OnHScroll and OnVScroll functions by ON_WM_HSCROLL and ON_WM_VSCROLL entries in the window's message map. Scroll bar message handlers are prototyped like this:
afx_msg void OnHScroll (UINT nCode, UINT nPos, CScrollBar* pScrollBar)
afx_msg void OnVScroll (UINT nCode, UINT nPos, CScrollBar* pScrollBar)
nCode identifies the type of event that precipitated the message; nPos contains the latest thumb position if the thumb is being dragged or was just dragged and released; and, for a scroll bar that was created by adding a WS_HSCROLL or WS_VSCROLL style bit to a window, pScrollBar is NULL.
There are seven different event notifications that an application might receive in OnVScroll's nCode parameter, as shown in the table below.
Event Code
Sent When
SB_LINEUP
The arrow at the top of the scroll bar is clicked.
SB_LINEDOWN
The arrow at the bottom of the scroll bar is clicked.
SB_PAGEUP
The scroll bar shaft is clicked between the up arrow and the thumb.
SB_PAGEDOWN
The scroll bar shaft is clicked between the thumb and down arrow.
SB_ENDSCROLL
The mouse button is released, and no more SB_LINEUP, SB_LINEDOWN, SB_PAGEUP, or SB_PAGEDOWN notifications are forthcoming.
SB_THUMBTRACK
The scroll bar thumb is dragged.
SB_THUMBPOSITION
The thumb is released after being dragged.
Horizontal scroll bars send the same notifications as vertical scroll bars, but the notifications have slightly different meanings. For a horizontal scroll bar, SB_LINEUP signifies that the left arrow was clicked, SB_LINEDOWN means the right arrow was clicked, SB_PAGEUP means the scroll bar was clicked between the left arrow and the thumb, and SB_PAGEDOWN means the scroll bar was clicked between the thumb and the right arrow. If you prefer, you can use SB_LINELEFT, SB_LINERIGHT, SB_PAGELEFT, and SB_PAGERIGHT rather than SB_LINEUP, SB_LINEDOWN, SB_PAGEUP, and SB_PAGEDOWN. The discussions in the remainder of this chapter deal exclusively with vertical scroll bars, but keep in mind that anything said about vertical scroll bars also applies to horizontal scroll bars.
If the user clicks a scroll bar or scroll bar arrow and leaves the mouse button pressed, a series of SB_LINEUP, SB_LINEDOWN, SB_PAGEUP, or SB_PAGEDOWN notifications will arrive in rapid succession—similar to the stream of typematic key codes generated when a key is held down. SB_ENDSCROLL terminates a stream of UP or DOWN notifications and indicates that the mouse button has been released. Even a single click of a scroll bar or scroll bar arrow generates an UP or a DOWN notification followed by an SB_ENDSCROLL notification. Similarly, a window is bombarded with SB_THUMBTRACK notifications that report new thumb positions as a scroll bar thumb is dragged, and it receives an SB_THUMBPOSITION notification when the thumb is released. When an SB_THUMBTRACK or SB_THUMBPOSITION notification arrives, the message's nPos parameter holds the latest thumb position. For other event codes, the value of nPos is undefined.
How your program responds to scroll bar event messages is up to you. Most programs that use scroll bars disregard SB_ENDSCROLL messages and respond to SB_LINEUP, SB_LINEDOWN, SB_PAGEUP, and SB_PAGEDOWN messages instead. A typical response to SB_LINEUP and SB_LINEDOWN messages is to scroll the contents of the window up or down one line and call SetScrollPos or SetScrollInfo to set the new scroll bar position and update the thumb location. "Line" can have whatever physical meaning you want it to have; it might mean 1 pixel, or it might mean the height of one line of text. Similarly, the usual response to SB_PAGEUP and SB_PAGEDOWN messages is to scroll up or down a distance equal to or slightly less than one "page," which is typically defined as the height of the window's client area or slightly less, and to call SetScrollInfo to set the new scroll position. In any event, it's your responsibility to update the scroll bar position. The scroll bar doesn't do that by itself.
Another, though less common, approach to processing UP and DOWN notifications is to continually move the scroll bar thumb by calling SetScrollPos or SetScrollInfo but to defer scrolling the window until an SB_ENDSCROLL notification arrives. I once used this technique in a multimedia application that was relatively slow to respond to positional changes so that the latency of commands sent to a CD-ROM drive wouldn't impede the smooth movement of the scroll bar thumb.
SB_THUMBTRACK and SB_THUMBPOSITION notifications are handled a little differently. Since SB_THUMBTRACK notifications are liable to come fast and furious when the thumb is dragged, some Windows applications ignore SB_THUMBTRACK notifications and respond only to SB_THUMBPOSITION notifications. In this case, the window doesn't scroll until the thumb is released. If you can scroll the contents of your window quickly enough to keep up with SB_THUMBTRACK notifications, you can make your program more responsive to user input by scrolling as the thumb is dragged. It's still up to you to update the scroll bar position each time you scroll the window. Windows animates the movement of the scroll bar thumb as it's dragged up and down, but if you fail to call SetScrollPos or SetScrollInfo in response to SB_THUMBTRACK or SB_THUMBPOSITION notifications, the thumb will snap back to its original position the moment it's released.
Scrolling a Window
Now that you understand how a scroll bar works, it's time to think about how to scroll the contents of a window in response to scroll bar messages.
The simplest approach is to change the scroll bar position each time a scroll bar message arrives and to call CWnd::Invalidate to force a repaint. The window's OnPaint handler can query the scroll bar for its current position and factor that information into its output. Unfortunately, scrolling a window this way is slow—very slow, for that matter. If the user clicks the up arrow to scroll the window contents up one line, it's wasteful to redraw the entire window because most of the information you want to display is already there, albeit in the wrong location. A more efficient approach to processing SB_LINEUP messages is to copy everything currently displayed in the window down one line using a fast block copy and then to draw just the new top line. That's what CWnd::ScrollWindow is for.
ScrollWindow scrolls the contents of a window's client area—in whole or in part—up or down, left or right, by 1 or more pixels using a fast block pixel transfer. Moreover, it invalidates only the part of the window contents that is "uncovered" by the scrolling operation so that the next WM_PAINT message doesn't have to repaint the entire window. If ScrollWindow is called to scroll a window downward by 10 pixels, it performs the scroll by doing a block copy. Then it invalidates the window's top 10 rows. This activates OnPaint and causes only the top 10 rows to be redrawn. Even if OnPaint tries to redraw the contents of the entire client area, performance is improved because most of the output is clipped. A smart OnPaint handler can further boost performance by restricting its GDI calls to those that affect pixels in the window's invalid rectangle. You'll see sample programs in Chapters 10 and 13 that use this technique to optimize scrolling performance.
ScrollWindow accepts four parameters. Two are required and two are optional. The function is prototyped as follows:
void ScrollWindow (int xAmount, int yAmount,
LPCRECT lpRect = NULL, LPCRECT lpClipRect = NULL)
xAmount and yAmount are signed integers that specify the number of pixels to scroll horizontally and vertically. Negative values scroll left and up, while positive values scroll right and down. lpRect points to a CRect object or a RECT structure that specifies the part of the client area to scroll, and lpClipRect points to a CRect object or a RECT structure that specifies a clipping rectangle. Specifying NULL for lpRect and lpClipRect scrolls the contents of the entire client area. The statement
ScrollWindow (0, 10);
scrolls everything in a window's client area downward by 10 pixels and prompts a redraw of the first 10 rows.
You can use ScrollWindow whether your application displays text, graphics, or both. In Windows all things are graphical—including text.