ResizeScrollbar - How to change width of built-in scroll bars






4.76/5 (12 votes)
Changing scroll bar width for Windows controls like listbox
Introduction
Recently I needed to change the width of scroll bar in a listbox, for another project I submitted here. This turns out to be one of those things in Windows that are much more complicated than would first appear. Fortunately, the answer I came up with works well, and I believe it could be applied to other controls.
The problem lies in nature of scroll bars - or maybe I should say, natures of scroll bars, since there are actually two types, although they look identical. Paul DiLascia described them in his Nov. 2001 MSDN Magazine article:
Windows has two kinds of scroll bars. There are the "built-in"
scroll bars that you get when you use the WS_HSCROLL
and/or
WS_VSCROLL
window styles, and there are scroll bar controls,
actual child windows you can create within a window.
Inquiring minds might wonder why there are two kinds of scroll bars. The answer lies in history. In the early days when Windows was a wee lad, there was a pressing need to save every bit of memory, and since each window took up considerable space relative to what was available, and scroll bars were so ubiquitous, the designers figured it would be efficient to build them in as a window option instead of requiring programmers to create separate windows and hook them up. It also made life easier for programmers. At least, I am guessing that's the reason; perhaps the real reason is something else entirely.
Well, fine. That's very interesting, but how do you change width of the scroll bar in a listbox? Trying to change width of a built-in scroll bar sounds really nasty - a lot of owner-draw stuff, and in non-client area too. So what to do?
My Solution - A Classic Wrapper
The answer is to use the other type of scroll bar,
one based on CScrollBar
.
First, we need to get rid of listbox's built-in scroll bar.
This is trivial - just don't specify WS_VSCROLL
style.
Next, we create a wrapper class for
control - in this case, a listbox. Note that listbox will not
be ownerdrawn. The wrapper class intercepts scroll messages from
CScrollBar
object, and passes them on to the
contained CListBox
object. The wrapper class also must
forward any CListBox
-specific methods that
parent object (usually a dialog) invokes via wrapper class.
This is classic behavior of ActiveX and other COM wrappers -
intercepting (and acting on) some methods, and forwarding rest.
This would be a big job if we needed to duplicate all CListBox
methods. Usually, that's not necessary, since in most apps only
a subset of the methods are ever used. (You can find out which ones
you need fairly easily - just try to compile and link, and the errors
will tell you which ones are needed). In the case of CXComboList
class, you can look at XComboList.h to see which methods are forwarded:
int AddString(LPCTSTR lpszItem) { return m_ListBox.AddString(lpszItem); } int GetCount() { return m_ListBox.GetCount(); } void GetText(int nIndex, CString& rString) { m_ListBox.GetText(nIndex, rString); } int FindStringExact(int nIndexStart, LPCTSTR lpszFind) { return m_ListBox.FindStringExact(nIndexStart, lpszFind); } int SetCurSel(int nSelect) { return m_ListBox.SetCurSel(nSelect); } int GetCurSel() { return m_ListBox.GetCurSel(); } void SetFont(CFont* pFont, BOOL bRedraw = TRUE) { m_ListBox.SetFont(pFont, bRedraw); }
Creating the Scroll Bar
The next thing is to create the CScrollBar
. This is done in
OnCreate
:
int CXComboList::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) return -1; CRect rect(0,0,0,0); // create the listbox that we're wrapping VERIFY(m_ListBox.Create(WS_VISIBLE|WS_CHILD |LBS_NOINTEGRALHEIGHT/*|WS_BORDER*/, rect, this, 0)); // create the vertical scrollbar VERIFY(m_wndSBVert.Create(WS_VISIBLE|WS_CHILD|SBS_VERT, rect, this, 0)); // set font from parent CFont *font = GetParent()->GetFont(); if (font) { SetFont(font, FALSE); m_wndSBVert.SetFont(font, FALSE); } return 0; }
Setting Scroll Bar Properties
The method SetActive
is called by parent after listbox has been
populated. To position CScrollBar
in client area of wrapper object,
SetActive
asks listbox for its size properties:
void CXComboList::SetActive(int nScrollBarWidth) { if (!::IsWindow(m_ListBox.m_hWnd)) return; m_ListBox.SetFocus(); if (m_bFirstTime) { m_bFirstTime = FALSE; CRect rect; GetWindowRect(&rect); // set listbox size according to item height int nItemHeight = m_ListBox.GetItemHeight(0); CRect lbrect; GetClientRect(&lbrect); lbrect.top += 1; lbrect.bottom = lbrect.top + (rect.Height() / nItemHeight) * nItemHeight; lbrect.left += 1; lbrect.right -= nScrollBarWidth; int nItemsInView = (lbrect.Height()) / nItemHeight; // set size of listbox wrapper (from size of listbox) rect.bottom = rect.top + lbrect.Height() + 4; MoveWindow(&rect); m_ListBox.MoveWindow(&lbrect); m_ListBox.BringWindowToTop(); // set size and position for vertical scroll bar CRect sbrect; sbrect = lbrect; sbrect.left = lbrect.right; sbrect.right += nScrollBarWidth; m_wndSBVert.MoveWindow(&sbrect); SCROLLINFO si; si.cbSize = sizeof(si); si.fMask = SIF_ALL; m_wndSBVert.GetScrollInfo(&si); // set info for scrollbar si.nMin = 0; si.nMax = m_ListBox.GetCount(); if (si.nMax < 0) si.nMax = 1; si.nPage = nItemsInView; int nCurSel = m_ListBox.GetCurSel(); if (nCurSel == LB_ERR || nCurSel < 0) nCurSel = 0; si.nPos = nCurSel; // set top index, to force selected item to be in view m_ListBox.SetTopIndex(nCurSel > 0 ? nCurSel - 1 : 0); if (si.nPos < 0) si.nPos = 0; m_wndSBVert.SetScrollInfo(&si); m_wndSBVert.SetScrollPos(si.nPos, TRUE); RedrawWindow(); } }
One thing to note is SCROLLINFO
struct. The
SDK docs don't say much about how to set it up, but it's really
pretty simple - you can set it
up anyway you like, as long as you're consistent about how you
do it. I chose to use the number of items as basis for scrolling.
This makes sense, since to scroll one item means to scroll one line.
So the page height (the total number of items viewable) and maximum
scroll parameter (the total count of listbox items) are simple to
calculate. I have also seen other units, such as pixel count,
used as basis for scrolling, but this is something you will
have to decide.
Once the SCROLLINFO
struct is filled in and you set it with
SetScrollInfo
, you can simply refer to it
again (using GetScrollInfo
) whenever you need the parameters -
nPage
and nMax
items will not be changed behind your back.
Handling Scroll Messages
There are two sources for scroll messages: the mouse and the keyboard.
Mouse messages are seen by CXComboList
as WM_VCSROLL
messages.
These need to be forwarded to listbox, and then scroll
position needs to be updated.
void CXComboList::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar*) { if (!::IsWindow(m_ListBox.m_hWnd)) return; // forward scroll message to listbox const MSG* pMsg = GetCurrentMessage(); m_ListBox.SendMessage(WM_VSCROLL, pMsg->wParam, pMsg->lParam); SCROLLINFO si = { sizeof(SCROLLINFO), SIF_ALL | SIF_DISABLENOSCROLL, }; m_wndSBVert.GetScrollInfo(&si); switch (nSBCode) { case SB_BOTTOM: // scroll to bottom si.nPos = si.nMax; break; case SB_TOP: // scroll to top si.nPos = 0; break; case SB_PAGEDOWN: // scroll one page down si.nPos += si.nPage; break; case SB_PAGEUP: // scroll one page up si.nPos -= si.nPage; break; case SB_LINEDOWN: // scroll one line up si.nPos += 1; break; case SB_LINEUP: // scroll one line up si.nPos -= 1; break; case SB_THUMBTRACK: // drag scroll box //to specified position. The // current position is provided in nPos case SB_THUMBPOSITION: // scroll to the absolute //position. The current // position is provided in nPos si.nPos = nPos; break; case SB_ENDSCROLL: // end scroll return; default: break; } if (si.nPos < 0) si.nPos = 0; if (si.nPos > si.nMax) si.nPos = si.nMax; m_wndSBVert.SetScrollInfo(&si); }
Keyboard messages come in as WM_KEYDOWN
messages.
While the general approach here is same as for WM_VSCROLL
messages, it was interesting to find that nPos
parameter from
SCROLLINFO
struct was not always correct upon entry to
WM_COMMAND
handler. So index from listbox is used to
position the scroll bar.
The WM_COMMAND
handler processes the arrow keys, PAGE UP,
PAGE DOWN, HOME, and END keys. It must also process alpha keys,
which position listbox selection to next item that
begins with that alpha character.
BOOL CXComboList::PreTranslateMessage(MSG* pMsg) { switch (pMsg->message) { case WM_KEYDOWN: { ///////////////////////////////////////// // we need to trap all cursor keys & // alpha keys to reposition the // scroll bar ///////////////////////////////////////// SCROLLINFO si = { sizeof(SCROLLINFO), SIF_ALL | SIF_DISABLENOSCROLL, }; m_wndSBVert.GetScrollInfo(&si); BOOL bSetScrollInfo = FALSE; int nIndex = 0; if (::IsWindow(m_ListBox.m_hWnd)) nIndex = m_ListBox.GetCurSel(); if (nIndex == LB_ERR || nIndex < 0) nIndex = 0; // use index from listbox, because scroll // position cannot be relied // upon here switch (pMsg->wParam) { case VK_RETURN: SendRegisteredMessage(WM_XCOMBOLIST_VK_RETURN, 0, 0); break; case VK_ESCAPE: SendRegisteredMessage(WM_XCOMBOLIST_VK_ESCAPE, 0, 0); break; // handle scrolling messages case VK_DOWN: si.nPos = nIndex + 1; bSetScrollInfo = TRUE; break; case VK_END: si.nPos = si.nMax; bSetScrollInfo = TRUE; break; case VK_HOME: si.nPos = 0; bSetScrollInfo = TRUE; break; case VK_NEXT: // PAGE DOWN si.nPos = nIndex + (si.nPage-1); bSetScrollInfo = TRUE; break; case VK_PRIOR: // PAGE UP si.nPos = nIndex - (si.nPage - 1); bSetScrollInfo = TRUE; break; case VK_UP: si.nPos = nIndex - 1; bSetScrollInfo = TRUE; break; default: if (pMsg->wParam >= 0x41/*VK_A*/ && pMsg->wParam <= 0x5A/*VK_Z*/) { // this was an alpha key - // try to find listbox index CString strAlpha; strAlpha = (_TCHAR) pMsg->wParam; int nIndex2 = 0; if (::IsWindow(m_ListBox.m_hWnd)) nIndex2 = m_ListBox.FindString(nIndex, strAlpha); if (nIndex2 != LB_ERR) { si.nPos = nIndex2; bSetScrollInfo = TRUE; } } break; } if (bSetScrollInfo) { // let parent know the selection has changed SendRegisteredMessage(WM_XCOMBOLIST_KEYDOWN, 0, 0); // update scrollbar if (si.nPos < 0) si.nPos = 0; if (si.nPos > si.nMax) si.nPos = si.nMax; m_wndSBVert.SetScrollInfo(&si); } break; } case WM_LBUTTONUP: SendRegisteredMessage(WM_XCOMBOLIST_LBUTTONUP, 0, 0); break; } return CWnd::PreTranslateMessage(pMsg); }
Demo Project
The demo project shows how this approach works in an MFC application, to allow you to step through message flow.
Summary
Using wrapper classes is a simple, effective technique when you need to resize scroll bars of a Windows control. The benefit is that there is no need for ownerdraw controls. The downside is that many forwarding methods may be required, depending on app. However, for user, the visual appearance and operation is what matters, and it is very detracting to have crafted a very nice UI, only to see big, sloppy scroll bars.
Usage
This software is released into the public domain. You are free to use it in any way you like. If you modify it or extend it, please to consider posting new code here for everyone to share. This software is provided "as is" with no expressed or implied warranty. I accept no liability for any damage or loss of business that this software may cause.