[Previous] [Next]

Image Lists and ComboBoxEx Controls

Chapter 10's DriveTree and WinDir programs used image lists to provide iconlike images to a tree view and a list view. At the time, I didn't say much about image lists other than that they hold collections of bitmapped images and that MFC wraps them with the class CImageList. As it turns out, image lists are extraordinarily useful not only for supplying images to other controls, but also for blitting bitmaps with special effects such as transparency and blending. We'll examine these and other features of image lists in the next section.

When Internet Explorer 3.0 or later is installed, it replaces Comctl32.dll with a newer version that includes enhanced versions of the existing controls as well as several new common control types. One of those new control types is the extended combo box control, better known as the ComboBoxEx control. A ComboBoxEx control differs from a standard combo box control in several important respects, most notably in the fact that it can display images next to each item. Not surprisingly, the images come from an image list. You can combine image lists and ComboBoxEx controls to create drop-down lists containing both graphics and text.

In the sections that follow, I'll use a broad brush to paint a picture of image lists and ComboBoxEx controls and introduce the fundamental principles involved in programming them. Then you'll see just how powerful image lists and ComboBoxEx controls can be by developing a combo box that depicts path names visually.

Image Lists

An image list is a collection of identically sized bitmap images joined together to form one logical unit. MFC's CImageList class provides functions for creating image lists, adding and deleting images, drawing images on the screen, writing image lists to an archive and reading them back, and more. Image lists are useful in and of themselves because many of the functions that operate on them have no direct counterparts elsewhere in Windows. But image lists were added to the operating system in the first place so that bitmaps could be grouped and passed as a unit to other common controls. When you supply images to a tree view control, for example, you don't pass it an array of CBitmaps; you pass it a handle to an image list (an HIMAGELIST) or a pointer to a CImageList object. Individual images are then referenced with 0-based indexes.

The best way to picture an image list is to think of a filmstrip with images laid horizontally from end to end. The leftmost image is image number 0, the one to its right is image number 1, and so on. The images can be any height and width, but they must all be the same height and width.

MFC provides three ways to create an image list. You can create an empty image list and add images to it with CImageList::Add; you can create an initialized image list from an existing bitmap containing an array of images; or you can create an initialized image list by merging images from existing image lists. CImageList::Create is overloaded to support all three creation methods. The second (and probably the most common) of these methods is illustrated in the following example. Suppose IDB_BITMAP is the resource ID of a bitmap that contains five images, each measuring 18 pixels wide and 16 pixels high. The bitmap itself is 90 pixels wide (5 times 18) and 16 pixels high. These statements create an image list from the bitmap:

CImageList il;
il.Create (IDB_BITMAP, 18, 1, CLR_NONE);

The first parameter passed to Create is the bitmap's resource ID. You can also pass a string resource ID for this parameter. The second parameter is the width, in pixels, of the individual images. Windows determines how many images to add to the list by dividing the bitmap width by the image width. The third parameter is the grow size. Image lists are sized dynamically just as arrays created from MFC collection classes are, and the grow size tells the image list how many additional images to make room for when more memory must be allocated to accommodate new images. The final parameter—CLR_NONE—creates an unmasked image list. Unmasked images are ordinary bitmaps that are blitted directly to the destination when they're drawn on the screen.

Passing CImageList::Create a COLORREF value instead of CLR_NONE creates a masked image list. In addition to storing color information for a masked image, Windows also stores a monochrome bit mask that allows it to distinguish between foreground and background pixels. The COLORREF value passed to CImageList::Create specifies the background color, and any pixel set to that color is assumed to be a background pixel. What's cool about masked images is the fact that you can call CImageList::SetBkColor before drawing from an image list and set the background color to any color you like. The background color in the original bitmap might be magenta, but if you set the background color to red and draw the image, all the magenta pixels will come out red. What's really cool is that you can pass CImageList::SetBkColor a CLR_NONE parameter and background pixels won't be drawn at all. Consequently, image lists provide a simple means of drawing bitmaps with transparent pixels. Remember the DrawTransparent function we developed in Chapter 15 for drawing nonrectangular bitmaps? An image list lets you do the same thing with less code. The image list method is faster, too, because the masks don't have to be generated anew each time the image is blitted to the screen.

CImageList::Draw draws images on the screen. The following statement draws the third image in the list (image number 2) to the screen DC referenced by the CDC pointer pDC:

il.Draw (pDC, 2, point, ILD_NORMAL);

point is a POINT structure containing the x and y coordinates of the point in the destination DC where the upper left corner of the image will be drawn. ILD_NORMAL is a flag that tells the Draw function to draw a masked image using the current background color. (This flag has no effect on unmasked images.) If you'd like background pixels to be transparent regardless of what the current background color happens to be, you can use an ILD_TRANSPARENT flag instead:

il.Draw (pDC, 2, point, ILD_TRANSPARENT);

For some truly interesting effects, try drawing a masked image with an ILD_BLEND25 or ILD_BLEND50 flag to blend in the system highlight color (COLOR_HIGHLIGHT). CImageList::Draw also accepts ILD_SELECTED and ILD_FOCUS flags, but they're nothing more than ILD_BLEND50 and ILD_BLEND25 in disguise. To see blending at work, select an icon on the Windows desktop. To show the icon in a selected state, the system dithers the icon with the system highlight color by drawing it with an ILD_BLEND50 flag.

An aside: Drawing with an ILD_TRANSPARENT flag or with the background color set to CLR_NONE is always a little slower than drawing an unmasked image. If an image contains transparent pixels but is being blitted to a solid background, use CImageList::SetBkColor to set the image list's background color to the color of the solid background and then call CImageList::Draw with an ILD_NORMAL flag. You'll improve performance and still get those transparent pixels you wanted.

ComboBoxEx Controls

ComboBoxEx controls exist to simplify the task of including images as well as text in combo boxes. Assuming that m_wndCBEx is a ComboBoxEx data member of a dialog class, that m_wndCBEx is mapped to a ComboBoxEx control in the dialog, and that m_il is an instance of CImageList, the following statements initialize the control with items labeled "Item 1" through "Item 5." Next to each item appears a folder image extracted from the image list. The image list, in turn, acquires the image from the bitmap resource IDB_IMAGE:

m_il.Create (IDB_IMAGE, 16, 1, RGB (255, 0, 255));
m_wndCBEx.SetImageList (&m_il);

for (int i=0; i<5; i++) {
    CString string;
    string.Format (_T ("Item %d"), i);

    COMBOBOXEXITEM cbei;
    cbei.mask = CBEIF_IMAGE ¦ CBEIF_SELECTEDIMAGE ¦ CBEIF_TEXT;
    cbei.iItem = i;
    cbei.pszText = (LPTSTR) (LPCTSTR) string;
    cbei.iImage = 0;
    cbei.iSelectedImage = 0;

    m_wndCBEx.InsertItem (&cbei);
}

The key functions used in this sample include CComboBoxEx::SetImageList, which associates an image list with a ComboBoxEx control, and CComboBoxEx::InsertItem, which adds an item to the control. InsertItem accepts a pointer to a COMBOBOXEXITEM structure containing pertinent information about the item, including the item's text and the 0-based indexes of the images (if any) associated with the item. iImage identifies the image displayed next to the item when the item isn't selected, and iSelectedImage identifies the image that's displayed when the item is selected. Figure 16-7 shows the resulting control with its drop-down list displayed.

Figure 16-7. A ComboBoxEx control containing both text and images.

You can indent an item in a ComboBoxEx control by specifying a nonzero number of "spaces" in COMBOBOXEXITEM's iIndent field. Each space equals 10 pixels. The following example initializes a ComboBoxEx control that's identical to the one in the preceding example except for the fact that each successive item is indented one space more than the previous item:

m_il.Create (IDB_IMAGE, 16, 1, RGB (255, 0, 255));
m_wndCBEx.SetImageList (&m_il);

