CListBox with the Horizontal Scroll Bar that Works

Environment: Visual Studio, VC++ 6.0, MFC, W9x/Me, W2k, XP

Overview

CListBox: This is a wrapper class for the ListBox control, and it's used in almost every application. There is a little "gotcha" to this class: The horizontal scroll bar doesn't work. Okay, I created it with the WS_HSCROLL flag set, the scroll bar is visible, and strings that I'm adding to the box are obviously longer then the box's horizontal extent, but I'm unable to scroll. What's wrong?

My theory is that the guy who originally implemented that control had way too much Greek mythology in his childhood. Villainous Procrustes and his bed where he had to cut off travelers' legs if they didn't fit in had a huge impact on the poor guy. If something is too long, chop it off. Well, whatever the reason is, the control simply ignores the fact that the strings are longer then it can show. How can we fix it?

Well, first, we have to catch all messages sent to the control that affect the box's content, hence the strings that it has (behind such functions as AddString() or ResetContent(), is SendMessage() with LB_ADDSTRING and LB_RESETCONTENT respectively). There are five messages of that type (at least that's the number I've come with. There may be more; I'm not sure.). Once they are identified, they can be caught and modified the same way as the others:

  • LB_ADDSTRING
  • LB_INSERTSTRING
  • LB_DELETESTRING
  • LB_DIR
  • LB_RESETCONTENT

After the control is subclassed, we can catch any message that is sent to it through the control's message map:

...
BEGIN_MESSAGE_MAP(CHScrollListBox, CListBox)
  //{{AFX_MSG_MAP(CHScrollListBox)
  // NOTE - the ClassWizard will add and remove mapping macros here.
  //}}AFX_MSG_MAP
  ON_MESSAGE(LB_ADDSTRING, OnAddString)
  ON_MESSAGE(LB_INSERTSTRING, OnInsertString)
  ON_MESSAGE(LB_DELETESTRING, OnDeleteString)
  ON_MESSAGE(LB_DIR, OnDir)
  ON_MESSAGE(LB_RESETCONTENT, OnResetContent)
END_MESSAGE_MAP()
...

When we catch the message (for example, LB_ADDSTRING), we are going to call default processing first, and, if the returned code is okay (most of these messages return some kind of a completion indicator), call our functions, either SetNewHExtent() or ResetHExtent(), that will bring the horizontal extent member of ListBox (this is the parameter that affects the horizontal scroll bar, if the bar is present) in sync with the longest string that it contains:

...
////////////////////////////////////////////////////////////////
// OnAddString: wParam - none, lParam - string, returns - int
////////////////////////////////////////////////////////////////
LRESULT CHScrollListBox::OnAddString(WPARAM wParam, LPARAM lParam)
{
  LRESULT lResult = Default();
  if (!((lResult == LB_ERR) || (lResult == LB_ERRSPACE)))
    SetNewHExtent((LPCTSTR) lParam);
  return lResult;
}
...

SetNewHExtent() in turn will calculate in pixels the length of the supplied string in the given device context (GetTextLen()) and set a new horizontal extent for the control. The extent can be set through CListBox::SetHorizontalExtent(extent) and we do it only if it's bigger then the current extent:

...
////////////////////////////////////////////////////////////////
void CHScrollListBox::SetNewHExtent(LPCTSTR lpszNewString)
{
  int iExt = GetTextLen(lpszNewString);
  if (iExt > GetHorizontalExtent())
    SetHorizontalExtent(iExt);
}
...

Let's take a look at what GetTextLen() does:

...
int CHScrollListBox::GetTextLen(LPCTSTR lpszText)
{
  ASSERT(AfxIsValidString(lpszText));

  CDC *pDC = GetDC();
  ASSERT(pDC);

  CSize size;
  CFont* pOldFont = pDC->SelectObject(GetFont());
  if ((GetStyle() & LBS_USETABSTOPS) == 0)
  {
    size = pDC->GetTextExtent(lpszText, (int) _tcslen(lpszText));
    size.cx += 3;
  }
  else
  {
    // Expand tabs as well
    size = pDC->GetTabbedTextExtent(lpszText, (int)
          _tcslen(lpszText), 0, NULL);
    size.cx += 2;
  }
  pDC->SelectObject(pOldFont);
  ReleaseDC(pDC);

  return size.cx;
}
...

The first call is a sanity check with AfxIsValidString(). Then we get the control's DC through GetDC() and select the control's font in the context. After that, we look at the styles. If LBS_USETABSTOPS is set, we call GetTabbedTextExtent(); otherwise, it's GetTextExtent(), which returns the number of pixels that the specified string will occupy on the screen in a horizontal direction when drawn in that device context. I add a few more pixels here, just to make sure the string looks symmetrical when displayed, because the box has a margin on the left. That's it.

