Introduction

Many controls are designed to present some dynamic contents, or larger amount of data which cannot fit into the control or a dialog. In such cases, the controls often need to be equipped with scrollbars so that user can navigate within the data. Standard controls like list view or tree view can serve as prime examples of this approach.

Windows API directly supports scrollbars within every single window and in the todays article we are going to show how to take advantage of it.

Note that in COMCTL32.DLL, Windows also offers a standalone scrollbar control but we won't cover it here. Once you understand the implicit scrollbar support we will talk about, usage of the standlone scrollbar control becomes very simple and straightforward.

Non-Client Area

Before we start talking about the scrollbars, we need to know about the concept of non-client area. In Windows, every window (HWND) distinguishes its client and non-client area. The client area (usually) covers most (or all) of the window on the screen, and actually more or less all fundamental contents of controls is painted in it.

Non-client area is an optional margin around the client area which can be used for some auxiliary content. For top-level windows, this involves the window border, the window caption with the window title and buttons for minimizing, maximizing and closing the window, the menubar and a border around the window.

Also child windows can and quite often take use of the non-client area. In most cases, a simple border and possibly (if needed) the scrollbars are painted in it. Usually (i.e. unless overridden) if the control has a style WS_BORDER or extended style WS_EX_CLIENTEDGE, the control gets the border.

Similarly, if the control decides it needs a scrollbar, Windows reserves more space in the non-client area on top and/or bottom side of the control for the scrollbars.

This behavior for the border and scrollbars is implemented in the function DefWindowProc() which handles many messages:

WM_NCCALCSIZE determines dimensions of the non-client area. The default implementation looks for example at the style WS_BORDER, extended style WS_EX_CLIENTEDGE and state of the scrollbars to do so.

WM_NCxxxx counterparts of various mouse messages together with WM_NCHITTEST handle interactivity of the non-client area. In the case of child control this typically involves reaction on the scrollbar buttons and DefWindowProc() does this for us.

WM_NCPAINT is called to paint the non-client area. Again, handler of the message in DefWindowProc() knows how to paint the border and the scrollbars.

All of this standard behavior can be overridden if you handle these messages in your window procedure, but that's not the what I want to talk about today. For the purpose of scrolling we can stick with the default behavior offered by DefWindowProc().

Setting Up the Scrollbars

Each HWND remembers two sets of few integer values which describe state of both the horizontal and vertical scrollbars. The set corresponds to the members of structure SCROLLINFO (except the auxiliary cbSize and fMask):

Note you are free to choose any units for the scrolling you like. Use whatever suits logic of your control the best. The values may be pixels, count of rows (or columns), amount of lines of text or whatever.

The values nMin and nMax determine range of the scrollbars, i.e. minimal and maximal positions corresponding to the scrollbar's thumb moved to top (or left) and bottom (or right) position. In most cases nMin can be just always set to zero and control updates just the upper limit nMax.

The value nPage describes portion of the contents between nMin and nMax which can be displayed in the control given the size of its client area. Windows also visualizes this value in proportional size of the scrollbars thumb.

The value nPos determines the current scrollbar position, so the control is supposed to paint the corresponding portion of its content.

The value nTrackPos is position of the thumb when it is currently being dragged to a new position. This value is read-only and cannot be directly changed programmatically.

Controls which want to support the scrolling can update these values with function SetScrollInfo(), or alternatively with some less general function like SetScrollPos() or SetScrollRange() which can update only subsets of the values.

Remember that all these setter functions implicitly ensure that nPos and nPage are always in the allowed ranges so that the following conditions hold all the time:

0 <= nPage < nMax - nMin

nMin <= nPos <= nMax - nPage

If it is logically impossible to fulfill the conditions, e.g. because nPage > nMax - nMin, then no scrolling is needed, Windows resets nPos to zero and hides the scrollbar (which results to the resizing of the client area and WM_SIZE message).

This means that you, as a caller of those function, do not need to care too much about the boundary cases. If, for example, you handle reaction to the key [PAGE UP] as scrolling a page up, you simply may do something like this:

// Get current nPos and nPage:int scrollbarId = (isVertical ? SB_VERT : SB_HORZ);
SCROLLINFO si;
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_POS | SIF_PAGE;
GetScrollInfo(hwnd, scrollbarId, &si);
// Set new position one page up.// Note we do not care whether we underflow below nMin as SetScrollInfo()// does that for us automatically.
si.fMask = SIF_POS;
si.nPos -= si.nPage;
SetScrollInfo(hwnd, scrollbarId, &si);
// If we need to count with nPos below, we may need to ask again for the fixed// up value of it:
GetScrollInfo(hwnd, scrollbarId, &si);

Typically, controls supporting the scrolling need to update the state of the scrollbars in the following situations:

Control has to update nMin and/or nMax when amount or size of visible contents of the control changes. E.g. in a case of a control similar to a standard tree-view whenever new (visible) items are added or removed, or when an item is expanded or collapsed.