for (int i=0; i<5; i++) {
    CString string;
    string.Format (_T ("Item %d"), i);

    COMBOBOXEXITEM cbei;
    cbei.mask = CBEIF_IMAGE ¦ CBEIF_SELECTEDIMAGE ¦ CBEIF_TEXT ¦
        CBEIF_INDENT;
    cbei.iItem = i;
    cbei.pszText = (LPTSTR) (LPCTSTR) string;
    cbei.iImage = 0;
    cbei.iSelectedImage = 0;
    cbei.iIndent = i;

    m_wndCBEx.InsertItem (&cbei);
}

The result is shown in Figure 16-8. The ability to indent items an arbitrary number of spaces comes in handy when you use a ComboBoxEx control to display items that share a hierarchical relationship, such as the names of the individual directories comprising a path name.

Figure 16-8. A ComboBoxEx control containing indented items.

InsertItem is one of four CComboBoxEx member functions that you can use to manipulate items in a ComboBoxEx control. The others are DeleteItem, which removes an item; GetItem, which copies the information about an item to a COMBOBOXEXITEM structure; and SetItem, which modifies an item using information supplied in a COMBOBOXEXITEM structure.

CComboBoxEx has just a handful of member functions of its own. Common operations such as selecting an item or retrieving the index of the selected item are performed with CComboBox member functions. Because CComboBoxEx derives from CComboBox, you can call CComboBox functions on a CComboBoxEx. For example, the statement

m_wndCBEx.SetCurSel (nIndex);

selects the item whose 0-based index is nIndex, and the statement

int nIndex = m_wndCBEx.GetCurSel ();

sets nIndex equal to the index of the currently selected item.

Like conventional combo boxes, ComboBoxEx controls come in three varieties: simple, drop-down, and drop-down list. You pick the type by choosing one of the three primary combo box styles: CBS_SIMPLE, CBS_DROPDOWN, or CBS_DROPDOWNLIST. Other CBS styles, such as CBS_SORT, don't apply to ComboBoxEx controls and are ignored if you use them. ComboBoxEx controls do support a few styles of their own, however. These styles are known as extended styles and can't be applied in a dialog template or a Create statement; instead, you must apply them programmatically with CComboBoxEx::SetExtendedStyle after the control is created. The following table lists the extended styles that are supported on all platforms. To configure the control to treat text as case-sensitive, for example, you would write:

m_wndCBEx.SetExtendedStyle (CBES_EX_CASESENSITIVE, 
    CBES_EX_CASESENSITIVE);

The second parameter you pass to SetExtendedStyle specifies the style or styles that you want to apply. The first parameter is a style mask that you can use to prevent other styles from being affected, too. Passing zero in parameter 1 effectively eliminates the mask.

ComboBoxEx Control Extended Styles

Style Description
CBES_EX_CASESENSITIVE Makes string searches case-sensitive
CBES_EX_NOEDITIMAGE Suppresses item images
CBES_EX_NOEDITIMAGEINDENT Suppresses item images and left-indents each item to remove the space normally reserved for the item image
CBES_EX_NOSIZELIMIT Allows the ComboBoxEx control's height to be less than the height of the combo box contained inside the control

A ComboBoxEx control sends the same CBN notifications to its parent that a conventional combo box sends. It also supports the notifications of its own that are listed in the following table.

ComboBoxEx Notifications

Notification Sent When
CBEN_BEGINEDIT The user displays the control's drop-down list or clicks the edit control to begin editing.
CBEN_ENDEDIT The user selects an item from the control's list box or edits the control's text directly.
CBEN_DRAGBEGIN The user drags an item in the control to begin a drag-and-drop operation.
CBEN_INSERTITEM An item is added to the control.
CBEN_DELETEITEM An item is removed from the control.
CBEN_GETDISPINFO The control needs additional information—a text string, an image, an indentation level, or some combination thereof—about an item before displaying that item.
NM_SETCURSOR The control is about to set the cursor in response to a WM_SETCURSOR message.

