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

  • Adaptation and evolution are fundamental requirements of survival -- not only in nature, but also in business. Our world has changed dramatically in a short amount of time. Many businesses are fueling and capitalizing on this change, while others are desperately clinging to a bygone era. Who is left standing in the years and decades ahead should come as no surprise. This edition of Unleashing IT highlights the companies that are embracing new circumstances, new methods, and new opportunities. By downloading …

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds