Currently viewing: GipsySoft » Front Page» Articles

Global Subclassing - adding a horizontal scrollbar to LISTBOX controls

This article demonstrates a simple technique for extending Windows controls as well as giving a handy utility function that adds a basic extension to a LISTBOX.

The Problem

LISTBOX controls don't normally have horizontal scrollbars. I don't know what wisdom lies behind this decision but that's the way it is. The authors did thoughtfully provide an API for setting the horizontal extent of the control but it's rather clumsy to use.

If you add a long string to a list box the string is truncated, by default, and the horizontal scrollbar doesn't appear. If you wish to have a horizontal scrollbar then for each item in the LISTBOX you need to:

  • Create a HDC
  • Get the font used by the LISTBOX and select it into the DC
  • Measure the text and add a 'fudge-factor'
  • See if the width is larger than your recorded maximum width
  • Tell the LISTBOX the maximum width using LB_SETHORIZONTALEXTENT

That's quite a bit of hassle and it means modifying a lot of code just to get something as basic as a horizontal scrollbar on a LISTBOX.

The Solution

I love simple solutions. If I can get the solution down to the minimum number of lines of code possible then I will, or as Mark Twain said; If I had more time I would have written less.

You'll need to add just one line of code (call the function I write below) to your applications to have all of your LISTBOX's capable of automatically showing a horizontal scrollbar.

Now we could use Instance Subclassing and subclass each LISTBOX as we create it (In MFC derive from CLISTBOX) and then catch the messages for adding/removing items - and perform our measuring there. However, I don't like this solution as it requires me to write one line of code for every LISTBOX I use - and I might forget one!

Instead we'll use Global Subclassing. Global Subclassing is the method of altering the window class information (in our case the window procedure) but keeping the class name the same. All windows of this class, in our case LISTBOX, will use our stuff instead of the default. This has the side benefit of giving us the ability to catch all messages destined to the window - this is not possible when using Instance Subclassing and is a very handy way of hooking into standard window classes or classes you don't have source code for.

Here is a good overview of the various subclassing techniques:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwui/html/msdn_subclas3.asp

The Demo

The demo is a straight dialog application using MFC. It has facilities to manipulate the LISTBOX and was my test application for the function.

Solution Overview

The steps we take are basically very simple, here they are:

  • Create a LISTBOX
  • Store it's current class window procedure so we can call it to let it handle default behaviour
  • Set it's class window procedure to ours
  • Destroy the LISTBOX window.

Simple? Of course it is, and here's the complete source code:


BOOL EnhanceLISTBOX( HINSTANCE hInstance )
//
//  This is the main API. Call it once and that's it!
{
  if( !g_wpOld )
  {
    HWND hwnd = ::CreateWindow( _T("LISTBOX")
        , NULL, 0, 0, 0, 0, 0, 0, 0, hInstance, 0 );
    ASSERT( hwnd );
    
    g_wpOld = reinterpret_cast< WNDPROC >( ::GetClassLongPtr( hwnd, GCLP_WNDPROC ) );
    ::SetClassLongPtr( hwnd, GCLP_WNDPROC, (LONG_PTR)WndProc );

    VERIFY( DestroyWindow( hwnd ) );
    return TRUE;
  }
  return FALSE;
}

To use it in your projects download the source, which also includes everything to build the demo, and add the source file EnhanceListBox.cpp to your project. Then call the function EnhanceListbox at startup.

Using MFC you would add it to your application InitInstance function just like below:



BOOL CScrollingListboxTestApp::InitInstance()
{
	...

	extern BOOL EnhanceListbox( HINSTANCE hInstance );
	EnhanceListbox( AfxGetInstanceHandle() );

	...
}

Now all your LISTBOX are belong to us ;-)

Implementation details

Changing the window procedure is the first step. Now we need to add message processing to our window and call the default. It's important to call the default using CallWindowProc as, perhaps oddly, the window procedure pointer returned by GetClassLongPtr might not be a window procedure at all - so calling it directly is not a Good Thing. This is because of the potential mix of unicode and ansi windows in NT/2K/XP applications.

