HOME
Login
Change Info
Logout


TUTORIALS

C, C++
Win32
Java
Visual Basic
MFC
DCOM
Networking
C#
Perl
HTML
XML
ASP
PHP
Javascript
Other

DOWNLOADS
ITCLib
SourceVizor meets the notification, reporting, and admin needs of teams using Microsoft Visual SourceSafe.
Free 30-day trial!


Teaching an Old CList Box New Tricks


by David Campbell

Questions, comments, suggestions - We want to hear from you!Comment on this article Get the Acrobat PDF File For This Article (23 KB) Get Adobe Acrobat Reader
Most downloads under 500KB


The Problem

While the newer versions of Visual Studio provide developers with many new controls and enhancements, with those enhancements comes the ocassional penalty of more code, and most definitely more complicated controls.

There is nothing simpler for displaying a sorted list of items to an end-user than the old standard CListBox. The CListBox is being used less and less though, because users have come to expect enhancements to the control. The most basic enhancement I like to see is what I call smart scrollbars. By smart scrollbars, I mean the scrollbars only appear when needed, and automatically size themselves. The vertical scrollbar already does this quite nicely, but the horizontal scrollbar support seems to leave something to be desired.


The Solution

There is a solution to the horizontal scrollbar issue, and it is not painful to program. Best of all, once debugged and inserted in your project, the solution can be propagated to every CListBox control by changing only a few lines of code for each CListBox in your project.

For those of you reading this that are already familiar with getting "Text Extents" and setting "Horizontal Extents," please continue reading. In the development of this text I uncovered a puzzle within a puzzle that you may find interesting.


The Example

To demonstrate the new class, I built a new project called "CustomCListBox" using the Visual Studio Application Wizard. I selected Dialog-based, and allowed the remainder of the options to remain at their default values. This is identical to the project from my CDialog article, so please refer to that if you need assistance.

For this demonstration, I inserted a CListBox into the main dialog of the application, gave it the ID IDC_LLISTTEST, and took the default for the About Box.


Populating the Control

Once the basic project is properly executing to ensure the code is correct, we begin to make changes for our testing purposes. Using the MFC Class Wizard, select the Member Variables tab, and add a member variable for the listbox. I used the name m_lListTest, and selected "Control" as the Category. This will give us a member automatically pointing to our control to allow us to populate the control from our application.

Now go to the ClassView tab of the Workspace window. Right-click on any toolbar to get a list of toolbars and select Workspace if it is not currently available. If you have named the project as I have stated above, under the CustomCListBox classes tree node will be a CCustomCListBoxDlg class. Expand that class and double-click the OnInitDialog() method to bring the code into the edit window. Find the commented line "TODO: Add extra initialization here", and add the following lines of code:


    m_lListTest.AddString(_T("One"));
    m_lListTest.AddString(_T("Two"));
    m_lListTest.AddString(_T("Three"));
    m_lListTest.AddString(_T("Four"));
    m_lListTest.AddString(_T("Five"));
    m_lListTest.AddString(_T("Six"));
    m_lListTest.AddString(_T("Seven"));
    m_lListTest.AddString(_T("Eight"));
    m_lListTest.AddString(_T("Nine"));
    m_lListTest.AddString(_T("Ten"));

The listbox I built was large enough to hold ten entries without needing scrollbars.

Notice that if you took the defaults for the properties of the listbox, the entries are sorted. If you would prefer to track the entries in sequence, go to the Resource View, and open up the Properties dialog for the CListBox. Deselect sort on the Styles tab. With this tab open, take notice that the default for the control is Vertical scroll selected.

Rebuilding and running the application will now display the entries in the order entered in OnInitDialog().


Demonstrating the Problem

