May 1998

Owner-drawn list boxes

by Matt Telles

Every time I work with a Windows list box control, the same thing happens--I want more! Most recently, I wanted to be able to change the color in which the list box displayed items. It seemed like a good idea to have the more interesting (and appropriate) items highlighted in red, while the rest of the items were black. The problem with this desire, of course, is that the list box simply wasn't made to handle different colors in its display. There's a solution to this dilemma, though, and its name is owner-drawn list boxes.

What does "owner-drawn" mean?

Normally, when you work with list boxes, you add strings to the list box and Windows does the rest. Each item is displayed by the code that makes up the list box control within Windows. When you use an owner-drawn list box, however, all the display code falls within your control. Owner-drawn list boxes allow you to display each item any way you want. This can mean drawing check boxes for the items (as the Delphi 3.0 TChecklistBox component does) or changing the color for each item, as I wanted to do. Let's work through an example by creating a TColorListBox component.

Creating the color list box component

The first step in building a list box whose colors you can change is to create a component that encapsulates the problem. To do this, you create a component based on the TCustomListBox. If you've never created a component from scratch, don't fret, it's easy! (For an overview of the process, see the three-part "Building Components" article series in the September, October, and November issues of C++Builder Developer's Journal.) First, select Component | New from the main IDE menu. You'll see a dialog box that asks for the name of your new component; a combo box lets you select the base class to extend. Enter TColorListBox as the new component's name, then select TCustomListBox for the base class and click OK. The wizard will do the rest, generating the basic files needed for creating and registering the component in the C++Builder system.

Making it owner drawn

Once you've created the new component, the next step is to make Windows let you draw each element of the component. If you were adding a list box to a form, you could do this by selecting the Style property and changing its value to Owner Draw Variable. For this component, though, you want the owner-drawn style to be automatic. You must be able to modify the style of the control before it's displayed onscreen. There are several places you could do this, but it's easiest in the class constructor. Open the component unit in the editor, and you'll see the constructor as one of the first class methods. Add the following code to the constructor:
__fastcall TColorListBox::TColorListBox(TComponent* Owner)
  : TCustomListBox(Owner)
{
    Style = lbOwnerDrawVariable;
}

s This code simply tells the underlying list box component that it will be of the owner-draw variable style. The owner-draw part indicates to the component that you--the programmer--will handle the drawing part of the display. The variable part indicates that you'd also like control over the height of each item in the list. This step isn't strictly necessary for this component, but you might as well learn about it!

Handling the item size

When the underlying Windows list box is displayed, it first finds out how much space to reserve for each item. It does so by sending the list box a message that requests the size of the item currently being drawn. For the TCustomListBox class, the MeasureItem method handles this message. Since you made your list box variable size, this method will be called for each item in the list. Here's one implementation for the MeasureItem method:
void __fastcall TColorListBox::MeasureItem(
                int Index, int &Height)
{
    Height = 12;
}
In this case, you simply want to make each item be a certain size: 12 pixels high. Note that you pass the Height parameter by reference, allowing it to be modified within the method and returned to the calling function.

Note: Changing the font

If you want to change the font for each item in the list box as well as the item's color, you can determine the height of each item by checking the font that you assigned to it.

Drawing the item

Once the item has been measured and fitted for its little square of the list box, the next step is to actually draw that item into its drawing area. You accomplished this task using the DrawItem method of the TCustomListBox class. Add a new method to the class by modifying the source and header file for the new TColorListBox class. The complete code for the new method is as follows:
void __fastcall TColorListBox::DrawItem(
                int Index, const Windows::TRect 
                &Rect, TOwnerDrawState State)
{
   Canvas->Font->Color = GetColor(Index);
   Canvas->TextRect(Rect, Rect.Left, Rect.Top, 
                    Items->Strings[Index].
                    c_str());

}
There isn't a lot to the method. You get the color for your index by passing the index of the item (given to you by the list box itself) to the GetColor method. This method, which you'll write very shortly, simply returns the color assigned to a given list box item.

Once you have the color, you just use the TextRect method of the Canvas property to display the string in the area given to you for this item. The area is defined by the height of the item (returned by MeasureItem) and the width of the list box. Given this information, you can easily display the string in your desired color.

Note that you use the text stored in the list box's Strings property. Doing so permits the list box to work exactly like a normal list box, but with added functionality!

Adding the Color property

The last step to make this a fully functioning component is to add the Color property. Normally, you'd make this a published property and let the form designer enter the values for the colors at design time. In our example, you won't take that route. In order to implement the Color property correctly, it needs to be an array of colors that refer to the strings in the list box. Creating an array property is simple enough, as you'll see, but making an editor for that property is a non-trivial matter. For this reason, you'll make the property runtime only. (The colors for the list box are useless without strings, which provides additional motivation.)

To add the new property, you must first decide how to represent it internally. The internal representation may have nothing to do with the external representation to the programmer, but it will drive how that external representation works. In this case, since there's an indeterminate number of entries, I decided to use the Standard Template Library (STL) vector class to represent the colors. To do so, add the following section to your header file for TColorListBox:

private:
   std::vector<TColor> FColors;
Next, you need to add the Color property definition to the class, using a public entry like the following:

 

__property TColor Colors[int nIndex] = 
  {read=GetColor, write=SetColor};
Again, there's nothing surprising here. The property is an array, so it takes an index to represent the number of the item for which to set the color. You also need to add the SetColor and GetColor methods. Here's the header file entry for these methods:
protected:
virtual void __fastcall SetColor(
                       int Index, TColor clr );
  virtual TColor __fastcall GetColor(
                            int Index );
Finally, you need to implement the SetColor and GetColor methods. Listing A contains the relevant code extracts, which go in the source file (TColorListBox.cpp).

Listing A: The SetColor and Get Color methods

void __fastcall TColorListBox::SetColor(
                int Index, TColor clr)
{
   if ( Index < 0 )
      return;
   // See if we already have this many elements
   // in the array. If we don't do this it will
   // throw an exception when we try to set the
   // color.
   if ( Index < FColors.size() )
      FColors[Index] = clr;
   else
   {
      // Hold onto the current size
      int nCurSize = FColors.size();
      // Resize the array
      FColors.resize( Index+1 );
      // Initialize the remainder of the
      // entries to black (our default)
      for ( int i=nCurSize; i<=Index; ++i )
          FColors[i] = clBlack;
// Finally, set the one they want      
      FColors[Index] = clr;
   }
}

TColor __fastcall TColorListBox::GetColor(int Index)
{
   if ( Index >= 0 && Index < FColors.size() )
      return FColors[Index];
   return clBlack;
}
The code is pretty straightforward. For the set case, you simply check to see if that many entries have already been created. If so, the entry the user wants to set replaces the one already in the array. If the entry doesn't already exist, the array is resized to the proper size and all elements are initialized to the default black color.

For the get case, which we also use in the drawing code, you once again consult the array to see whether the element is already there. If so, you return the value at that location. If the array isn't yet that size, the user hasn't set a color for this item, and you return the default black color. Note that in all cases, you check for invalid entries, such as an index less than zero. Doing so is just good programming practice that you should use in your own coding.

Conclusion

That's all there is to it! If you complete this code exercise and install the component, you'll have a complete color list box that you can use in your own applications. Good luck and happy coding!