MFC applications use ON_NOTIFY macros to map CBEN notifications to handling functions in the parent's window class. CBEN_GETDISPINFO notifications are sent only if the pszText field of a COMBOBOXEXITEM structure passed to InsertItem contains LPSTR_TEXTCALLBACK, the iImage or iSelectedImage field contains I_IMAGECALLBACK, or the iIndent field contains I_INDENTCALLBACK. You can use these special values to create dynamic ComboBoxEx controls that supply text, images, and indentation levels on the fly rather than at the time the items are inserted.

The PathList Application

PathList, shown in Figure 16-9, is a dialog-based MFC application that uses a ComboBoxEx control to depict path names. The control is an instance of CPathComboBox, which I derived from CComboBoxEx. CPathComboBox has two public member functions: SetPath and GetPath. When passed a fully qualified path name, SetPath parses the path name and adds indented items representing the individual directories that make up the path. (SetPath checks the drive letter but doesn't validate the remainder of the path name.) GetPath returns a fully qualified path name that corresponds to the drive or directory that is currently selected in the control.

Figure 16-9. The PathList window.

The source code for PathList's dialog and ComboBoxEx classes is shown in Figure 16-10. PathList's dialog window does very little with the ComboBoxEx control other than host it. It calls SetPath with the path to the current directory when it starts up, and it displays the path name returned by GetPath when an item is selected. The control class CPathComboBox contains most of the interesting stuff, including the code that parses path names passed to SetPath, adds items to the control, removes the old items when the path name changes, and so on. Take the time to understand how it works and you'll be well on your way to understanding ComboBoxEx controls, too.

Figure 16-10. The PathList application.

PathListDlg.h

// PathListDlg.h : header file
//

#if !defined(
    AFX_PATHLISTDLG_H__710413E6_AC66_11D2_8E53_006008A82731__INCLUDED_)
#define AFX_PATHLISTDLG_H__710413E6_AC66_11D2_8E53_006008A82731__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

///////////////////////////////////////////////////////////////////////////
// CPathListDlg dialog

class CPathListDlg : public CDialog
{
// Construction
public:
    CPathListDlg(CWnd* pParent = NULL);    // standard constructor
// Dialog Data
    //{{AFX_DATA(CPathListDlg)
    enum { IDD = IDD_PATHLIST_DIALOG };
    CPathComboBox    m_wndCBEx;
    //}}AFX_DATA

    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CPathListDlg)
    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
    //}}AFX_VIRTUAL

// Implementation
protected:
    HICON m_hIcon;

    // Generated message map functions
    //{{AFX_MSG(CPathListDlg)
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    afx_msg void OnSelEndOK();
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations 
// immediately before the previous line.

#endif 
// !defined(
//     AFX_PATHLISTDLG_H__710413E6_AC66_11D2_8E53_006008A82731__INCLUDED_)

PathListDlg.cpp

// PathListDlg.cpp : implementation file
//

#include "stdafx.h"
#include "PathList.h"
#include "PathComboBox.h"
#include "PathListDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

///////////////////////////////////////////////////////////////////////////
// CPathListDlg dialog

CPathListDlg::CPathListDlg(CWnd* pParent /*=NULL*/)
    : CDialog(CPathListDlg::IDD, pParent)
{
    //{{AFX_DATA_INIT(CPathListDlg)
    //}}AFX_DATA_INIT
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CPathListDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CPathListDlg)
    DDX_Control(pDX, IDC_CBEX, m_wndCBEx);
    //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CPathListDlg, CDialog)
    //{{AFX_MSG_MAP(CPathListDlg)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_CBN_SELENDOK(IDC_CBEX, OnSelEndOK)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

///////////////////////////////////////////////////////////////////////////
// CPathListDlg message handlers

BOOL CPathListDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    SetIcon(m_hIcon, TRUE);
    SetIcon(m_hIcon, FALSE);
    
    //
    // Initialize the ComboBoxEx control.
    //
    TCHAR szPath[MAX_PATH];
    ::GetCurrentDirectory (sizeof (szPath) / sizeof (TCHAR), szPath);
    m_wndCBEx.SetPath (szPath);
    return TRUE;
}