To provide pleasant data display for end-users, I would prefer that the listbox was just wide enough to encompass the largest entry. If you cut-and-try one text line, for instance line seven, you can find a text length that will work for your particular dialog. In my example, I modifed the seventh line to be:


    m_lListTest.AddString(_T("This is line number
     seven (7) of the properly formatted lines"));

At this point, we have a perfectly formatted list control, and our user should be very happy, and impressed with the display. Unless there are eleven lines of data that is! To see what happens, add an eleventh line, rebuild and run the application.

The vertical scroll bar is indeed 'smart' in that it only appears when needed, however the width of the scroll bar itself causes clipping problems to happen horizontally on our most carefully calculated seventh line. Unfortunately Murphy's Law will probably hold true, and our end-user will end up with data longer than we expected in any case, so no manner of careful calculation can keep us from dealing with scrollbars.

As one more test, go back into the dialog, and select the listbox properties once more. Under Styles, select Horizontal scroll, rebuild and run the application. You'll now notice that the Horizontal Scroll bar does not appear. Checking the "Disable no scroll" property does put up a grayed horizontal scroll bar, but this also messes with the handling of the vertical that we liked so much above, so turn it back off, but leave Horizontal scroll checked.

Now we realize the delta between what the control displays for us as a default, and what we as the developer would like for it to do. All of this could be dealt with on a singular basis for this control, but I'd rather do this in a general case so it can be used across the application, and kept in my "pocket" for later use in other applications.


Building Another Derived Class

This section will be almost identical to that of the previous article. From the menu bar select Insert | New Class. I used CDJListBox as the Name and selected CListBox as the Base Class. Take all other defaults, and press OK. Visual Studio will build DJListBox.cpp and DJListBox.h and add them to our project.

To test the defaults, back in ClassView, under the CCustomListBoxDlg class, double-click the m_lListTest member variable to bring up the header file in the edit window.

Modify the line that reads:

    CListBox   m_lListTest;

To

    CDJListBox   m_lListTest;

Then, above the class declaration, add the line:

    #include "DJListBox.h"

Build and run, and no changes should be seen. As with the DJDialog class last time, our derived class is properly passing all requests to its base, or CListBox.

Note the "wiring in" of our new base class was much simpler this time in that only the header file needed a single change, and an include file.


Teaching Our ListBox New Tricks

The hard work is completed now. At this point we simply think through what it is we would prefer our listbox to do that it is not handling now.

Personally, I would prefer to have horizontal scroll bars appear and work similarly to the way our vertical ones work by default. The obvious choice of methods we need to handle in our base class is "AddString". Allowing Visual Studio to display the help file for AddString, we find the prototype is:

    int AddString( LPCTSTR lpszItem );

Add this method to our DJListBox.h file under the public implementation area just under the destructor prototype. Now in the DJListBox.cpp file, add a new member function at the end:


    int CDJListBox::AddString(LPCTSTR lpszItem)
    {
        return CListBox::AddString(lpszItem);
    }

Rebuilding and running once more should prove we have made no significant changes yet, but we're getting ready to!

First, from one call to the AddString method to the next, we need to keep track of the width of the longest line in the control. Add an integer member variable to the DJListBox.h file to keep track of this length. I added the following line just under the 'protected:' declaration, and above the MFC AFX_MSG code:

    int     m_nMaxWidth;

In the constructor for CDJListBox, in DJListBox.cpp, add the initialization for our width:

    m_nMaxWidth = 0;

Now we have to enhance our AddString method to enable the magic we intend to perform. Because the Vertical ScrollBar appears courtesy of CListBox, we first add our string in the base class, and save the return value for our return:

    int nRet = CListBox::AddString(lpszItem);

Next we are going to request information about the Vertical ScrollBar by using a GetScrollInfo call. The information requested is returned to us in a SCROLLINFO structure that we have to initialize. The following lines of code do that initialization and make our call:


    SCROLLINFO scrollInfo;
    memset(&scrollInfo, 0, sizeof(SCROLLINFO));
    scrollInfo.cbSize = sizeof(SCROLLINFO);
    scrollInfo.fMask  = SIF_ALL;
    GetScrollInfo(SB_VERT, &scrollInfo, SIF_ALL);

By watching the SCROLLINFO structure through the debugger, I have found that to get a valid result in the nMax and nPage variables, I need to already have one entry in the listbox. nPage keeps track of the number of items on a logical 'page' of our listbox, and nMax is the total number of items. Once nMax is greater or equal to nPage, we get a Vertical ScrollBar, and need to know the width of it. I preset an integer nScrollWidth to be zero, then fill that same integer with the width of the scrollbar if it is displayed.

    int nScrollWidth = 0;
    if(GetCount() > 1 && ((int)scrollInfo.nMax
        >= (int)scrollInfo.nPage))
    {
       nScrollWidth = GetSystemMetrics(SM_CXVSCROLL);
    }

Next we declare a SIZE variable, and instantiate a CCLientDC for our ListBox:

    SIZE      sSize;
    CClientDC myDC(this);

This is the point in the code that solves the 'puzzle within a puzzle'. We select either through default, or purposefully, a font for each dialog. This is seen in the dialog editor by right clicking on the dialog frame, and selecting properties. The default is MS Sans Serif, 8 points. My assumption was that if I get the CclientDC for the ListBox, that the selected font would be represented by the CclientDC. This has always given me unsatisfactory results, and that unsatisfactory code is running in applications in more than one country!

While working on this article, I decided to solve the puzzle. As it turns out, the only way I could change the length returned by GetTextExtentPoint32() was to change from small to large fonts in Control Panel. That told me that the font selected at dialog creation time was not 'selected into' the CclientDC. I further found that if I inserted a GetFont() call at this point in the code, and then selected the returned CFont pointer into my CclientDC, I magically get the correct value!

To continue, then, we first get a pointer to a CFont object which contains the font information for our ListBox display. If this returns a NULL pointer, we have no reason to continue.

    CFont* pListBoxFont = GetFont();
    if(pListBoxFont != NULL)
    {

Given a good return from GetFont(), we select the CFont pointer into our CclientDC.


        CFont* pOldFont =
                myDC.SelectObject(pListBoxFont);

Next, we use the GetTextExtendPoint32 function to fill in our SIZE structure we declared above. We are interested in the cx member, which is the width of our string as just added to the CListBox. Our maximum width is compared to our line width, and adjusted appropriately.


        GetTextExtentPoint32(myDC.m_hDC, lpszItem,
            strlen(lpszItem), &sSize);

        m_nMaxWidth = max(m_nMaxWidth, (int)sSize.cx);

All that remains now is to tell our scrollbar how big to display itself. This is done by calling SetHorizontalExtent(). As it turns out, if the width sent to this function is the same or less than the width of the ListBox itself, the Horizontal ScrollBar will be turned off. This is the action we were wanting.

One more piece of magic remains, however. If you examine the data in a CListBox very closely, you'll notice a 'gutter' to the left of the displayed text. This gutter is 3 pixels. To get our extent to work correctly, those 3 pixels need to be added to the max width. If you would also like a 3 pixel gutter to the right, add 3 more.

        SetHorizontalExtent(m_nMaxWidth + 3);

Before we leave, we need to clean up the CClientDC by selecting our font back out. The font is selected out of the CClientDC by selecting the old one back in:

        myDC.SelectObject(pOldFont);     }

     return nRet;

Compiling and executing this code should display the required display of scrollbars in our CListBox.


Enhancements

Some enhancements that I have found in a real-world environment is to also handle InsertString() and DeleteString(). The sample code shows radio buttons and a check box on the main dialog display. To get the interaction correct between the controls and the CListBox, I also had to handle the RemoveAll() method.


Conclusion

I believe once you've seen this derived ListBox in action, you will appreciate the enhancement to the standard CListBox, and hopefully whet your appetite for more of the same. Your end-users will also appreciate the extra effort you put forth to enhance their data display.

Developed Under:

Visual C++ 6.0 Service Pack 5


Featured Article

An Introduction to C#
By Joey Mingrone

Register Today!


100% FREE

Members enjoy these benefits:
Access to ITI Downloads
Access to more articles and tutorials
Optional weekly newsletter
And more...

Click here to register
Or
Click here to log in



© 2008 Interface Technologies, Inc. All Rights Reserved
Questions or Comments? devcentral AT iticentral DOT com
PRIVACY POLICY