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.
At first, the thought of being responsible for drawing all the contents of a list box might send chills down your spine. Take heartit 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 flavorsfixed, 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.
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.
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 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 bea 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().)
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 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 objecta
TMemoryDC from the device context that the DRAWITEMSTRUCT
passed to usand 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.
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.
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.
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.