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);
VERIFY(m_ListBox.Create(WS_VISIBLE|WS_CHILD
|LBS_NOINTEGRALHEIGHT,
rect, this, 0));
VERIFY(m_wndSBVert.Create(WS_VISIBLE|WS_CHILD|SBS_VERT,
rect, this, 0));
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);
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;
rect.bottom = rect.top + lbrect.Height() + 4;
MoveWindow(&rect);
m_ListBox.MoveWindow(&lbrect);
m_ListBox.BringWindowToTop();
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);
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;
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;
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:
si.nPos = si.nMax;
break;
case SB_TOP:
si.nPos = 0;
break;
case SB_PAGEDOWN:
si.nPos += si.nPage;
break;
case SB_PAGEUP:
si.nPos -= si.nPage;
break;
case SB_LINEDOWN:
si.nPos += 1;
break;
case SB_LINEUP:
si.nPos -= 1;
break;
case SB_THUMBTRACK:
case SB_THUMBPOSITION:
si.nPos = nPos;
break;
case SB_ENDSCROLL:
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:
{
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;
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;
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:
si.nPos = nIndex + (si.nPage-1);
bSetScrollInfo = TRUE;
break;
case VK_PRIOR:
si.nPos = nIndex - (si.nPage - 1);
bSetScrollInfo = TRUE;
break;
case VK_UP:
si.nPos = nIndex - 1;
bSetScrollInfo = TRUE;
break;
default:
if (pMsg->wParam >= 0x41 &&
pMsg->wParam <= 0x5A)
{
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)
{
SendRegisteredMessage(WM_XCOMBOLIST_KEYDOWN,
0, 0);
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.