Borland Online And The Cobb Group Present:


February, 1996 - Vol. 3 No. 2

Owner-drawn list boxes in OWL

by Kent Reisdorph

List boxes are among Windows' most useful controls. They offer a consistent way to display data and to allow user-selection of data. Sooner or later, though, you'll need to go beyond displaying simple text. You may need to display formatted text, bitmaps, or a combination of the two. Owner-drawn list boxes allow you to present information in a format of your own design. Owner-drawn list boxes are everywhere. For example, when you choose to open a file in most Windows applications, the dialog box that appears uses an owner-drawn list box to display folder icons next to directory names. Similarly, the Windows 3.1 File Manager uses two owner-drawn list boxes to display file information, as shown in Figure A.


Figure A - File Manager uses owner-drawn list boxes to display the contents of disks and directories.

Perhaps most obviously, the Borland C++ Integrated Development Environment (IDE) project window is an owner-drawn list box. If you think about it, you see such list boxes in many commercial applications. In this article, we'll show you how to achieve those effects in your OWL applications.

Owner drawing isn't rocket science

At first, the thought of being responsible for drawing all the contents of a list box might send chills down your spine. Take heart­­it really isn't that difficult! You just have to know a few basics and you'll be adding owner-drawn list boxes to your applications in no time at all.

Owner-drawn list boxes come in two flavors­­fixed, in which all items are the same height (LBS_OWNERDRAWFIXED), and variable, in which each item can be a different height (LBS_OWNERDRAWVARIABLE). In addition, the list box can use strings (LBS_HASSTRINGS) or not. See Owner-drawn list boxes without strings, for information about list boxes that store pointers to objects instead of using strings.

OWL makes owner drawing of controls fairly painless. The OWL class TControl, from which all of the control classes are derived, has several functions that aid in owner drawing. These functions are ODADrawEntire(), ODAFocus(), ODASelect(), CompareItem(), DeleteItem(), DrawItem(), and MeasureItem(). We'll be concentrating on the ODA functions, as they're the heart of owner-drawn controls in OWL. These functions take a reference to an instance of the DRAWITEMSTRUCT structure, which contains all of the information necessary to draw each item in the list box.

As a bonus, we'll see how to query Windows for the colors we should use to show a list box item in the selected state. We do this with the Windows API function GetSysColor(), which returns a COLORREF value that you can use to create TBrush or TColor objects. As an example, we'll create a list box that displays all of the bitmap (BMP) files in your WINDOWS directory, along with their filenames and size in pixels.

Getting started

A list box is considered owner-drawn if you have set either the LBS_OWNERDRAWFIXED or LBS_OWNERDRAWVARIABLE style bits. If your list box is in a dialog box that you're creating in Borland's Resource Workshop, you can set the style bit via the List Box Style dialog box. Click either Fixed or Variable in the Owner Drawing section, depending on the type you require. If you're not using Resource Workshop or if your list box will appear in a window, set the appropriate style in the list box's constructor via the Style member of the Attr structure. For example,

Attr.Style |= LBS_OWNERDRAWFIXED | 
              LBS_HASSTRINGS;

will set up a fixed-height owner-drawn list box.

Once you've told Windows that the list box will be owner-drawn, you must then override three OWL functions to get owner drawing to work. These functions are virtual functions of TControl, so you only have to provide the functions; OWL takes care of the rest. Let's take a look at those functions.

ODA functions at a glance

The three functions that you'll be concerned with are ODADrawEntire(), ODAFocus(), and ODASelect(). Of these functions, the first two are the most important.

Your program calls ODADrawEntire() once for each element in the list box. You'll use ODAFocus() to draw the focus rectangle around an item when the list box receives focus.

Normally your program would call ODASelect() whenever the user selects a particular item in the list box. You don't have to call ODASelect(), however, because the itemState member of DRAWITEMSTRUCT tells you which item the user has selected. Thus we can simply have ODASelect() call ODADrawEntire().

DRAWITEMSTRUCT contains other members that you'll need to know about before we jump into our example. Let's take a brief look at some of them.

DRAWITEMSTRUCT

DRAWITEMSTRUCT looks like this:

typedef struct tagDRAWITEMSTRUCT {
	UINT  CtlType;
	UINT  CtlID;
	UINT  itemID;
	UINT  itemAction;
	UINT  itemState;
	HWND  hwndItem;
	HDC   hDC;
	RECT  rcItem;
	DWORD itemData;
} DRAWITEMSTRUCT;

For now, we'll concern ourselves with only four of this structure's data members. Those members are itemID, itemState, hDC, and rcItem.

itemID represents, of course, the index of an item in the list box. itemState is an unsigned integer that indicates the state of the item to be drawn. You can set bit flags to include the constants ODA_DRAWENTIRE, ODA_FOCUS, and ODA_SELECT. It's the ODA_SELECT bit that we'll watch in order to perform the bypass of the ODASelect() function that we mentioned earlier.

hDC is just what it appears to be­­a handle to the device context (DC) of the list box's client area. You'll use this DC to draw the text and display the bitmap for each item in the list box.

The rcItem member is the last part of DRAWITEMSTRUCT that we'll discuss for now. rcItem is a RECT structure containing the rectangle of the current item. You'll need this since you'll call ODADrawEntire() once for each item in the list box. (We could get this information in other ways, but since it's already in our DRAWITEMSTRUCT we're saved a call to GetItemRect().)

Enough talk­­let's do it!

Listing A contains the code for our example class, which we'll call ODAListBox. This class, as previously mentioned, will display any bitmap files found in the WINDOWS directory.


Listing A: ODALBOX.CPP

#include <owl\owlpch.h>

class ODAListBox : public TListBox {
  public:
    ODAListBox();
    void SetupWindow();
    void ODASelect(DRAWITEMSTRUCT far& drawInfo);
    void ODAFocus(DRAWITEMSTRUCT far& drawInfo);
    void ODADrawEntire(DRAWITEMSTRUCT far&
                       drawInfo);
    int itemHeight;
};

ODAListBox::ODAListBox()
  : TListBox(0, 0, 0, 0, 0, 0, 0)
{
  Attr.Style |= LBS_OWNERDRAWFIXED | 
                LBS_HASSTRINGS | 
                LBS_NOINTEGRALHEIGHT;
  Attr.Style &= ~(WS_BORDER);
}

void
ODAListBox::SetupWindow()
{
  TListBox::SetupWindow();
  char path[128];
  GetWindowsDirectory(path, sizeof(path));
  strcat(path, "\\*.bmp");
  DirectoryList(DDL_READWRITE, path);
  path[strlen(path) - 5] = 0;
  strcat(path, "argyle.bmp");
  TDib* dib = new TDib(path);
  itemHeight = dib->Height();
  SetItemHeight(0, itemHeight);
  delete dib;
}

void
ODAListBox::ODASelect(DRAWITEMSTRUCT far& di)
{
  ODADrawEntire(di);
}

void
ODAListBox::ODAFocus(DRAWITEMSTRUCT far& di)
{
  ::DrawFocusRect(di.hDC, &di.rcItem);
}

