Displaying a Hierarchy in a List Box

Kyle Marsh
Microsoft Developer Network Technology Group

Created: December 22, 1992

Click to open or copy the files in the HierList sample application for this technical article.

Abstract

This article explains how to create a hierarchical list box that includes icon bitmaps and connecting lines. It describes what an application must do to display the hierarchy and discusses some helper functions I developed specifically to make the process easier for the application developer. HierList, the sample application included with this article, implements a hierarchical list box with these helper functions.

Introduction

Many applications need to display information to a user in a hierarchical, or outline, format. In the Microsoft® Windows™ operating system, the best-known example is the list of directories in the File Manager. The directory structure of a disk is a hierarchical data structure. Databases often contain hierarchical data as well; for example, the relationship between managers and the people they manage is a hierarchical relationship.

A common way to display a hierarchy is with a tree diagram, as in Figure 1.

Figure 1. A tree diagram

Unfortunately this display requires a lot of work to program, especially when a level changes. Each box, line, and text string must be placed and drawn. When a change occurs to the data (for example, a new vice president is added), the entire display must be recalculated and displayed. Tree diagrams also take up a lot of screen space and, when there is a lot of data to display, the individual boxes become so small that the writing inside becomes unreadable.

An alternate way to display the data is to turn the tree diagram on its side, as in Figure 2.

Figure 2. A turned tree diagram

This technique requires less space. It is a little easier to program than the tree diagram, but it still requires quite a bit of development effort. Text and lines must still be placed and drawn, but calculating the positions is simpler because you don't have to worry about centering the boxes and lines as in Figure 1.

Another option is to display the data in a list, as in Figure 3.

Figure 3. A hierarchical list

This version is not quite as easy to read as the tree diagram, or even the tree diagram turned on its side, but it has one big advantage: It can be programmed using an owner-drawn list box. If you capitalize on some easily used features of Windows-based programming, you can implement a hierarchical data structure that looks like the one in Figure 4.

Figure 4. A list box displaying a hierarchical list

With the addition of the icons, lines, and color, the list box display becomes almost as visually appealing as the tree diagram, but it requires a lot less programming effort. The display uses a standard list box control, so it can be easily added to a dialog box or other window—an additional advantage.

Displaying a Hierarchy

Let's go through the steps involved in displaying a hierarchy in a list box. To make things easier I wrote some helper functions that any application can use. These functions are described in "The Helper Functions" section later in this article.

Here are the steps needed to display a hierarchy using the helper functions:

Designing the Hierarchy

An application developer must first design the appearance of the hierarchy. The developer determines what types of items are in the hierarchy and how the icons that represent these items should look. A hierarchical display usually needs at least two icons:

The display may use additional icons, and they may vary. The File Manager can display plain open and closed folders or, at the user's option, it can indicate which directories have subdirectories by displaying a closed folder with a "+" on it. The color of the icon can also vary. The Microsoft Developer Network CD's index window uses color to add visual variety to the index display. When choosing color combinations for a hierarchy, be sure to consider that many laptop and palmtop computers do not have color capability. Pick combinations that look good in monochrome and shades of gray.

Once a developer knows which types of items will be displayed and establishes the corresponding icons and their variations, he or she can create a bitmap that contains the icons. Using one bitmap that contains all the icons (an icon bitmap) improves performance of the owner-drawn functions. The icons should be arranged in rows and columns within the bitmap; therefore, all icons must be the same size.

There should be a column for each icon type and a row for each variation. This is not a hard and fast rule, however, because the application will designate the row and column numbers to determine which icon the drawing helper function will use to draw a particular item. Figure 5 shows the bitmap used in the sample application.

Figure 5. A sample icon bitmap

The helper functions require the following:

Once the bitmap is assembled, it must be linked into the application as a bitmap resource. For example, you could include the following line in the application's .RC file:

IDR_HIERICONS bitmap Hiericon.bmp

Creating and Initializing an Owner-Drawn List Box

The application must create a list box with the LBS_OWNERDRAWFIXED style and without the LBS_HASSTRINGS and LBS_SORT styles, either by using a dialog box or by calling CreateWindow (or CreateWindowEx) directly. For example, you could use the following statement in a .DLG file:

LISTBOX    IDLIST, 14, 10, 165, 195, LBS_OWNERDRAWFIXED |
           LBS_WANTKEYBOARDINPUT | WS_VSCROLL

Or you could use this call to CreateWindow:

CreateWindow("LISTBOX", NULL, 
             LBS_OWNERDRAWFIXED | LBS_WANTKEYBOARDINPUT | 
                WS_VSCROLL,
             14, 10, 165, 195,
             hwndParent, NULL, _hInstance, NULL);

If you use the dialog box method, the application calls the HierDraw_DrawInit helper function to initialize the list box. A dialog procedure's WM_INITDIALOG handler cannot call this function because Windows sends a WM_MEASUREITEM message to a list box with the LBS_OWNERDRAWFIXED style before the dialog manager sends a WM_INITDIALOG message to the dialog procedure. The item height, which must be returned in response to the WM_MEASUREITEM message, is either the height of the text or the height of the bitmap, whichever is greater. Calling the initialization function before creating the dialog box provides a value (the bitmap height) that the list box can return with the WM_MEASUREITEM message. If the list box uses an alternate font, the dialog procedure can send a WM_MEASUREITEM message to the list box to adjust the item height when the dialog manager sends a WM_SETFONT message to the dialog procedure.

The application must tell HierDraw_DrawInit the ID of the bitmap resource and how many rows and columns of icons there are in the bitmap. The helper functions also need the following information from the application:

The following code shows how you can use HierDraw_DrawInit and other helper functions to initialize a list box within a dialog box:

HIERDRAWSTRUCT HierDrawStruct;

int HLDialog_Do(HWND hwndOwner)
{
    int result;
    DLGPROC lpfndp;

    //
    // Initialize the HierDraw stuff.
    // Need to do this here so we have a value for WM_MEASUREITEM,
    // which is sent before the WM_INITDIALOG message.
    //
    HierDraw_DrawInit(_hInstance,        // Instance.
                      IDR_LISTICONS,     // Bitmap resource ID.
                      4,                 // Rows in the bitmap.
                      3,                 // Columns in the bitmap.
                      TRUE,              // Draw connecting lines
                                         // between items?
                      &HierDrawStruct,   // HierDrawStruct.
                      TRUE               // Initialize the list box.
                      );

    lpfndp = (DLGPROC)MakeProcInstance((FARPROC)HLDialog_DlgProc, 
                                       _hInstance);

    result = DialogBox(_hInstance,
            MAKEINTRESOURCE(IDHIERDLG),
            hwndOwner, lpfndp);

    FreeProcInstance((FARPROC)lpfndp);
    HierDraw_DrawTerm(&HierDrawStruct);

    return result;
}

Adding Items to the List Box

The application must store or access the hierarchical data and identify an item in the hierarchy with a 32-bit data value. This value can be a pointer to where the item is stored in memory, a record identifier for a database, an offset in a file, or any other value that the application can use to uniquely identify an item. The 32-bit data value is added to the list box with an LB_INSERTSTRING message. Use the application-supplied 32-bit data value in the lParam parameter in the SendMessage call instead of a far pointer to a string.

Handling the WM_DRAWITEM Message

The HierDraw_OnDrawItem helper function does the drawing work for the application. The application calls this function whenever Windows sends the application a WM_DRAWITEM message. The application must pass these arguments to HierDraw_OnDrawItem:

Here is the procedure that handles drawing an item for the HierList sample application, which displays the hierarchy of numbers shown in the above figure:

