Download the code for this issue
Scrollbar controls are useful for many functions. They can be used to select a value, scroll a window, or set a zoom range. They are typically used for scrolling window or drawing areas, as setting position data is typically done with the trackbar (slider) controls. And scrollbars still seem to have their purpose in Windows. However, would they be more useful if they could be customized?
One problem with using a standard scrollbar in Windows is that you have to handle the WM_HSCROLL and/or WM_VSCROLL messages. These messages are handled in the scrollbar's parent window, and then need to handle the scrollbar position and range to be able to have the parent window update the scrollbar position. So you can see that the scrollbar object itself is not really in control of updating its value and position. The trackbar (slider) control in Windows does handle updating its position and then notifying the parent of the position change (also with the WM_ HSCROLL/WM_ VSCROLL messages). Wouldn't it be nice if the scrollbar behaved more like the trackbar control? While we are talking about limitations with the scrollbars, I have noticed that their range is still limited to integer values. (What year is this?) So if we are going to create a custom control, we should probably extend the range to LONG values.
As a developer, you know that scrollbars do not support owner drawing, such as buttons or listboxes. There is also no provision for custom drawing like a list or tree control. As a matter of fact, the scrollbar controls in Windows are pretty much untouchable as far as customization (other than changing the background color by handling the WM_CTLCOLOR message). So what are the options? You could have some fancy subclassing, but why not just write a custom scrollbar control and have complete control over all drawing, mouse, and functional operations?
When I first thought about this, I, too, put it off due to fear of having to replicate most of the code that already existed for a scrollbar. It seems like a daunting task. I searched the Web for existing code, hoping that someone had already written a custom scrollbar replacement. But even with the help of Google, I was unable to find anything for Win32 or MFC. So I sat down and began charting a course through the unknown waters of writing a custom scrollbar control. I knew I would have to support most, if not all, of the functionality and design of the existing scrollbars horizontal and vertical styles, scroll arrows, thumb scroller, and so on. But as I started jotting down notes, I realized that there were only a few basic things that needed to be handled: drawing, hit-testing, and mouse movement. That sounds like an over-simplification, but it really is the case. And to have any of this happen, the first requirement was to map out the components (and thus, rectangle areas) of a scrollbar.
Custom Scrollbar Design
Let's determine the requirements for a custom scrollbar control. First, it must look and behave like the scrollbar that users are already familiar with. Second, it should support a range of LONG values. Third, it should support custom drawing. This is a given, as we are creating a control from scratch. If we create the control in a true object-oriented design, the draw methods can each be subclassed/overridden to enable new classes to inherit from the scrollbar and customize drawing. Last but not least, the scrollbar should handle updating itself when its position is changed, and simply notify the parent window when the position does change. So with that, let's put together the design.
Step one layout the parts of a scrollbar. A scrollbar is a relatively simple control, mostly because it is rectangular. It has many parts, so there is more work in the hit-testing than, for example, a button control, but if you simplify the areas, it becomes manageable. In a horizontal scrollbar (going from left to right in Figure 1), you have the left arrow button for scrolling, the area to the left of the thumb slider, the thumb slider itself, the area to the right of the thumb slider, and finally the right arrow button. Depending on the position of the thumb slider, the page up/down area to the left or right of the thumb may not exist. The thumb slider is the only part of a scrollbar that moves its position, and we can calculate that position based on the scroll range and current position of the thumb.
The next step is to enable a response to a mouse-down message and detect which area of the scrollbar was affected. You will see various methods in the code that are named "IsMouseOver...()". These helper methods can be used to determine if the mouse is over a specific area in the scrollbar. For instance, IsMouseOverScrollButton() can be called with either of the scroll buttons as a parameter to determine if the mouse was clicked (or is over) one of the scroll buttons at either end of the scrollbar.
The code for the custom scrollbar class (CScrollBarEx, for scrollbar extended) is in the ScrollBarEx.h and ScrollBarEx.cpp files. The class is inherited from a standard CWnd class, which means our class will be able to handle all of its own mouse and drawing operations.
Full source code is provided for the custom scrollbar, as well as sample code to show various custom scrollbars created in a dialog, as shown in Figure 2. Note that although the sample dialog and program only show examples of horizontal scrollbars, the CScrollBarEx class does support vertical scrollbars as well. The source code for this article is available for download online.
Drawing the Scrollbar
Now that the areas have been defined, each part can be drawn. The CScrollBarEx::Draw() method handles the drawing. In turn, this method will call other draw methods to handle each part of the control: the background, scroll areas, arrow buttons, and thumb slider. Note that in the Draw() method, CMemDC is used for the drawing DC. This is an extended CDC object so that drawing can occur offscreen and then be blitted to the screen without flicker. DrawBackground simply fills the entire client rectangle with the background color.
After drawing the background, DrawScrollAreas() is called to handle drawing the area(s) between the arrow buttons and the thumb slider. If the thumb slider is at the minimum or maximum position, there will only be one scroll area to draw. Note that the scroll areas will be drawn, inverting the background pixels, if the mouse is being used to scroll by the page size in the scroll area. If not, the scroll area will simply remain the background area drawing in the DrawBackground() method.
The arrows at either end of the scrollbar can be drawn with the Win32 API call: DrawFrameControl(). This function is useful for a wide variety of drawing controls in Windows. (If you look at the MSDN documentation for DrawFrameControl, you will see just how powerful it is for drawing.) The scrollbar uses it to draw the arrow buttons at the ends of the scrollbar. The DrawButtons() method handles drawing the buttons in our custom class.
To complete the scrollbar drawing, DrawThumbSlider() handles the drawing of the thumb slider. After that, the CMemDC is released, which copies all of the offscreen drawing to the screen in the scrollbar's client rectangle.
The Windows scrollbar also uses a visual indicator to show when a scrollbar has focus. It does not appear often, as scrollbars are typically used as controls on other windows. In this case, the window itself has focus, so the scrollbars will not show their focus state. However, in the case of standalone scrollbar controls, when a scrollbar gets focus, you will see that the thumb slider "flashes" to indicate the control indeed has focus. This is handled in our custom scrollbar control using a timer, which gets set when the WM_SETFOCUS message is received. An internal flag is set, and when the timer is triggered (TIMER_FLASHFOCUS, on 500 ms intervals), the flash flag is toggled and the scrollbar is redrawn. The resulting effect is that the thumb slider is flashed from a background system color of COLOR_3DSHADOW to COLOR_3DFACE.
Mouse Interaction
Now that the parts have been drawn, we need to make them interactive with the mouse. This is not all that difficult, once you know the rectangular areas you have defined. Basically, you will have to track various mouse states:
- When and where the mouse is clicked in the scrollbar (WM_LBUTTONDOWN).
- When the mouse is moving (WM_MOUSEMOVE) and how it affects the scrollbar position and drawing.
- When the mouse button is released (WM_LBUTTONUP), stop all actions on the scrollbar and return to an idle state.
When the control receives a WM_LBUTTONDOWN message, it is a matter of determining the hit-test area beneath the mouse cursor. There are up to five areas that we must check to determine mouse position. They are the same areas that were outlined earlier in the design section: either scroll button, scroll area (to either side of the thumb slider), and the area of the thumb slider. We will store this area in m_ScrollBarClickArea, as the mouse move handling will depend on what area/action is occurring.
When the mouse button is down, you will actively track the mouse position by calling SetCapture(). This will ensure that all WM_MOUSEMOVE messages go to the control, even when the mouse cursor is not over the control. To ensure Windows does not get into an improper state, we need to be sure and call ReleaseCapture() when the WM_LBUTTONUP message is received by the control. So between clicking in the scrollbar and releasing the left mouse button, all the mouse movement will be sent to the control. When the scrollbar thumb is moved, the position is updated accordingly. When the scrollbar position is changed, the WM_SCROLLBARPOSCHANGE custom message is sent to the parent window (WPARAM = the control ID and LPARAM = the scroll position). This message to the parent is simply a notification the parent does not need to act on it like the WM_HSCROLL/WM_VSCROLL messages.
Clicking on one of the arrow buttons moves the position of the scrollbar by the line size. For scrollbars with large ranges, the scrollbar may not show the position differently, at least visually. (If the range is greater than the number of pixels in the scrollbar, then there are not enough pixels to show each position update.) When an arrow button is pressed, EnableScrollTimer() is called to enable the scrollbar timer. If the mouse is held down on an arrow button, after a short delay, the timer is used to repeatedly scroll the scrollbar until the mouse is released. This mimics the standard scrollbar functionality, which behaves the same way.
Clicking in the scroll area between the thumb slider and arrow button is handled the same way as the scroll arrow buttons, but the scrollbar position is moved by the m_PageSize value rather than the m_LineSize value.
You will also notice that the CScrollBarEx class supports the ::EnableScrollBar() method, which is the same as the CScrollBar::EnableScrollBar() method. This allows either, both, or neither of the scrollbar arrow buttons to become disabled. It does not disable the thumb slider or scrollbar, but only affects the buttons at the ends of the scrollbar.
Keyboard Control
In order for the custom scrollbar to handle keyboard control, it must handle the WM_KEYDOWN message in the OnKeyDown() method. The arrow keys are supported, as are the PageUp/PageDown and Home/End keys. This will work if the scrollbar has the current focus and will process the key presses. Or will it? If the scrollbar control is placed in a dialog, we must also remember to handle the WM_GETDLGCODE message and return a value of DLCG_WANTARROWS from the OnGetDlgCode() method. This will allow the arrow keys to be passed to the scrollbar control rather than being handled by the dialog as navigation keys.
Proportional Thumb Slider
The concept of a proportional thumb slider is not difficult. With some basic math, using a proportional thumb slider is just as easy as using a fixed-size thumb slider. If you look at the CScrollBarEx::GetThumbSliderRect() method, this is used to calculate the bounding rectangle of the thumb slider. This calls GetThumbSliderWidth(), which computes the current thumb slider width, depending on the value of the m_ThumbSliderSize variable. This could be one of the following states: THUMBSLIDERSIZE_DEFAULT (the standard size of a thumb slider, using GetSystemMetrics(SM_CXHTHUMB)), THUMBSLIDERSIZE_FIXEDPIXELS (a fixed thumb slider size), or THUMBSLIDERSIZE_RELATIVEPCT (which is the proportional thumb slider).
Customization Methods
Various functions have been added to show how you may customize the scrollbar code. The following methods in the CScrollBarEx code enable some customizations:
SetThumbSliderSize(EThumbSliderSize inThumbSliderSize, short inThumbSliderSizeValue) // For inThumbSliderSize: // THUMBSLIDERSIZE_DEFAULT, // inThumbSliderSizeValue = IGNORED // THUMBSLIDERSIZE_FIXEDPIXELS, // inThumbSliderSizeValue = pixels // THUMBSLIDERSIZE_RELATIVEPCT, // inThumbSliderSizeValue = percent // of total scrollbar pixel size SetThumbStyle(EScrollThumbStyle inThumbStyle) : Sets the thumb style SetBackgroundColor(COLORREF inBackgroundColor): Sets the background color of the scrollbar. SetHotTrackThumb(BOOL bHotTrackThumb) : Sets the hot-tracking ability for the thumb slider.
Future Improvements
Now that I have presented the basics for a custom scrollbar control, here are some thoughts for improvement. First, it could be enhanced to use the XP Theme Manager to make the control look like the standard Windows XP scrollbars.
You could add additional buttons to the end(s) of the scrollbar (next to the arrows), similar to the scrollbars used in the MS Office applications. Maybe you want to change the background from a filled color to an image or pattern. You could subclass a window's existing scrollbar(s) with a CScrollBarEx instance to make them customized. And now that you have control over the entire scrollbar, you may want to customize the cursors to something other than the standard arrow. Finally, porting this code to C# and using GDI+ should not be too tough a task, and may provide good learning experience for those wanting to get into .NET coding.
The possibilities for customization are endless. However, I hope that what I have presented here has taught something about writing a custom control and can perhaps get the sparks of innovation flying for some cool customizations. Please send me an e-mail I'd like to hear about your ideas.
Don Metzler is the president and lead software developer for Barefoot Productions Inc. in Louisville, Colorado. He has been developing software since 1980, and has been programming in C/C++/C# Windows application development since 1990. He has assisted more than two-dozen companies in bringing their commercial Windows application software to market. He has also authored two-dozen programs of his own, released as shareware.