Click here to Skip to main content
15,886,518 members
Articles / Desktop Programming / MFC
Article

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

Rate me:
Please Sign up or sign in to vote.
4.76/5 (13 votes)
7 Feb 2002CPOL5 min read 146.8K   3.2K   52   3
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Hans Dietrich Software
United States United States
I attended St. Michael's College of the University of Toronto, with the intention of becoming a priest. A friend in the University's Computer Science Department got me interested in programming, and I have been hooked ever since.

Recently, I have moved to Los Angeles where I am doing consulting and development work.

For consulting and custom software development, please see www.hdsoft.org.






Comments and Discussions

 
QuestionVery interesting article. Has this been ported to .NET? Pin
DimondWolfe2-Oct-09 5:26
DimondWolfe2-Oct-09 5:26 
GeneralFormview scrollbar Pin
Bui Tan Duoc22-Jun-07 15:53
professionalBui Tan Duoc22-Jun-07 15:53 
GeneralRepositioning build-in scroll bars Pin
Marcus Carey11-Mar-04 17:00
Marcus Carey11-Mar-04 17:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.