Directory list control

A directory-picker list box

Does anyone else miss the old style Windows 3.1 directory list boxes? I find the new Windows 95 directory tree view controls pretty cumbersome to navigate around. They display far too much information at any one time, and as a result it can be confusing to navigate around the control to locate a folder.

The original style controls were declared obsolete because they didn't do a very good job of displaying long file names. The beauty of them was though, that they displayed only the necessary information to describe to the user what the current directory was, and what sub-directories were available. If you look at the picture above, you can instantly tell that the current directory is G:\WINNT\system32, because of the intuitive use of a hierarchy which displays sub-directories at increasing levels of indentation. A subtle but effective way to communicate a complex idea.

The directory list control that I have implemented solves the long filename problem by enabling the horizontal scrollbar in the control when necessary, i.e. when a filename becomes too big to display in the list without scrolling. Because the full source code is available, you can make the list control any size you wish as well, so you can make it big enough to display typical long filenames.

Getting Started

Implementing a directory list control is pretty easy once you know how. The process can be broken down into two major steps:

  • Building the list control
  • Drawing the list control

The key to the implementation is separating the logic needed to draw the list from the logic used to build the list contents. We are going to use the owner-draw facility provided by a list box control to customize the appearence of the list. Owner-draw operates on an item-based manner. So, we need to know how to draw each item in the list, without having to work out the relationship between items each time we have to draw them.

Look the image of a directory list once more. You will see that each item in the list posesses two properties that makes it unique. Each item's folder icon is in one of two states: either opened (1) or closed (0). In addition, each item has a hierarchy in the list. The first item has a "depth" of 0, the second item a depth of 1, and so on.

The image on the right identifies the two properties that each list item requires. The first number is the item depth, or hierarchy level. The second value is the state of the folder image. You should be able to see the relationship between the item properties and what they represent.