We keep a data structure associated with the window, it contains some handy values used whilst measuring, it also contains an array of widths for each item. This array allows us to ensure that when removing an item we can set the horizontal width to be the previous largest item and so on.

The messages that most interest us are the window creation and destruction messages, and the messages associated with add/removing items from the LISTBOX, we throw in WM_SIZE and WM_SETFONT for proper handling of these events.

WM_NCCREATE

We handle WM_NCCREATE because we want to associate so data with the LISTBOX and make a minor modification to the LISTBOX style. Creating our data is a simple and store in the window properties.

We modify the style by adding the WS_HSCROLL if the LISTBOX doesn't already have it. Without this style the horizontal scrollbar won't show no matter what we do.

WM_NCDESTROY

Here we simply destroy our data structure and remove it from the window properties. Nothing exciting.

WM_SETFONT

Changing the font used in the LISTBOX will impact the size of items drawn, this will alter the need and the size of the horizontal scrollbar. Updating the font size forces us to throw out our measurements and recreate them all - altering the horizontal extent of the LISTBOX where appropriate.

There's a small quirk in here though. For some reason the LISTBOX extent needs to be a little bigger than the width of the text. I thought it was a bug until I read more articles about it. It seems that the stock solution is to add the average width of a character onto the extent - I follow the herd and do the same, I keep the average width for later measuring when adding new items.

This function is a little convoluted because I need to ask the LISTBOX for each text item in order to measure it for the new font. I did consider adding the text for each item into my data structure but decided that changing the font happened so infrequently that optimisation at the expense of memory wasn't required.

WM_SIZE

Changing the size of the LISTBOX affects the width of the window, which affects whether we should show a scrollbar or not.

We primarily need this because of the average width addition. If we didn't add an average font width then we could probably do without the check against the client width, but with the addition we need to check to see if the measured string length exceeds our LISTBOX width - if it does then we alter the horizontal extent…if we didn't do this test then it's possible for the scrollbar to appear when the string is just on the edge of the LISTBOX, and that just plain odd...phew!

LB_INSERTSTRING and LB_ADDSTRING

They are both very similar. They create a HDC, select the LISTBOX font and measure the item. Note that for each we add our item at the position given to us by the call to the LISTBOX window procedure. This is required because the LISTBOX may be sorted and may insert the item anywhere - we don't really care, all we care about is getting the item width in the correct place in our array.

Both methods of adding an item use a helper function called BaseMeasureItem. This function measures the string and also manages the horizontal extent of the LISBOX.

Also note that for LB_ADDSTRING we check to see if the LISTBOX is sorted and act appropriately.

LB_RESETCONTENT

All we do here is remove everything from our data structure and reset the horizontal extent.

LB_DELETESTRING

Removing a string is special because it happens before we call the LISTBOX window procedure. This is required when we have one item. It seems that the LISTBOX sends itself an LB_RESETCONTENT message whilst in the LB_DELETESTRING handler. This messes with our plans because our LB_RESETCONTENT handler will remove everything and then our LB_DELETESTRING handler can't remove the item!

Other notes

The demo source has been tested on Win2K and that's all. It builds both in ansi and unicode and both debug and release builds of the demo include ansi and unicode.

The demo is built with level 4 warnings and warnings as errors.

The demo includes two other source libraries:

DialogSizer
Handles sizing dialog boxes, property sheets etc. It dynamically moves the controls around, loads and saves the window position and a few other things.
DebugHlp
A debugging extension library that extends things like TRACE and ASSERT with some drop-dead-gorgeous features I just can't live without including writing trace output to a log file.

The general technique of global subclassing can be used for pretty much any control but sometimes it's better to subclass a single control...it depends on what you want to achieve and how many times you will need to achieve it. Global subclassing will add a very minor extra burden where the extra functionality isn't required - but it can substantially reduce the coding needed when the functionality is used throughout.

The code supplied has not been tested for owner drawn, multiple column or LISTBOX controls using tabstops. I doubt it would work correctly for any of these. The code to prevent it from working for these controls would probably just be a matter of testing the style at WM_NCCREATE time and then just doing nothing for the remainder of the time - perhaps setting an appropriate flag. I have not done this for this article.

[TOP]