Control has to update nPage when size of the client area changes (i.e. when handling WM_SIZE) so that it reflects amount of content which can fit in it.

Control has to update nPos when it responds to the scrolling event as described by WM_VSCROLL or WM_HSCROLL. We will cover this more thoroughly later in this article.

Some other situations when the scrollbar state needs to be updated can be when dimension of some elements of the contents changes, e.g. when control starts to use different font which has different size. Often, the amount of related work depends how smartly you choose the scrolling unit: Consider a tree-view control and vertical scrolling: If it uses pixels as the scrolling units, then change of item height (e.g. as a result of WM_SETFONT) has to be reflected by recomputing of the scrollbar's state, but if you use rows as the scrolling units, then it does not.

Little Gotcha

When your control supports both horizontal and vertical scrollbars, there is a little trap. Remember that when setting up e.g. a vertical scrollbar, and the values change so that the scrollbar gets visible or gets hidden, the size of its client area changes.

This change in client area size can result also in the need to update state of the other scrollbar.

Handling WM_VSCROLL and WM_HSCROLL

When the scrollbar is visible (i.e. whenever nMax - nMin > nPage), and user interacts with it e.g. by clicking on a scrolling arrow button or by dragging the thumb, the window procedure gets corresponding non-client mouse messages. When passed to DefWindowProc(), they are translated to messages WM_VSCROLL (for the vertical scrollbar) and WM_HSCROLL (for the horizontal scrollbar).

The control's window procedure is supposed to handle them as follows:

Analyze the action requested by the user.

Update nPos accordingly.

Refresh client area so that the control presents corresponding portion of the contents.

Updating the Client Area

In the code snippet above, we have used the function ScrollWindowEx(). Lets now take a closer look on it.

Painting the client area is task the control usually performs in the handler of message WM_PAINT. In case of control which supports scrolling, the function has to take the current nPos value into consideration. (Or actually two values, nPosHoriz and nPosVert if the control supports scrolling in both directions.)

Typically this means that the control contents is painted with vertical and horizontal offsets, -(nPosVert * uScrollUnitHeight) and -(nPosHoriz * uScrollUnitWidth), so that the control presents content further to the bottom and right when the scrollbars are not in the minimal positions. (uScrollUnitWidth and uScrollUnitHeight determine width and height of the scrolling units in pixels.)

When application changes state of the scrollbar (i.e. the range, the position, or even the page size), it usually needs to repaint itself. It could just invalidate its client area and let WM_PAINT paint everything from scratch.

Or it can do something much smarter. In most cases when scrolling, there is often quite a lot of correctly painted stuff already available on the screen. It's just painted on bad position which corresponds to the old value of nPos, right?

The solution is to simply move all the still valid contents from the old position to the new one, and only invalidate portion of the client area which really needs to be repainted from scratch, i.e. only the area which roughly corresponds to the horizontal or vertical stripe which moves into the visible view-port from "behind the corner" during the scrolling operation.

And that is exactly what the function ScrollWindowEx() is good for. You tell it a rectangle, you tell it a horizontal and vertical offsets (difference between old and new nPos) in pixels, and it does all the magic. It actually copies/moves some graphic memory from one place to another to reuse as much as possible of the old contents, and it only invalidates those portions of the rectangle which really need to be repainted. Assuming the handler of WM_PAINT is implemented as it should and repaints only the dirty rectangle (refer to our 2nd part of this series about painting), it will then have much less work to do.

Scrolling with Keyboard

In many cases it's useful to scroll by appropriate keys on a keyboard. Assuming for example that arrow keys, [HOME], [END], [PAGE DOWN] and [PAGE UP] should translate directly to the scrolling commands, the code can be very simple:

Scrolling with Mouse Wheel

Adding support for scrolling with a mouse wheel is somewhat more interesting. The main reason why it is not that simple is diversity of available hardware. The mouse wheel in many cases is not really a wheel, and often there is no mouse at all. Consider for example modern trackpads which may map some finger gestures to a virtual mouse wheel.

Even among mouses, there are vast differences. As you should know, computers work mainly with numbers. And hence a scrolling the mouse wheel translates to some number which we may call "delta". Depending on the hardware, its driver, system configuration and position of planets in the Solar system, the same action with the mouse wheel can sometimes result in a larger delta coming at once, or a sequence of smaller deltas coming in short succession.

On Windows, the delta propagates as a parameter of the message WM_MOUSEWHEEL for vertical wheel, or WM_MOUSEHWHEEL for horizontal one: It is stored as the high word of WPARAM.

So, to handle these messages, application (or control in our case) has to accumulate the delta until it reaches some threshold value meaning "scroll one line down" (or up; or to left or right for horizontal scrolling).

Furthermore, the control should respect sensitivity of the wheel as configured in the system. On Windows, this settings can be retrieved with SystemParametersInfo(SPI_GETWHEELSCROLLLINES) for vertical wheel and SystemParametersInfo(SPI_GETWHEELSCROLLCHARS) for horizontal one. Both values correspond to the amount of vertical or horizontal scrolling units the control should scroll when the accumulated delta value reaches the value defined with macro WHEEL_DELTA (120).