You also should notice how the directory components appear in the list. The actual directory that is displayed (in this case G:\WINNT\system32) is split into its component directory names ("G:\", "WINNT" and "system32"). Each component uses the "opened" icon. Each component also has an increasing depth (starting with 0).

The subdirectories under G:\WINNT\system32 always appear after the "current directory". They all have the same depth, or hierarchy. In addition, all sub-directories use the "closed" icon to indicate that they havn't been opened yet.

Associating properties with each list item

Now that we know we need two properties per list item, we need to set these two properties somehow. There is an easy solution: we will use the LB_SETITEMDATA message to set the 32bit integer value that each list item has. Because we need to set two values, we can use the low 16bits to set one value, and the high 16bits to set the second value. The following function associates two properties with one list item.

void SetItemState(HWND hwndList, int idx, int level, int state)
{
    SendMessage(hwndList, LB_SETITEMDATA, idx, MAKELONG(level, state));
}

This is a simple way to associate value(s) with a list item. However, if you needed to associate more complicated data with a list item then the best solution would be to allocate a structure containing the properties. You would then set the item data to be a pointer to one of these structures. Our simple solution is fine for this purpose however.

Building the directory list control

The first step is to fill the list box with the component names of the directory that you want to browse. For example, assuming that you want to browse the G:\WINNT\system32 directory:

  1. First, the components that make up the path "G:\WINNT\system32" must be added to the start of the list. So, the strings "G:\", "WINNT" and "system32" are added first of all. These strings are added with the "opened" icon state.
  2. Second, any sub-directories under the "G:\WINNT\system32" are added after the path components. These sub directory names can be obtained using the FindFirstFile / FindNextFile API calls. Every name is added with the "closed" icon state, and all with a constant depth.

After these two steps have been performed, the list-box will look something like this:

Here is an example of what the code might look like to perform this initial step.

void DirList_AddItem(HWND hwndList, char *szSubDir, int level, int state)
{
    int idx;
    idx = SendMessage(hwndList, LB_INSERTSTRING, -1, (LPARAM)szSubDir);
    SetItemState(hwndList, idx, level, state);
}
//
// This function adds the names of all sub-directories
// under the path specified by szPath, into the list identified
// by hwndList
//
void DirList_AddSubDirs(HWND hwndList, char *szPath)
{
    HANDLE hFind;
    WIN32_FIND_DATA win32fd;
    char szSearchPath[_MAX_PATH];
    
    lstrcpy(szSearchPath, szPath);
    
    if(szPath[lstrlen(szPath) - 1] != '\\')
        lstrcat(szSearchPath, "\\");

    lstrcat(szSearchPath, "*.*");
    if((hFind = FindFirstFile(szSearchPath, &win32fd)) == INVALID_HANDLE_VALUE) 
        return;

    do
    {
        if(win32fd.cFileName[0] != '.' && 
          (win32fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && 
         !(win32fd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN))
        {
            DirList_AddItem(hwndList, win32fd.cFileName, level, FOLDER_CLOSED);
        }
    } while(FindNextFile(hFind, &win32fd) != 0);

    FindClose(hFind);
}
//
// This function fills the specified list box control
// with the specified path name
//
BOOL DirList_SetPath(HWND hwndList, char *szPath)
{
    char szFullPath[MAX_PATH];
    char szText[MAX_PATH];
    int items = 0;

    char *szName;
    char *szSep = szFullPath;

    GetFullPathName(szPath, MAX_PATH, szFullPath, &szName);
    SendMessage(hwndList, LB_RESETCONTENT, 0, 0);

    szSep = strchr(szSep, '\\');

    if(!szSep) lstrcpy(szText, szFullPath);
    else       lstrcpyn(szText, szFullPath, szSep-szFullPath+2);

    DirList_AddItem(hwndList, szText, items++, FOLDER_OPENED);

    while(szSep && *(szSep+1) != '\0')
    {
        char *szOld = szSep;
        szSep = strchr(szSep+1, '\\');

        if(!szSep) lstrcpy(szText, szOld+1);
        else       lstrcpyn(szText, szOld+1, szSep-szOld);

        DirList_AddItem(hwndList, szText, items++, FOLDER_OPENED);
    }

    //Add the sub-directories
    DirList_AddSubDirs(hwndList, szFullPath, items);

    SendMessage(hwndList, LB_SETCURSEL, items-1, 0);
    return TRUE;
}

Drawing the list control

We are going to use Owner-draw to display this directory list. To enable owner-draw for a list-box, we have to set the LB_OWNERDRAWFIXED style. Once this style is set, the parent of the list control will receive a WM_DRAWITEM message whenever an individual item needs to be painted.

The only thing left to do is obtain a couple of images for the folder states. The best place to find these is inside the System Image List. This image list is just a standard HIMAGELIST, as found in the common controls library. We get a handle to the system image list using the ShGetFileInfo function.

SHFILEINFO shfi;

HIMAGELIST hSysImgList;

//retrieve the index of the icon in the system image list
hSysImgList = (HIMAGELIST)SHGetFileInfo("C:", 0, &shfi, sizeof(SHFILEINFO), 
                                          SHGFI_SMALLICON | SHGFI_SYSICONINDEX | SHGFI_ICON);

The advantage of the system image list is two-fold. Firstly, the list is made up of icon images. This is good because it is easy to draw icons with a transparent back-ground. The second advantage is that the folder images will look correct on whatever version of Windows that you run on.

BOOL DirListDraw(HWND hwnd, UINT uCtrlId, DRAWITEMSTRUCT *dis)
{
    HWND hwndCombo = GetDlgItem(hwnd, uCtrlId);
    char szText[MAX_PATH];
    int idx;
    int nState, nLevel;
    
    switch(dis->itemAction)
    {
    case ODA_FOCUS:
        
        //Windows 2000 doesn't display a control's focus in
        //a dialog box until the ALT key is pressed.
        if(!(dis->itemState & ODS_NOFOCUSRECT))
            DrawFocusRect(dis->hDC, &dis->rcItem);
        
        break;

    case ODA_SELECT:
    case ODA_DRAWENTIRE:
        // get the text string to display, and the item state.
        SendMessage(hwndCombo, LB_GETTEXT, dis->itemID, (LONG)szText);
        GetItemState(hwndCombo, dis->itemID, &nLevel, &nState);

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

        //draw the item text first of all. The ExtTextOut function also
        //lets us draw a rectangle under the text, so we use this facility
        //to draw the whole line at once.
        ExtTextOut(dis->hDC, 
            dis->rcItem.left + nBitmapXSpace + nBitmapWidth + nLevel * CX_OFFSET, 
            dis->rcItem.top + 1, 
            ETO_OPAQUE, &dis->rcItem, szText, lstrlen(szText), 0);

        idx = nState == FOLDER_OPENED ? nImgFolderOpened : nImgFolderClosed;

        //draw the folder image, using an icon from an image list.
        ImageList_Draw(hDirImgList, idx, dis->hDC, 
            dis->rcItem.left + 2 + nLevel * CX_OFFSET, 
            dis->rcItem.top + nBitmapYOff, 
            ILD_TRANSPARENT);

        break;
    }
    return TRUE;
}

Centering the folder image

If you actually looked at the source above, you will see that there is a variable called nBitmapYOff. This variable is the distance in pixels that the folder image needs to be offset by so that it is centered in the list item. When a large system font is being used, the text of a list box will be much larger than the height of the folder bitmap, so the folder image is centered to create a more pleasing effect. A simple formula is used to calculate the nBitmapYOff variable.

nBitmapYOff = (nLineHeight - (nBitmapHeight - 1)) / 2;

The nLineHeight value is the size in pixels of each line in the list. This is calculated whenever a WM_MEASUREITEM message is received. This message instructs the parent of the directory list to set the size in pixels of each item in the list. The code below calculates this amount by taking the maximum value between the folder bitmap height and the height of a line of text.

TEXTMETRIC      tm;
HANDLE          hOldFont = NULL;
HDC             hdc;

int nTextHeight;
int nLineHeight;

hdc = GetDC(hwndCombo);
hOldFont = SelectObject(hdc, hFont);

GetTextMetrics(hdc, &tm);
SelectObject(hdc, hOldFont);

ReleaseDC(hwndCombo, hdc);

nTextHeight = tm.tmHeight + tm.tmInternalLeading;
nLineHeight = max(nBitmapHeight, nTextHeight);

Adding horizontal scrollbar support

To make sure that extra-long directory names are still visible in our list, we need to enable the horizontal scrollbar on the list control. There is a list-box message called LB_SETHORIZONTALEXTENT which is used to set the amount by which the list-box scrolls horizontally. Without this message, no horizontal scrollbar will be shown.

LB_SETHORIZONTALEXTENT requires one parameter - the size in pixels of the area to scroll horizontally. Therefore we need to calculate the width of the longest entry in the directory list, and use this value when we send the message.

The easiest way to calculate the length of the longest line is to use a seperate function, which loops through all of the items in the list. The length of each line is calculated using the GetTextExtentPoint32 function. Each item's hierarchy in the list is taken into account, and the appropriate distance added to the line length.

void DirList_UpdateWidth(HWND hwndList)
{
    HANDLE hOld;
    HDC hdc; 
    HFONT hFont;
    RECT rect;
    int i, count, width = 0;
    int nLevel, nState;

    hdc   = GetDC(hwndList);
    hFont = (HFONT)SendMessage(hwndList, WM_GETFONT, 0, 0);
    hOld = SelectObject(hdc, hFont);

    count = SendMessage(hwndList, LB_GETCOUNT, 0, 0);

    // loop over each line in the directory list
    for(i = 0; i < count; i++)
    {
        SIZE sz;
        char ach[MAX_PATH];

        // retrieve this item's text
        SendMessage(hwndList, LB_GETTEXT, i, (LONG)ach);
        GetItemState(hwndList, i, &nLevel, &nState);

        // calculate the size of the text
        GetTextExtentPoint32(hdc, ach, lstrlen(ach), &sz);

        // add on the size of the folder image, and the amount
        // that this item is indented by
        sz.cx += 4 * GetSystemMetrics(SM_CXBORDER) + 
            nBitmapXSpace + nBitmapWidth + nLevel * CX_OFFSET;

        if(sz.cx > width)
            width = sz.cx;
    }

    GetClientRect(hwndList, &rect);

    // if the horizontal scrollbar is not needed, the hide it
    if(width < rect.right) ShowScrollBar(hwndList, SB_HORZ, FALSE);
    else                   ShowScrollBar(hwndList, SB_HORZ, TRUE);

    // set the horizontal scroll amount
    SendMessage(hwndList, LB_SETHORIZONTALEXTENT, width, 0);
    SelectObject(hdc, hOld);
    ReleaseDC(hwndList, hdc);
}

Conclusion

That's basically all that needs to be done to implement a directory list control. There are a few more little bits and pieces to get it to operate correctly. The supplied source code provides a fully implemented directory list, so you can look there to see the other details.

The technique presented here can also be used to draw pretty almost any list-based hierarchy. Just remember to separate the implementation into two stages: building the list and drawing the list. If you stick to this method it is much easier to draw hierarchies in this way.