In calls OnDeleteString() and OnDir(), I use the function named ResetHExtent(). Because I do not know what string has the next longest extent from the one I'm deleting and since OnDir() will reset the content and fill it in with directory or file names, I'm simply going through the whole list, looking for the longest string the list has and setting its extent.

CHScrollListBox should only be used with a single column, non-ownerdrawn list box. There are a couple of ASSERTs in PreSubclassWindow() to alert you in case you are setting the flags, which the box does not know how to handle. A typical propsheet for the box will look something like the picture below:



Click here for a larger image.

If you don't need the box to display tabs, uncheck Use Tabstops. The main flags for CHScrollListBox to function properly are Horizontal Scroll, Selection->Single, Owner Draw->No.

Usage

Okay, how one can utilize CHScrollListBox class? Here are some steps to take, sort of manual labor, that will get you there:

  1. In the resource editor, create a dialog template that will host a listbox control.
  2. Set properties of the list box according to the picture above (at least, Horizontal Scroll, Selection, and Owner Draw should match).
  3. In the header file of your dialog's class, associated with the template, add:
    #include "HScrollListBox.h"
  4. Define a member variable of CHScrollListBox, like so:
    CHScrollListBox m_listBox;
  5. In the implementation file, in DoDataExchange() function, add a line:
    DDX_Control(pDX, IDC_YOUR_LISTBOX_CTRL_ID, m_listBox);
    This call in turn calls SubclassDlgItem(), which does the trick of routing all messages destined to the control to our message map.

Of course, IDC_YOUR_LISTBOX_CTRL_ID and m_listBox are orbitrary names; you should use your own instead.

Downloads

Download demo project - 23 Kb
Download source - 3 Kb


Comments

  • Thanx

    Posted by Avm on 04/12/2012 12:05am

    Thank you. Very helpful.

    Reply
  • Thanks

    Posted by majoob on 01/31/2007 06:02pm

    Works great!

    Reply
  • Fonts

    Posted by fcoarturobn on 11/06/2006 11:30am

    First of all, thanks a lot !!
    
      about the fonts :
    
    * to change the font after add all the strings, then ResetHExtent() must be called again. (GetTextExtent/GetTabbedTextExtent depends on the font)
    
    * try using SetFont before add the strings (that is the ideal situation),  or maybe write your own SetFont in order to call it at any time.
    
    void CHScrollListBox::SetFont( CFont* pFont, BOOL bRedraw){
        CListBox::SetFont( pFont );
        ResetHExtent();
    }
    
    Thanks !
    Arturo.-

    Reply
  • XVT Library?

    Posted by rbinu on 10/25/2005 07:03pm

    That is a neat job! I am using XVT Library instead of MFC. Have a clue how I can make horizontal scrollbars appear using XVT? The library doesn't seem to have functions equivalent to SetHorizontalExtent....:(

    • XVT?

      Posted by Geno Carman on 11/24/2005 02:39pm

      Use the list box messages: LB_GETHORIZONTALEXTENT, etc.

      Reply
    Reply
  • THANKS!

    Posted by Legacy on 02/10/2004 12:00am

    Originally posted by: Roku

    It helps alot. You are the best!

    Reply
  • Very useful

    Posted by Legacy on 01/31/2004 12:00am

    Originally posted by: James Bond

    Very useful.......good work.
    

    Reply
  • Thanks...So Cool

    Posted by Legacy on 11/12/2003 12:00am

    Originally posted by: Ashish Shah

    Thank You Very Much...
    
    I almost wasted whole day in this problem...but its good to get this solution before going home!!!!!

    Reply
  • Thanx!!!!!!!!!!!!!

    Posted by Legacy on 10/25/2003 12:00am

    Originally posted by: Skygod

    Thanks for this fix! I was about to go really mad/crazy before I found this fine little hack!

    "Respect to the man in the ice-cream van..."

    Reply
  • Works on PocketPC too!

    Posted by Legacy on 10/02/2003 12:00am

    Originally posted by: Claire

    Apparently GetTabbedTextExtent() is not supported in
    WinCE, but that was the only difference...

    Thank you!!!

    Reply
  • Thank you

    Posted by Legacy on 09/20/2003 12:00am

    Originally posted by: Tatobrutto

    Thank you.
    Simple and pretty...I'd like it.

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Protecting business operations means shifting the priorities around availability from disaster recovery to business continuity. Enterprises are shifting their focus from recovery from a disaster to preventing the disaster in the first place. With this change in mindset, disaster recovery is no longer the first line of defense; the organizations with a smarter business continuity practice are less impacted when disasters strike. This SmartSelect will provide insight to help guide your enterprise toward better …

  • Live Event Date: August 14, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Data protection has long been considered "overhead" by many organizations in the past, many chalking it up to an insurance policy or an extended warranty you may never use. The realities of today make data protection a must-have, as we live in a data driven society. The digital assets we create, share, and collaborate with others on must be managed and protected for many purposes. Check out this upcoming eSeminar and join eVault Chief Technology …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds