Last year (2009), I needed a 'owner draw' scroll bar for UltiMate Grid I was using in my project. My first thought was to subclass
CScrollBar and do my own painting, this turned out to be impossible. My next option was to search the internet for a custom scroll bar, but soon found that no one had bothered to create it, at least not for free, all the scroll bars I could find could not be used to replace the standard windows scroll bars because they did not behave in the way a standard scroll bar does. That is why I created this scroll bar I present here in this article.
My mission was simple, to create a custom scroll bar control that behaved exactly like a standard windows scroll bar but having a 'custom' look. I mean how hard can it be? As it turns out, not very hard but a lot of work, far more than I originally intended.
Many rarely used features of the standard Windows scroll bar are implemented in this scroll bar, including the context menu. I have taken great care to implement every little thing the standard scroll bar does. If you see something I've omitted or done incorrectly, please let me know. I've also implemented some features not found in the standard Windows scroll bar, e.g. mouse wheel support.
Using the Code
It should be noted that
CXeScrollBar cannot be used to replace scroll bars in windows created with
WS_VSCROLL window styles, the reason is that those scroll bars are part of that window non-client area, i.e., the scroll bars are not child windows but rather a part of the parent window.
CXeScrollBar can be used in place of the standard windows scroll bar (
CScrollBar) in any situation except as noted above. Keep in mind though that
CXeScrollBar is dependant on MFC.
Using this code in your own project is very simple. Just add XeScrollBar.h, XeScrollBar.cpp, XeScrollBarBase.h, XeScrollBar.cpp and XeScrollBar.rc to your project, copy the eight bitmap files (XSB_?_???.bmp) to the 'res' folder of your project. In your code, create
CXeScrollBar object in same way you would create
CScrollBar. The following code is an example of how the
Create member is used.
m_sbH.Create( WS_CHILD | WS_VISIBLE | SBS_HORZ, CRect(11,265,400,282), this, IDC_SB_H );
m_sbV.Create( WS_CHILD | WS_VISIBLE | SBS_VERT, CRect(419,11,436,260), this, IDC_SB_V );
Another common use of
CXeScrollBar is on a form or in a dialog box, in that case, the scroll bar(s) already exist and need to be replaced. The following code shows how you can replace existing scroll bars with
CXeScrollBar. Typically you would do that in
OnInitialUpdate() for a form view or in
OnInitDialog() for a dialog. It's best to use the
CreateFromExisting member to do this because that function takes care of all the boring details involved when creating a window to replace another; Window style, Control ID, size, position, scroll range, scroll position and scroll page size are copied. Z-order (Tab order) is also preserved, that is very important if the existing scroll bar has a
WS_TABSTOP style set because when a new child window is created, it is placed last in the Z-order of child windows (of parent window).
m_sbH.CreateFromExisting( this, IDC_XE_SB_H );
m_sbV.CreateFromExisting( this, IDC_XE_SB_V );
CXeScrollbar in UltiMate Grid is also fairly simple. It's a simple matter of modifying the
CUGVScroll classes a little bit: change base class from
CXeScrollBar. Please read 'Using
CXeScrollBar in Ultimate grid.txt' I've included with the downloads above, for details on how to modify the source files and how to build and run the UltiMate Grid samples.
Please note that
CXeScrollBar implements a full custom control, for that reason it is not possible to simply subclass a standard scroll bar,
CXeScrollBar must be created using the
Create member or the
How It Works
CXeScrollBar is implemented as two classes,
CXeScrollBar does all the painting and
CXeScrollBarBase implements all the 'business' logic needed. I decided early in the development to split the 'business' logic and the painting into two classes to make it easy to change the look of the scroll bar in the future.
Here is where the article title finally starts to make sense. If you are feeling artistic, you can create a scroll bar that looks just the way you want it to very simply. Either by creating the eight bitmaps needed for
CXeScrollBar or deriving your own class from
CXeScrollBarBase and do the painting needed any way you like.
CXeScrollBar uses four bitmaps for horizontal scroll bar and four bitmaps for vertical scroll bar. One for 'up' state, one for 'hot' state, one for 'down' state and one for 'disabled' state.
If you do decide to create bitmaps to replace the ones used by
CXeScrollBar - read comments regarding how the code uses the bitmaps in XeScrollBar.cpp before you do. I've included the PhotoShop document I used to create the bitmaps in the download section above. Tip - create horizontal bitmaps first, then open those in PhotoShop and do rotate canvas 90 degrees and flip vertical to create the bitmaps for the vertical scroll bar.
The bitmaps are divided into logical sections: Left button, Left shaft, Thumb, Right shaft and Right button for horizontal scroll bars, Top button, Top shaft, Thumb, Bottom shaft and Bottom button for vertical scroll bars. The offsets in pixels into the bitmaps for each section are hard-coded in
CXeScrollBar. For that reason, it is important be very careful when creating the bitmaps. Unless of course you are prepared to change the code in
CXeScrollBar to suit your bitmaps.
The 'business' logic in
CXeScrollBarBase provides the 'state' and size information of each scroll bar section for
CXeScrollBar to use when painting. The following code shows what steps are needed when painting the scroll bar.
const CRect *prcElem = 0;
for( int nElem = eTLbutton; nElem <= eThumb; nElem++ )
stArea.eArea = (eXSB_AREA)nElem;
prcElem = GetUIelementDrawState( stArea.eArea, eState );
if( !prcElem || eState == eNotDrawn ) continue;
DrawScrollBar( CDC* pDC ) member in
CXeScrollBar uses the
eState enum to select the bitmap to use for painting each section of the scroll bar.
The constructor in
CXeScrollBar loads all bitmaps into memory from resources when called for the first time. The resource name of each bitmap is hard-coded. A reference counter is used to keep track of how many objects have been created. The bitmaps remain in memory until the last object is destroyed.
Points of Interest
I've taken great care to faithfully implement this scroll bar so it behaves exactly like the standard windows scroll bar does. There are a few minor (IMHO) areas I deviated from the standard.
SBM_ENABLE_ARROWS message handler behaves a little bit differently that the standard scroll bar does. During testing, the standard scroll bar behaved strangely when
ESB_DISABLE_LEFT, ESB_DISABLE_RIGHT, ESB_DISABLE_UP or
ESB_DISABLE_DOWN flags are used, for that reason I've only implemented support for the
SBM_GETSCROLLBARINFO message handler is also different from the standard. I've implemented better support for the
STATE_SYSTEM_PRESSED flags. I've implemented this as per Microsoft documentation, not as Microsoft implemented the standard scroll bar.
There are a few other message handlers that I think deserve special mention.
WM_LBUTTONDOWN message handler is interesting because here is where many of the other scroll bars I looked at before creating this one got it wrong. A scroll bar MUST not 'take' focus unless it has the
WM_TABSTOP windows style. This is also the only place in the code where we 'take' focus. The dialog manager will set focus to us in all other cases. This may seem trivial but keep in mind that users will find it very annoying if input focus changes unexpectedly.
WM_GETDLGCODE message handler is very interesting because here is where we interact with the dialog manager. Tab order and input focus management is implemented here. The dialog manager will ask if we want input focus when the user presses the Tab key, we return
DLGC_STATIC depending on the presense or absense of the
WM_TABSTOP style. The dialog manager will also send a
WM_GETDLGCODE message for every keystroke, we need to tell the dialog manager if we will process the keyboard message by returning
DLGC_WANTMESSAGE. We do so only if 'this' window has
WM_TABSTOP style set. There is an exception to this, the standard windows scroll bar will process Up/Down keys for a vertical scroll bar and Left/Right keys for a horizontal scroll bar even if the window does not have
WM_TABSTOP style set. I've also implemented this behaviour just to remain true to the standard.
WM_MOUSEWHEEL message handler is also noteworthy because we will never get those messages unless we have input focus. This is normal windows behaviour. The parent window will usually provide mouse wheel message handling. E.g. UltiMate grid is implemented like that. I should also mention that the standard windows scroll bar does not implement support for mouse wheel messages.
WM_CONTEXTMENU message handler is worth mentioning because the context menu is loaded from user32.dll if possible and because of that should be in the 'local' language. If the load from user32.dll failed, a menu in English is created instead. Note - I have not been able to test if this method works for other languages. I would be very interested to know.
WM_KEYUP message handlers are interesting in that those messages are not sent to us unless we have input focus. Also when those messages are processed, it's important to return
0 to indicate the message was processed even if we ignored the key, otherwise the dialog manager will look for another child window to process the message and we lose input focus.
WM_KILLFOCUS message handlers implement the 'blinking' input focus state of the scroll bar. The class derived from
CXeScrollBarBase needs to show that the scroll bar has input focus somehow, in
CXeScrollBar I've implemented this by painting the 'gripper' section of the thumb box only when
TRUE, the base class uses a timer to 'blink' every 500mS. Most users will never see this because it's rather unusual for a scroll bar to receive focus
The demo project included with this article demonstrates how
CXeScrollBar compares to the standard windows scroll bar.
I used this extensively when developing the
The 'Tab stops' check box - sets/clears the
WM_TABSTOP style on all scroll bars.
The scroll bar information dialog shows information from all the scroll bars on the main form view. The 'Lock scroll bars' locks/unlocks together the horizontal and vertical scroll bars. The 'Set scroll info' button will set scroll range, page size and position to all scroll bars. The 'Set range' button will set scroll range only. The 'Set POS' button will set scroll position only. The edit boxes show information we get from the scroll bars when we send
SBM_GETSCROLLBARINFO messages. The lower left corner shows the state of the
STATE_SYSTEM_XXX flags for each section of the scroll bars.
The XeScrollBar.cpp file includes a list of research sources I used during the development of this scroll bar. Of particular interest is SkinControls 1.1 - A journey in automating the skinning of Windows controls article by .dan.g. here on CodeProject. If you read it, you will understand why subclassing a Windows scroll bar and do your own painting does not work.
Version 1.1 - 2010 October 21
- Changed base class to
- Fixed many issues to make this scroll bar behave (almost) the same as windows scroll bar
Version 1.0 - 2009