Drive-list control

A drop-down list displaying drive names and icons

This tutorial will show you how to implement a drop-down list of all the logical drives installed on a Windows system. There are basically three major steps involved in achieving this goal, listed in the order that we will tackle them.

DriveList screen-shot

  • Enumerating the disk drives on the current system.
  • Obtaining the correct icon images used to draw the drives.
  • Handling the WM_DRAWITEM message to draw the drop-down list.

Enumerating disk drives

This one is really easy. All we do is call the GetLogicalDriveStrings API call to retrieve the complete list of all logical drives currently installed.

The returned buffer contains a number of zero-terminated strings, one after the other. The buffer is terminated by an additional null character, to mark the end of the drive names. The code below demonstates how to retrieive the list of drive names, and extract each name in turn from the string buffer.

char szDrives[1024];
char szText[32];
int drivelen;

drivelen = GetLogicalDriveStrings(1023, szDrives);while(drivelen != 0)
{
    int drivetype, len;
    len = lstrlen(driveptr) + 1; //plus the NULL

    drivelen -= len;

    //convert drive name to lower case
    driveptr = strlwr(driveptr);
    lstrcpyn(szText, driveptr, 3);  //only copy the 'X:' portion

    //retrieve the index of the icon in the system image list

    //add the drive name to the combo box
    driveptr += len;
}

Getting the icon image

This is the easiest job of all. We don't need to draw our own icons, because we can use the icons that the Windows shell uses to display drives. These system icons are located in a resource called the System Image List. So, we need to obtain the HIMAGELIST handle to this image list, and the index into this list of the icon image we want. Thankfully there is a function which obtains an index this for us, called ShGetFileInfo.

SHFILEINFO shfi;
HIMAGELIST hSysImgList;

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

The snippet above retrieves a handle to the system image list, and also the index of the icon associated with the "C:" drive. The icon index is stored in the shfi.iIcon varaible when the ShGetFileInfo function returns.

Handling the WM_DRAWITEM message

It is also pretty straight-forward to draw the icons along side each drive name. We do this using the standard Owner-Draw feature that a Combo box supports. Owner-Draw is enabled for a combo-box by creating the control with the CBS_OWNERDRAWFIXED style.

BOOL DriveListMeasure(HWND hwnd, UINT uCtrlId, MEASUREITEMSTRUCT *mis)
{
    HFONT hFont = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0);

    mis->itemHeight = DriveList_GetLineHeight(hwnd, hFont, nBitmapHeight);  

    //(16)
    nBitmapYOff = (mis->itemHeight - (nBitmapHeight-1)) / 2;
    return TRUE;
}
BOOL DriveListDraw(HWND hwnd, UINT uCtrlId, DRAWITEMSTRUCT *dis)
{
    HWND hwndCombo = GetDlgItem(hwnd, uCtrlId);
    char szText[MAX_PATH+1];
    char *ptr;
    int idx;

    switch(dis->itemAction)
    {
    case ODA_FOCUS:
        if(!(dis->itemState & ODS_NOFOCUSRECT))
            DrawFocusRect(dis->hDC, &dis->rcItem);
        break;

    case ODA_SELECT:
    case ODA_DRAWENTIRE:

        SendMessage(hwndCombo, CB_GETLBTEXT, dis->itemID, (LONG)szText);

        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));
        }

        // we set the SB_SETITEMDATA DWORD to be the system image list index
        // for this drive icon. Retrieve the value so we know which picture
        // to display for this drive
        idx = dis->itemData;        
        
        ptr = strchr(szText, ':');
        if(ptr) ptr[1] = '\0';

        //Draw the drive letter
        ExtTextOut(dis->hDC, dis->rcItem.left+22, dis->rcItem.top+1, 
                   ETO_OPAQUE, &dis->rcItem, szText, lstrlen(szText), 0);

        //Draw the drive volume name
        if(ptr)
            ExtTextOut(dis->hDC, dis->rcItem.left+22+16, 
             dis->rcItem.top+1, 0, 0, ptr+1, lstrlen(ptr+1), 0);

        if(dis->itemState & ODS_FOCUS) 
        {
            if(!(dis->itemState & ODS_NOFOCUSRECT))
                DrawFocusRect(dis->hDC, &dis->rcItem);
        }

        //Draw the drive icon over the top of the selection
        ImageList_Draw(hSysImgList, idx, dis->hDC, 
            dis->rcItem.left + 2, 
            dis->rcItem.top + nBitmapYOff, 
            ILD_TRANSPARENT);

        break;
    }

    return TRUE;
}

Conclusion

That's all there is to do for now. I've omitted a few details for clarity's sake, but the source code download shows how to do this in full.

This implementation of a drive-list uses the standard Windows combo box to display the drives. The ComboBoxEx control which is available in the Common Controls Library could have been used instead. This new style combo box has automatic support for item images through an image list. There were two reasons I chose not to use this new control. The first is that the ComboBoxEx is not available under some versions of Windows (those which have an early version of the common controls library). The second reason is that I wanted to achieve the same look and feel as a "normal" drive list control, so the standard Combo box was chosen.