void
ODAListBox::ODADrawEntire(DRAWITEMSTRUCT far& di)
{
  TRect rect(di.rcItem);
  TDC dc(di.hDC);
  char path[128];
  GetWindowsDirectory(path, sizeof(path));
  strcat(path, "\\");
  char filename[128];
  GetString(filename, di.itemID);
  strcat(path, filename);
  TDib* dib = new TDib(path);
  TBitmap* bitmap = new TBitmap(*dib);
  TMemoryDC* memdc = new TMemoryDC(dc);
  memdc->SelectObject(*bitmap);
  dc.SetBkMode(TRANSPARENT);
  if (di.itemState & ODS_SELECTED) {
    dc.FillRect(rect, 
        TBrush(GetSysColor(COLOR_HIGHLIGHT)));
    dc.SetTextColor(
       GetSysColor(COLOR_HIGHLIGHTTEXT));
  }
  else {
    dc.FillRect(rect, 
        TBrush(GetSysColor(COLOR_WINDOW)));
    dc.SetTextColor(
       GetSysColor(COLOR_WINDOWTEXT));
  }
  if (bitmap->Height() > itemHeight)
    dc.StretchBlt(rect.left, rect.top, 
                  itemHeight, itemHeight,
                  *memdc, 0, 0, bitmap->Width(), 
                  bitmap->Height());
  else
    dc.BitBlt(rect.left, rect.top, 
              bitmap->Width(), bitmap->Height(), 
              *memdc, 0, 0);
  rect.left = itemHeight + 5;
  dc.DrawText(filename, -1, rect, DT_SINGLELINE | 
              DT_LEFT | DT_VCENTER);
  wsprintf(filename, "Size: %d X %d", 
           bitmap->Width(), bitmap->Height());
  rect.left += 125;
  dc.DrawText(filename, -1, rect, DT_SINGLELINE | 
              DT_LEFT | DT_VCENTER);
  if (di.itemState & ODS_FOCUS) {
    dc.DrawFocusRect(rect);
  }
  delete dib;
  delete bitmap;
  delete memdc;
}

class TTestApp : public TApplication {
  public:
    TTestApp(const char far* title) :
     TApplication(title) {}

    void InitMainWindow()
    {
      SetMainWindow(new TFrameWindow(0, 
        "Owner-drawn Listbox Test App", 
        new ODAListBox));
    }
};

int OwlMain(int /*argc*/, char* /*argv*/ [])
{
  TTestApp app("Test App");
  int ret = app.Run();
  return ret;
}

Let's examine Listing A. Note first that we've set the Attr.Style member of the class to indicate that we'll be drawing the list box ourselves. Also, we've included the LBS_NOINTEGRALHEIGHT style bit. This style ensures that the list box covers its entire client area when you're using the list box in a window. If you don't use this style, you may wind up leaving the bottom border of the list box showing.

Our SetupWindow() function loads the list box. First, we get the WINDOWS directory path from the Windows API function GetWindowsDirectory(). We then add the backslash and the filespec for Windows bitmap files, *.BMP. Next, the call to the TListBox function DirectoryList() loads the list box with any files matching the filespec we provided. The list box now contains the filenames of all BMP files in the WINDOWS directory.

The last four lines of SetupWindow() set the height of the items in the list box using the TListBox function SetItemHeight(). Since we're using a fixed-height owner-drawn list box, we have to set the item height only once, using the item ID of 0. To determine the height of the items, we create a device-independent bitmap object (TDib) from the Windows wallpaper bitmap, ARGYLE.BMP, which should be present on the typical Windows system. We'll pass the height of the ARGYLE bitmap to SetItemHeight(). Then we'll save the height of ARGYLE.BMP because we'll need it later when we draw the items in the list box.

As we mentioned earlier, our ODAFocus() and ODASelect() functions are pretty simple. Our ODASelect() function simply calls ODADrawEntire():

void 
ODAListBox::ODASelect(DRAWITEMSTRUCT far& di)
{
   ODADrawEntire(di);
}

ODAFocus() just draws the focus rectangle:

void ODAListBox::ODAFocus(DRAWITEMSTRUCT far& di)
{
   ::DrawFocusRect(di.hDC, &di.rcItem);
}

Since we're performing just one action in ODAFocus(), we'll use the API version of DrawFocusRect(). We could use the OWL version, but that would require creating a TDC and a TRect, so we won't bother. As a side note, DrawFocusRect() uses raster style code R2_NOT. This code indicates that your application can draw the rectangle once to display it and again to make it disappear. Next, we'll look at how it all comes together in the ODADrawEntire() function.

The heart of the system

The ODADrawEntire() function is the real heart of the owner-drawn list box. As we mentioned earlier, the DRAWITEMSTRUCT contains the handle to a device context (HDC) that we can use to do any and all drawing. We'll create a TDC class from the HDC passed to us so we can use OWL's drawing functions. We'll also create a TRect from the rectangle passed in the DRAWITEMSTRUCT data member rcItem to simplify our rectangle manipulation chores.

