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:

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

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read