void HLDialog_OnDrawItem(HWND hwnd, DRAWITEMSTRUCT FAR* lpDrawItem)
{
   char  szText[128];
   DWORD dwData;
   int   nLevel;                // The level of the item.
   int   nTempLevel;
   int   nRow;                  // The row in the bitmap 
                                // for the item's icon.
   int   nColumn;               // The column in the bitmap 
                                // for the item's icon.
        
   DWORD dwConnectLevel = 0L;
   DWORD dwMask;
   DWORD dwLevelMask;
   DWORD dwLevelAdd;

   dwData = lpDrawItem->itemData;
   wsprintf(szText,"Item Number: %ld",dwData);

   //
   // Select the correct icon, open folder, closed folder, or 
   // document.
   //
   // Can this item be opened?
   //
   if ( dwData % 10 == 0 ) {
      //
      // Is it open?
      //
      if ( HierDraw_IsOpened(&HierDrawStruct, dwData) ) {
         nColumn = 1;
      }
      else {
         nColumn = 0;
      }
   }
   else {
      nColumn = 2;
   }

   //
   // Select the correct level/color.
   // This sample changes the row of the bitmap (color) 
   // just because it can.
   //
   if ( dwData % 1000 == 0 ) {
      nLevel = 0;
      nRow = 0;
   }
   else if ( dwData % 100 == 0 ) {
      nLevel = 1;
      nRow = 1;
   }
   else if ( dwData % 10 == 0 ) {
      nLevel = 2;
      nRow = 2;
   }
   else {
      nLevel = 3;
      nRow = 3;
   }

   //
   // Now build dwConnectLevel, which tells the helper function
   // the levels for this item that need connecting lines.
   // If this item is the last child or one of its parents
   // is a last child, it does not need a connector at that level.
   //
   if ( nLevel == 0 ) {
      //
      // Level 0 items never have connectors.
      dwConnectLevel = 0L;
   }
   else {
      //
      // Check parents (grand, great, ...) to see it they are last 
      // children.
      //
      // Start at parent (one level up from here).
      //
      nTempLevel = nLevel-1;
      
      //
      // First bit to set (or not).
      //
      dwMask = (DWORD) pow(2,nLevel-1);
      
      dwLevelMask = (DWORD) pow(10,4-nLevel);
      dwLevelAdd  = dwLevelMask / 10;

      //
      // While we are not at level 0.
      //
      while ( nTempLevel >= 0 ) {
         //
         // Last child at this level?
         //
         if ( ((dwData+dwLevelAdd)%dwLevelMask) == 0 ) {
            //
            // Last kid, so no connection at this level.
            //
            dwConnectLevel &= ~dwMask;
         }
         else {
            //
            // NOT last kid, so connection at this level.
            //
            dwConnectLevel |= dwMask;
         }

         //
         // Stuff for this sample, figure out next parent.
         dwLevelAdd  *= 10;
         dwLevelMask *= 10;
         dwData = dwData-(dwData%dwLevelAdd);
         
         //
         // Move mask bit over.
         //
         dwMask /= 2;
         
         //
         // Move up one level.
         //
         nTempLevel--;
      }
   }
   
   //
   // All set to call drawing function.
   // 
   HierDraw_OnDrawItem(hwnd,
                       lpDrawItem,
                       nLevel,
                       dwConnectLevel,
                       szText,
                       nRow,
                       nColumn,
                       &HierDrawStruct);
   return;
}

Handling the WM_SYSCOLORCHANGE Message

The helper functions change the background of the icon bitmaps to the current window background color (COLOR_WINDOW). If the user changes the color of the window background, the icon bitmap background colors must also change. The application calls the HierDraw_DrawInit function with the last argument set to FALSE to change the icon bitmap backgrounds to the new window background color. For example, the application's main window procedure could handle the WM_SYSCOLORCHANGE message as follows:

        case WM_SYSCOLORCHANGE:
                HierDraw_DrawInit(_hInstance,     // Instance.
                  IDR_LISTICONS,    // Bitmap ID.
                  ROWS,             // Rows.
                  COLS,             // Columns.
                  TRUE,             // Loins.
                  &HierDrawStruct,  // HierDrawStruct.
                  FALSE             // DON'T initialize open 
                                    // folders.
                  );
            return TRUE;
        break;

Handling the WM_SETFONT Message

If the list box or dialog box receives a WM_SETFONT message specifying the font for drawing text, the application must call the HierDraw_DrawSetTextHeight helper function:

        case WM_SETFONT:
           {
             //
             // Set the text height.
             //
             HierDraw_DrawSetTextHeight(hwndDlg,
                               IDLIST,
                               (HFONT) wParam,
                               &HierDrawStruct);
           }
        break;

Handling Opening and Closing an Item

The usual behavior for a hierarchical list box is:

Four helper functions aid an application in opening and closing items:

Here is the HierList sample application's open item and close item function:

void ActionItem(HWND hWndList, DWORD dwData, WORD wItemNum)
{
   DWORD dwAddItem;
   DWORD dwIncr;
   WORD  wCount;
   WORD  wItem;

   //
   // Is this an item or a folder?
   //
   if ( (dwData % 10) ) {
      //
      // Not a folder, just pop a box.
      //
      MessageBox(hWndList, "Do something with this", 
                 "Simon Says", MB_OK);
   }
   else {
      // 
      // It's a folder, set up some junk for this sample app.
      //
      if ( dwData % 1000 == 0 ) {
         dwIncr = 100;
      }
      else if ( dwData % 100 == 0 ) {
         dwIncr = 10;
      }
      else if ( dwData % 10 == 0 ) {
         dwIncr = 1;
      }

      // 
      // Is it open?
      //
      if ( HierDraw_IsOpened(&HierDrawStruct, dwData) ) {
         //
         // It's open ... close it.
         //
         HierDraw_CloseItem(&HierDrawStruct, dwData);

         //
         // Remove the child items. Close any children that are 
         // open on the way.
         //
         // No need for recursion. We just close everything along 
         // the way and remove items until we reach the next sibling 
         // to the current item.
         //
         wItemNum++;
         dwIncr = SendMessage(hWndList, LB_GETITEMDATA, 
                             wItemNum, 0L);
         while ( dwIncr < dwAddItem ) {
            if ( (dwIncr % 10) == 0 &&
                  HierDraw_IsOpened(&HierDrawStruct, dwIncr)) {
               HierDraw_CloseItem(&HierDrawStruct, dwIncr);
            }
            SendMessage(hWndList, LB_DELETESTRING, wItemNum, 0L);
            dwIncr = SendMessage(hWndList, LB_GETITEMDATA, 
                                 wItemNum, 0L);
         }
      }
      else {
         //
         // It's closed ... open it.
         //
         HierDraw_OpenItem(&HierDrawStruct, dwData);

         //
         // Disable redrawing.
         //
         SendMessage(hWndList, WM_SETREDRAW, FALSE, 0L);   

         for ( wItem = wItemNum, dwAddItem = dwData+dwIncr, 
               wCount = 0, wItem++;
               wCount < 9; wItem++, wCount++, dwAddItem+=dwIncr ) {
             SendMessage(hWndList, LB_INSERTSTRING, 
                         wItem, dwAddItem);
         }
         //
         // Make sure as many child items as possible are showing.
         //
         HierDraw_ShowKids(&HierDrawStruct, hWndList, wItemNum, 9 );

         //
         // Enable, and force, redrawing.
         //
         SendMessage(hWndList, WM_SETREDRAW, TRUE, 0L);   
         InvalidateRect(hWndList, NULL, TRUE);            
      }
   }
}

The Helper Functions

This section describes the HierDraw structure and the helper functions that make it easier to display a hierarchy.

The HierDraw Structure

An application needs a structure that communicates information between calls to the helper functions. HIERDRAW.H, included in the HierList sample application, defines the HierDraw structure, which communicates this information. The application needs to declare a static or global HIERDRAWSTRUCT variable.

Syntax

typedef struct _HierDrawStruct {
    HDC       hdcMem;          
    HBITMAP   hbmIcons;
    HBITMAP   hbmMem;
    int       nBitmapHeight;
    int       nBitmapWidth;
    int       nTextHeight;
    int       nLineHeight;
    BOOL      bLines;
    int       NumOpened;
    DWORD FAR *Opened;
} HIERDRAWSTRUCT;

typedef HIERDRAWSTRUCT FAR *  LPHIERDRAWSTRUCT ;

Parameters

hdcMem

Handle to the memory device context (DC) that contains the icon bitmaps.

hbmIcons

Handle to the device-independent icon bitmap.

hbmMem

Handle to the memory icon bitmap.

nBitmapHeight

Height of an icon in the icon bitmap.

nBitmapWidth

Width of an icon in the icon bitmap.

nTextHeight

Height of the text used in the list box.

NLineHeight

Height of the item lines in the list box. This is either the bitmap height (nBitmapHeight) or the text height (nTextHeight), whichever is greater.

bLines

Flag that designates whether the helper functions should draw connecting lines between items in the list box.

NumOpened

Number of open items in the list box.

Opened

Pointer to a list of open items.

HierDraw_DrawInit

This function is responsible for:

The application must call HierDraw_DrawInit either before the application creates the dialog box that contains the list box or before it creates a child list box.

Syntax

BOOL HierDraw_DrawInit(HINSTANCE hInstance,
                       int  nBitmap,
                       int  nRows,
                       int  nColumns,
                       BOOL bLines,
                       LPHIERDRAWSTRUCT lpHierDrawStruct,
                       BOOL bInit);

Parameters

hInstance

Application's instance handle.

nBitmap

Integer identifier for the bitmap resource that contains the icon bitmap.

nRows

Number of rows in the icon bitmap.

nColumns

Number of columns in the icon bitmap.

bLines

Flag that specifies whether the list box should have connecting lines between items. If this parameter is TRUE, the list box will have connecting lines. If this parameter is FALSE, no lines will be drawn.

lpHierDrawStruct

Pointer to the HierDraw structure for this list box, which is filled in by HierDraw_DrawInit.

bInit

Flag that indicates whether to reload the icon bitmap or to initialize the HierDraw structure's data fields. If this parameter is TRUE, the data fields are initialized; otherwise, they are not. HierDraw_DrawInit should also be called with the bInit parameter set to FALSE in response to a WM_SYSCOLORCHANGE message.

This function returns TRUE if successful. Otherwise, it returns FALSE.

HierDraw_DrawTerm

This function cleans up any "leftovers" from the helper functions. It should be called when the list box or dialog box is destroyed.

Syntax

VOID HierDraw_DrawTerm(LPHIERDRAWSTRUCT lpHierDrawStruct);

Parameter

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

HierDraw_DrawSetTextHeight

This function sets the text height of the items in the list box. If necessary, HierDraw_DrawSetTextHeight will adjust the height of the items in the list box by sending a WM_MEASUREITEM message to the list box.

Syntax

VOID HierDraw_DrawSetTextHeight (HWND hwnd, 
                                 int  nTextHeight, 
                                 LPHIERDRAWSTRUCT lpHierDrawStruct);

Parameters

hwnd

Window handle to the list box.

nTextHeight

Height of the text used in the list box.

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

HierDraw_OnDrawItem

This function does the actual drawing of an item in the list box. The application calls HierDraw_OnDrawItem when Windows sends it a WM_DRAWITEM message.

Syntax

VOID HierDraw_OnDrawItem(HWND  hwnd,
                         const DRAWITEMSTRUCT FAR* lpDrawItem,
                         int   nLevel,
                         DWORD dwConnectLevel,
                         char  *szText,
                         int   nRow,
                         int   nColumn,
                         LPHIERDRAWSTRUCT lpHierDrawStruct);

Parameters

hwnd

Window handle to the list box.

lpDrawItem

Pointer to a DRAWITEMSTRUCT that the application received from Windows.

nLevel

Level of the current item.

dwConnectLevel

Contains bits that indicate the levels for which the current item needs connecting lines. Bit 0 designates whether the current item requires a connecting line for the children of level 0; bit 1 designates whether the current item requires a connecting line for the children of level 1; and so on.

szText

Text for the current item.

nRow

Row number for the current item's icon in the icon bitmap.

nColumn

Column number for the current item's icon in the icon bitmap.

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

HierDraw_OnMeasureItem

This function processes a WM_MEASUREITEM message for the list box. The application calls HierDraw_OnMeasureItem when Windows sends it a WM_MEASUREITEM message.

Syntax

VOID HierDraw_OnMeasureItem(HWND hwnd, 
                            MEASUREITEMSTRUCT FAR* lpMeasureItem,
                            LPHIERDRAWSTRUCT lpHierDrawStruct);

Parameters

hwnd

Window handle to the list box.

lpMeasureItem

Pointer to a MEASUREITEMSTRUCT that Windows sent to the application.

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

HierDraw_IsOpened

The application calls this function to determine if the specified item is in the list of open items.

Syntax

BOOL HierDraw_IsOpened(LPHIERDRAWSTRUCT lpHierDrawStruct, 
                       DWORD dwData);

Parameters

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

dwData

32-bit data value of the item.

This function returns TRUE if the item is in the open item list. Otherwise, it returns FALSE.

HierDraw_OpenItem

The application calls this function to add a value to the list of open item values.

Syntax

VOID HierDraw_OpenItem(LPHIERDRAWSTRUCT lpHierDrawStruct, 
                       DWORD dwData);

Parameters

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

dwData

32-bit data value to be added.

HierDraw_CloseItem

The application calls this function to remove a value from the list of open item values.

Syntax

VOID HierDraw_CloseItem(LPHIERDRAWSTRUCT lpHierDrawStruct, 
                        DWORD dwData);

Parameters

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

dwData

32-bit data value to be removed.

HierDraw_ShowKids

The application calls this function when it opens an item to ensure that as many children as possible are visible.

Syntax

VOID HierDraw_ShowKids(LPHIERDRAWSTRUCT lpHierDrawStruct,
                       HWND hwndList, 
                       WORD wCurrentSelection, 
                       WORD wKids);

Parameters

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

hwndList

Window handle to the list box.

wCurrentSelection

Index number of the current item.

wKids

Number of children of the current item.

HierDraw_DrawCloseAll

The application calls this function to remove all the values from the list of open items.

Syntax

VOID HierDraw_DrawCloseAll(LPHIERDRAWSTRUCT lpHierDrawStruct );

Parameter

lpHierDrawStruct

Pointer to the HierDraw structure for this list box.

So How Does It Work?

The HierDraw_DrawInit, HierDraw_OnDrawItem, and FastRect functions contain the interesting part of displaying a hierarchy (most of the other code is boring). Let's take a look at these functions.

HierDraw_DrawInit

The HierDraw_DrawInit function creates a memory DC that is compatible with the screen to hold the list's bitmap. It loads the bitmap using the FindResource, LoadResource, and LockResource functions, and receives a pointer to the BITMAPINFOHEADER structure for the bitmap. HierDraw_DrawInit then adjusts the background color of the bitmap by reading the color code for the bitmap's lower-left pixel and adjusting the bitmap's color table entry for that pixel to the current window background color. The lower-left pixel is located right after the bitmap's color table, which comes right after the bitmap's BITMAPINFOHEADER structure. HierDraw_DrawInit assumes that the bitmap is a 16-color bitmap, so the lower-left pixel is easily determined.

Here is the source for HierDraw_DrawInit:

BOOL HierDraw_DrawInit(HINSTANCE hInstance,
                       int  nBitmap,
                       int  nRows,
                       int  nColumns,
                       BOOL bLines,
                       LPHIERDRAWSTRUCT lpHierDrawStruct,
                       BOOL bInit)
{
    HANDLE hRes;
    HANDLE hResMem;
    LPBITMAPINFOHEADER lpbi;
    DWORD FAR * lpColorTable;
    LPSTR lpBits;
    int bc;
    HDC hDC;

    if ( bInit ) {
       lpHierDrawStruct->NumOpened = 0;
       lpHierDrawStruct->Opened = NULL;
       lpHierDrawStruct->bLines = bLines;
    }

    //
    // If the memory DC is not created yet, do that first.
    //
    if (!lpHierDrawStruct->hdcMem) {
        //
        // Get a screen DC.
        //
        hDC = GetDC(NULL);
        //
        // Create a memory DC compatible with the screen.
        //
        lpHierDrawStruct->hdcMem = CreateCompatibleDC(hDC);
        //
        // Release the screen DC.
        ReleaseDC(NULL,hDC);

        if (!lpHierDrawStruct->hdcMem)
            return FALSE;

        lpHierDrawStruct->hbmMem = NULL;
    }

    //
    // (Re)Load the bitmap (original from disk).
    //
    // Use the FindResource, LoadResource, and LockResource 
    // functions since it makes it easy to get the pointer to the 
    // BITMAPINFOHEADER we need.
    //
    //
    hRes = FindResource(hInstance, MAKEINTRESOURCE(nBitmap), 
                        RT_BITMAP);
    if (!hRes)
        return FALSE;

    hResMem = LoadResource(hInstance, hRes);
    if (!hResMem)
        return FALSE;

    //
    // Now figure out the bitmap's background color.
    // This code assumes these are 16-color bitmaps
    // and that the lower-left corner is a bit in the background
    // color.
    //
    //
    //
    lpbi = (LPBITMAPINFOHEADER)LockResource(hResMem);
    if (!lpbi)
        return FALSE;

    lpColorTable = (DWORD FAR *)(lpbi + 1);

    lpBits = (LPSTR)(lpColorTable + 16);   // Assumes 16 color.

    bc = (lpBits[0] & 0xF0) >> 4;          // Assumes 16 color.
                                           // Also assumes lower-
                                           // left corner is 
                                           // background color!!!

    lpColorTable[bc] = RGB2BGR(GetSysColor(COLOR_WINDOW));

    hDC = GetDC(NULL);

    lpHierDrawStruct->hbmIcons = 
        CreateDIBitmap(hDC,lpbi,(DWORD)CBM_INIT,lpBits, 
                      (LPBITMAPINFO)lpbi,DIB_RGB_COLORS);
    ReleaseDC(NULL,hDC);

    lpHierDrawStruct->nBitmapHeight = (WORD)lpbi->biHeight / nRows;
    lpHierDrawStruct->nBitmapWidth = (WORD)lpbi->biWidth / nColumns;

    lpHierDrawStruct->nLineHeight =
         max(lpHierDrawStruct->nBitmapHeight, 
             lpHierDrawStruct->nTextHeight);

    UnlockResource(hResMem);
    FreeResource(hResMem);

    if (!lpHierDrawStruct->hbmIcons)
        return FALSE;

    lpHierDrawStruct->hbmMem = 
        SelectObject(lpHierDrawStruct->hdcMem,
                     lpHierDrawStruct->hbmIcons);
    if (!lpHierDrawStruct->hbmMem)
        return FALSE;

    return TRUE;
}

HierDraw_OnDrawItem

The HierDraw_OnDrawItem function is a straightforward drawing routine for an owner-drawn list box. It calculates the offsets within the item's rectangle, which Windows supplies in DRAWITEMSTRUCT, for the item's indent, bitmap, and text, based on the level of the item.

If the hierarchy wants lines drawn to connect items, HierDraw_OnDrawItem checks the corresponding bit in the dwConnectLevel field for the item's level and each of its parent levels. If the corresponding bit is set in dwConnectLevel, HierDraw_OnDrawItem draws a vertical connecting line. At the item's level and for each parent level, HierDraw_OnDrawItem draws a horizontal line connecting the vertical line to the item's text.

HierDraw_OnDrawItem draws an icon for each item by calling BitBlt to copy the bitmap from the memory device context to the list box's device context.

If the item is selected, HierDraw_OnDrawItem draws the text with the current highlight colors; otherwise, it uses the current window and text colors to draw the text.

If the item has the focus and is not selected, HierDraw_OnDrawItem draws a focus rectangle by calling DrawFocusRect.

Here is the source for HierDraw_OnDrawItem:

VOID HierDraw_OnDrawItem(HWND  hwnd,
                         const DRAWITEMSTRUCT FAR* lpDrawItem,
                         int   nLevel,
                         DWORD dwConnectLevel,
                         char  *szText,
                         int   nRow,
                         int   nColumn,
                         LPHIERDRAWSTRUCT lpHierDrawStruct)
{
    HDC        hDC;
    WORD       wIndent, wTopBitmap, wTopText;
    RECT       rcTemp;

    if ( lpDrawItem->itemID == (UINT)-1 )
       return ;

    hDC = lpDrawItem->hDC;
    CopyRect(&rcTemp, &lpDrawItem->rcItem);

    wIndent = rcTemp.left + ((int)(nLevel) 
            * lpHierDrawStruct->nBitmapWidth) + XBMPOFFSET;

    rcTemp.left = wIndent + lpHierDrawStruct->nBitmapWidth;

    wTopText = rcTemp.top + ((rcTemp.bottom - rcTemp.top) / 2) 
             - (lpHierDrawStruct->nTextHeight / 2);

    wTopBitmap = rcTemp.top + ((rcTemp.bottom - rcTemp.top) / 2) 
               - (lpHierDrawStruct->nBitmapHeight / 2);

    if (lpDrawItem->itemAction == ODA_FOCUS)
        goto DealWithFocus;
    else if (lpDrawItem->itemAction == ODA_SELECT)
        goto DealWithSelection;

    //
    // Draw some lions, if we like lions.
    //

    if (lpHierDrawStruct->bLines && nLevel)
      {
        DWORD    dwMask = 1;
        int      nTempLevel;
        int      x,y;

        // Draw lines in text color.
        SetBkColor(hDC,GetSysColor(COLOR_WINDOWTEXT));

        //
        // Draw a series of vertical lines (|) for outer levels.
        //

        x = lpHierDrawStruct->nBitmapWidth/2 + XBMPOFFSET;

        for ( nTempLevel = 0; nTempLevel < nLevel ; nTempLevel++)
          {
            if ( dwConnectLevel & dwMask )
                FastRect(hDC,x,rcTemp.top,1,
                         rcTemp.bottom - rcTemp.top);

            x += lpHierDrawStruct->nBitmapWidth;
            dwMask *= 2;
          }

        //
        // Draw the short vertical line up toward the parent.
        //
        nTempLevel = nLevel-1;
        dwMask *= 2;

        x = nTempLevel * lpHierDrawStruct->nBitmapWidth 
          + lpHierDrawStruct->nBitmapWidth / 2 + XBMPOFFSET;

        if ( dwConnectLevel & dwMask )
            y = rcTemp.bottom;
        else
            y = rcTemp.bottom - lpHierDrawStruct->nLineHeight / 2;

        FastRect(hDC,x,rcTemp.top,1,y-rcTemp.top);

        //
        // Draw a short horizontal bar to the right.
        //
        FastRect(hDC,x,rcTemp.bottom-lpHierDrawStruct->nLineHeight/2 
                ,lpHierDrawStruct->nBitmapWidth/2,1);
      }

    //
    // Draw the selected bitmap.
    //

    BitBlt(hDC,
           wIndent,wTopBitmap,
           lpHierDrawStruct->nBitmapWidth,
           lpHierDrawStruct->nBitmapHeight,
           lpHierDrawStruct->hdcMem,
           nColumn*lpHierDrawStruct->nBitmapWidth,
           nRow*lpHierDrawStruct->nBitmapHeight,
           SRCCOPY);

DealWithSelection:

    if (lpDrawItem->itemState & ODS_SELECTED)
      {
        SetBkColor(hDC,GetSysColor(COLOR_HIGHLIGHT));
        SetTextColor(hDC,GetSysColor(COLOR_HIGHLIGHTTEXT));
      }
    else
      {
        SetBkColor(hDC,GetSysColor(COLOR_WINDOW));
        SetTextColor(hDC,GetSysColor(COLOR_WINDOWTEXT));
      }

    ExtTextOut(hDC, rcTemp.left + 1, wTopText, 
               ETO_CLIPPED|ETO_OPAQUE,&rcTemp, 
               szText,lstrlen(szText), NULL);

    if (lpDrawItem->itemState & ODS_FOCUS && 
        lpDrawItem->itemAction != ODA_SELECT) {
DealWithFocus:
        DrawFocusRect(hDC, &rcTemp);
    }

}

FastRect

So what's the fastest way to draw a rectangle? Use ExtTextOut without a text string to improve drawing performance.

static VOID near FastRect(HDC hDC, int x, int y, int cx, int cy)
{
    RECT rc;

    rc.left = x;
    rc.right = x+cx;
    rc.top = y;
    rc.bottom = y+cy;
    ExtTextOut(hDC,x,y,ETO_OPAQUE,&rc,NULL,0,NULL);
}

FastRect, an internal function in the helper functions, is just a little tidbit to improve drawing performance. And using the helper functions is an easy way to add hierarchical list boxes to your application.