void CPathListDlg::OnPaint() 
{
    if (IsIconic())
    {
        CPaintDC dc(this); // device context for painting

        SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);

        // Center icon in client rectangle
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
        GetClientRect(&rect);
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;

        // Draw the icon
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        CDialog::OnPaint();
    }
}

HCURSOR CPathListDlg::OnQueryDragIcon()
{
    return (HCURSOR) m_hIcon;
}

void CPathListDlg::OnSelEndOK() 
{
    //
    // Display the path just selected from the ComboBoxEx control.
    //
    MessageBox (m_wndCBEx.GetPath ());
}

PathComboBox.h

#if !defined(
    AFX_PATHCOMBOBOX_H__710413F1_AC66_11D2_8E53_006008A82731__INCLUDED_)
#define AFX_PATHCOMBOBOX_H__710413F1_AC66_11D2_8E53_006008A82731__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
// PathComboBox.h : header file
//

///////////////////////////////////////////////////////////////////////////
// CPathComboBox window

class CPathComboBox : public CComboBoxEx
{
// Construction
public:
    CPathComboBox();

// Attributes
public:

// Operations
public:

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CPathComboBox)
    //}}AFX_VIRTUAL

// Implementation
public:
    CString GetPath();
    BOOL SetPath (LPCTSTR pszPath);
    virtual ~CPathComboBox();

    // Generated message map functions
protected:
    void GetSubstring (int& nStart, CString& string, CString& result);
    int m_nIndexEnd;
    int m_nIndexStart;
    BOOL m_bFirstCall;
    CImageList m_il;
    //{{AFX_MSG(CPathComboBox)
    //}}AFX_MSG

    DECLARE_MESSAGE_MAP()
};

///////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations 
// immediately before the previous line.

#endif 
// !defined(
//     AFX_PATHCOMBOBOX_H__710413F1_AC66_11D2_8E53_006008A82731__INCLUDED_)

PathComboBox.cpp

// PathComboBox.cpp : implementation file
//

#include "stdafx.h"
#include "PathList.h"
#include "PathComboBox.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

///////////////////////////////////////////////////////////////////////////
// CPathComboBox

CPathComboBox::CPathComboBox()
{
    m_bFirstCall = TRUE;
    m_nIndexStart = -1;
    m_nIndexEnd = -1;
}

CPathComboBox::~CPathComboBox()
{
}

BEGIN_MESSAGE_MAP(CPathComboBox, CComboBoxEx)
    //{{AFX_MSG_MAP(CPathComboBox)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

///////////////////////////////////////////////////////////////////////////
// CPathComboBox message handlers