Let's look at the ODADrawEntire() function in Listing A. We first create our TDC and TRect objects. Next, we get the WINDOWS path as we did in the SetupWindow() function. Then, we use the TListBox function GetString() to retrieve the filename for this item. Remember that we call ODADrawEntire() once for each item in the list box that we need to draw. The DRAWITEMSTRUCT data member itemID contains the ID of the item currently being drawn. We use the itemID in GetString() to retrieve the proper string for the current item.

Now that we've obtained the filename of the bitmap we want to display for this item, we have to prepare to display it. We create a TDib object using the path and filename we just fetched. Next we create a TBitmap using the TDib.

At this point, we create a memory device context object­­a TMemoryDC from the device context that the DRAWITEMSTRUCT passed to us­­and we select the bitmap into the memory DC. The statement

dc.SetBkMode(TRANSPARENT);

sets the background mode to transparent, so that the color behind any text we draw shows through. Now we're ready to do some drawing.

The next step is to determine whether or not the user has selected the current item. We can determine that by checking the ODS_SELECTED bit in DRAWITEMSTRUCT's itemState data member. If the ODS_SELECTED bit is set, then we need to draw the item as selected.

Play it safe

In programming for Windows, it's never safe to assume that a particular color will be either available or compatible with the colors set up on a user's system. For that reason, you should never arbitrarily choose a color for drawing the rectangle showing the selected state. Instead, you should always use the colors chosen in the Windows setup. You can retrieve these colors by calling the GetSysColor() function with the appropriate bits set. The next several lines in Listing A demonstrate the use of GetSysColor().

Note that if the item has been selected (that is, the ODS_SELECTED bit is set), then we fill the rectangle passed to us in rcItem with the color that GetSysColor() returns to us when we query Windows for the highlight color. We do this by creating a temporary TBrush object in the FillRect() function. We also set the text color of this device context to the text highlight color. If the current item is not selected, then we fill the rectangle with the normal background color and set the text color to the normal foreground color. We'll do the actual drawing of the text a little later.

After drawing the rectangle with the proper color, the next few lines in Listing A display the actual bitmap on the screen. We first check to see if the bitmap is larger than our list box item height by testing it against the itemHeight variable that we saved from SetupWindow(). If the bitmap is larger than the item height, then we need to use StretchBlt() to shrink the bitmap to fit. If it isn't, then we can use BitBlt() to make the transfer to the screen device context. We could have used StretchBlt() in all cases, but StretchBlt() is much slower than BitBlt(), so we don't use it unless we need to.

Having drawn the bitmap, we now need to draw the text. We first offset the left edge of the rectangle by the width of our bitmaps plus an arbitrary margin of 5 pixels. We can now use DrawText() to display the text using the drawing rectangle. DrawText() is very handy in this situation because it allows us to easily center the text vertically in the item rectangle.

After drawing the filename on the screen, we offset the left edge of the rectangle by 125 pixels and display the width and height of the bitmap. (In a real application, you would calculate the actual width of the filename to determine where to start displaying the size information, but for an example program like this we cheat a little and just use a constant.)

We chose to draw the bitmap's size information to demonstrate that you can display any information you want in an owner-drawn list box. You're not limited to displaying just the text that's contained in the list box item.

Finally, if the item we're drawing has focus, then we draw the focus rectangle on the screen. Note that we clean up our drawing objects when we finish with them so we don't have any memory leaks.

The remainder of Listing A is the usual OWL startup code. The TTestApp class just creates a TFrameWindow and adds an instance of ODAListBox as its client window. When you compile and run the application, you'll see that the main window displays the bitmaps and their respective filenames, as shown in Figure B.


Figure B - The example application displays bitmaps and filenames in an owner-drawn list box.

Conclusion

Owner-drawn list boxes are a very effective way to add visual impact when presenting your data. As we've shown here, it's not hard to achieve visually pleasing results with a minimum of code.

Kent Reisdorph is a professional C++ developer and a member of TeamB, Borland's online support team. You can contact Kent via CompuServe at 75522,1174.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.