The above may look quite difficult, but it's not that bad. Furthermore we may actually implement it just once: Windows supports only one mouse pointer and that implies there is never more then one vertical wheel and one horizontal wheel. Therefore we can use global variables for the accumulated values and one wrapping function dealing with them instead of bloating per-control data structures and reimplementing it in each window procedure.

First of all, remember the word "line" in the function name and in names of some variables refers rather to general "scrolling units" and not necessarily any real lines in this context. This naming comes from the standard symbolic names for scrolling one scrolling unit up or down (SB_LINEUP and SB_LINEDOWN), or left or right (SB_LINELEFT and SB_LINERIGHT). Sorry for the terminology mess, but "scrolling units" is simply too much typing for someone as lazy as me...

If the function is used in an application where different HWNDs are living in multiple threads, it has to be thread-safe to protect the state described by the multiple static variables. Hence the use of CRITICAL_SECTION.

For historic reasons, the delta value for the vertical wheel is provided with opposite sign then most people expect, and in the opposite sense in comparison to the horizontal wheel. To simplify the code we deal with that on the single spot: the last line of the function.

The function WheelScrollLines() is quite generic and reusable. Actually one could even think such function should be implemented in some standard Win32API library. That would at least provide better guaranty that mouse wheels are used consistently by default among applications. But AFAIK it is not, at least not a publicly exported one.

Examples for Download

This time, there are two example projects available for download. You may find links to both of them at the very top of this article.

The simpler allows only vertical scrolling, but otherwise corresponds roughly to all the code provided in the article.

Screenshot of the simple demo

The 2nd (and more complex) example does scrolling in vertical as well as horizontal direction, it has a dynamically changing contents so that nMin and nMax change throughout lifetime of the control, it presents usage of non-trivial scrolling units and last but not least, it shows more advanced use of the function ScrollWindowEx() which scrolls only part of the window to keep the headers of columns and rows always visible.

Screenshot of the more complex demo

Real World Code

In this series, it's already tradition to provide also some links to real-life code demonstrating the topic of the article.

To get better insight, you might find very useful to study the (re)implementation of the scrollbar support in Wine. I especially recommend to pay attention to the function SCROLL_SetScrollInfo() which implements core of the SetScrollInfo():

Next Time: More About Non-Client Area

Today, we discussed how to implement scrolling support in a control. During the journey, we have lightly touched the topic of non-client area as that's where the scrollbars are living. Next time, we will take a look how to customize a little bit the non-client area, and how to paint in it.

Comments and Discussions

Dear Martin,
Thank you for such great tutorial, i learned so much about win32 from your articles!
I have a question however, is there any way to compile libmCtrl.lib as static mode without having the need for .dll ?
Thank you,
William

Longer answer: I know there were at least two teams/projects who did it. But in both cases it was very hacky. Therefore this feature was never merged into upstream mCtrl project and likely won't ever be. The main technical problem is that static lib cannot reasonably support resources: You would have somehow to merge mCtrl's resource script with resource script of the hosting .EXE/.DLL (and solve potential conflicts in resource IDs between the two) or use only those mCtrl features which do not need to load any resource.

Great article. Just a question:
Some #includes refers to Wine ("wine/debug.h", "wine/unicode.h", maybe others.). Are they needed for the examples to compile?. Can they be replaced by standard (Windows) debug.h, etc?.
Sorry if stupid question. I know about what Wine is but never used Linux.

The code examples, i.e. the Visual Studio projects linked at the top of the article, do not include anything from Wine, they just include normal headers from standard Microsoft SDK. These show how the API should be used.

The links to Wine code are meant purely for an illustration and further studying, not for compiling or running them. The Wine libraries include their own headers, of course, because it is a reimplementation of standard DLLs like KERNEL32.DLL, USER32.DLL, COMCTL32.DLL, and not at a source of program built on top of Win32API like normal Windows application. I provided it with an intent to offer you a way for studying what the relevant Win32API functions do inside and hence give you better insight. Wine implementation has to do something very close and morally equivalent to the original Microsoft implementation and I point to Wine just because the vanilla Microsoft code is not publicly available. So, if you are not curious how functions like SetScrollInfo() are implemented inside, simply ignore the links to Wine altogether.

Surely i misunderstood the inclusion of Wine examples. My bad. I revisited Mctrl. A great work too. I cant find a link to ask questions there so i must do it here. What is the minimum IE that version 0.11 of Mctrl supports?. It is a shame, but i never actualized my ver 6 IExplorer. In fact, i do use Firefox. Example-html.exe (32 bits version - 64 bit not tested yet) fails to execute, and i do suspect of my older Explorer. Thanks a lot for your patience.

From the point of view of the HTML control, it should work with MSIE as old as 5.01 (IE version deployed originally in Windows 2000). However in that case various HTML and JavaScript features may or may not work due to the limitations of such legacy IE version.