BOOL CPathComboBox::SetPath(LPCTSTR pszPath)
{
    if (m_bFirstCall) {
        m_bFirstCall = FALSE;

        //
        // Add an image list containing drive and folder images.
        //
        m_il.Create (IDB_IMAGES, 16, 1, RGB (255, 0, 255));
        SetImageList (&m_il);

        //
        // Add icons representing the drives on the host system.
        //
        int nPos = 0;
        int nCount = 0;
        CString string = _T ("?:\\");

        DWORD dwDriveList = ::GetLogicalDrives ();

        while (dwDriveList) {
            if (dwDriveList & 1) {
                string.SetAt (0, _T (`A') + nPos);
                CString strDrive = string.Left (2);
                UINT nType = ::GetDriveType (string);

                int nImage = 0;
                switch (nType) {

                case DRIVE_FIXED:
                    nImage = 0;
                    break;

                case DRIVE_REMOVABLE:
                    nImage = 1;
                    break;

                case DRIVE_CDROM:
                    nImage = 2;
                    break;

                case DRIVE_REMOTE:
                    nImage = 3;
                    break;
                }

                COMBOBOXEXITEM cbei;
                cbei.mask = CBEIF_TEXT | CBEIF_IMAGE | CBEIF_SELECTEDIMAGE;
                cbei.iItem = nCount++;
                cbei.pszText = (LPTSTR) (LPCTSTR) strDrive;
                cbei.iImage = nImage;
                cbei.iSelectedImage = nImage;
                InsertItem (&cbei);
            }
            dwDriveList >>= 1;
            nPos++;
        }
    }

    //
    // Find the item that corresponds to the drive specifier in pszPath.
    //
    CString strPath = pszPath;
    CString strDrive = strPath.Left (2);

    int nDriveIndex = FindStringExact (-1, strDrive);
    if (nDriveIndex == CB_ERR)
        return FALSE;

    //
    // Delete previously added folder items (if any).
    //
    if (m_nIndexStart != -1 && m_nIndexEnd != -1) {
        ASSERT (m_nIndexEnd >= m_nIndexStart);
        int nCount = m_nIndexEnd - m_nIndexStart + 1;
        for (int i=0; i<nCount; i++)
            DeleteItem (m_nIndexStart);
        if (m_nIndexStart < nDriveIndex)
            nDriveIndex -= nCount;
        m_nIndexStart = -1;
        m_nIndexEnd = -1;
    }

    //
    // Add items representing the directories in pszPath.
    //
    int nCount = 0;
    int nStringIndex = strPath.Find (_T (`\\'), 0);

    if (nStringIndex++ != -1) {
        CString strItem;
        GetSubstring (nStringIndex, strPath, strItem);

        while (!strItem.IsEmpty ()) {
            COMBOBOXEXITEM cbei;
            cbei.mask = CBEIF_TEXT | CBEIF_IMAGE | CBEIF_SELECTEDIMAGE |
                CBEIF_INDENT;
            cbei.iItem = nDriveIndex + ++nCount;
            cbei.pszText = (LPTSTR) (LPCTSTR) strItem;
            cbei.iImage = 4;
            cbei.iSelectedImage = 5;
            cbei.iIndent = nCount;
            InsertItem (&cbei);

            GetSubstring (nStringIndex, strPath, strItem);
        }
    }

    //
    // Record the indexes of the items that were added, too.
    //
    if (nCount) {
        m_nIndexStart = nDriveIndex + 1;
        m_nIndexEnd = nDriveIndex + nCount;
    }

    //
    // Finish up by selecting the final item.
    //
    int nResult = SetCurSel (nDriveIndex + nCount);
    return TRUE;
}

void CPathComboBox::GetSubstring(int& nStart, CString &string,
    CString &result)
{
    result = _T ("");
    int nLen = string.GetLength ();
    if (nStart >= nLen)
        return;

    int nEnd = string.Find (_T (`\\'), nStart);
    if (nEnd == -1) {
        result = string.Right (nLen - nStart);
        nStart = nLen;
    }
    else {
        result = string.Mid (nStart, nEnd - nStart);
        nStart = nEnd + 1;
    }
}

CString CPathComboBox::GetPath()
{
    //
    // Get the index of the selected item.
    //
    CString strResult;
    int nEnd = GetCurSel ();
    int nStart = nEnd + 1;

    //
    // Find the index of the "root" item.
    //
    COMBOBOXEXITEM cbei;
    do {
        cbei.mask = CBEIF_INDENT;
        cbei.iItem = —nStart;
        GetItem (&cbei);
    } while (cbei.iIndent != 0);

    //
    // Build a path name by combining all the items from the root item to
    // the selected item.
    //
    for (int i=nStart; i<=nEnd; i++) {
        TCHAR szItem[MAX_PATH];
        COMBOBOXEXITEM cbei;
        cbei.mask = CBEIF_TEXT;
        cbei.iItem = i;
        cbei.pszText = szItem;
        cbei.cchTextMax = sizeof (szItem) / sizeof (TCHAR);
        GetItem (&cbei);
        strResult += szItem;
        strResult += _T ("\\");
    }

    //
    // Strip the trailing backslash.
    //
    int nLen = strResult.GetLength ();
    strResult = strResult.Left (nLen - 1);
    return strResult;
}

The CHM file was converted to HTML by chm2web software.