Enumerating the shell namespace

by Kent Reisdorph

The Windows API provides several functions for accessing the Windows shell. The functions include functions for displaying the Windows shell browser, for adding to the recent documents folder, for accessing the recycle bin, for adding icons to the system tray, for spawning applications, and so on. We have covered some of these functions in previous articles and most of them are fairly easy to implement.

One aspect of the Windows shell that is not quite as straightforward is the task of enumerating the shell namespace. You may want to enumerate the shell namespace in order to provide a custom view into the shell for your applications. For example, you may wish to provide a tree view that shows specific folders and files beyond what the shell browser provides. Or maybe you need a tree view that can be placed on a form rather than using the browser dialog as provided by Windows.

This article will explain how to enumerate the Windows shell. First I will explain the basics of enumerating the shell using the IShellFolder and IEnumIDList COM interfaces. After that I will show how to obtain the display name and attributes for items in the shell. The functions and constants discussed in this article are defined in SHELLAPI.H and SHLOBJ.H so you will need to include these headers in any application that enumerates the shell.

Folders and Pidls

Manipulating the Windows shell requires an understanding of folders and item identifier lists. Iíll discuss these two elements in the following sections.

Folders and the IShellFolder interface

For the most part, shell folders are not difficult to comprehend. If you run Windows Explorer you will see folders representing My Computer, the Recycle Bin, and, if you are on a network, the Network Neighborhood. Opening the My Computer folder results in Explorer displaying subfolders that contain your file system, the Control Panel, the Printers folder, and so on.

Folders are represented by an instance of the IShellFolder interface. This interface is the key to manipulating shell folders. Once you have a pointer to an IShellFolder interface for a particular folder you can get the details for an item in the folder (display name, icon, file type, etc.), enumerate the folder, rename an item, or get the shell context menu for an item. Table A lists the most often used IShellFolder functions and gives a description of each. Later you will see examples of how to use many of these functions.

Table A: Commonly-used IShellFolder functions.

Function

Description

BindToObject()

Retrieves an IShellFolder interface for a subfolder.

EnumObjects()

Retrieves a pointer to an IEnumIDList interface in order to enumerate the folder.

GetAttributes()

Retrieves the attributes of a particular item in the folder.

GetDisplayName()

Retrieves the display name of an item in the folder. The string returned can be the normal
display name (as shown in Explorer) or a display name that can be used for parsing (the full path and filename of the item).

GetUIObjectOf()

Retrieves a pointer to a supporting COM interface. Typically used to get a pointer to an IContextMenu or IShellDetails interface.

ParseDisplayName()

Retrieves a pidl from a file system path.

SetNameOf()

Renames an item in the folder.

Understanding pidls

Most IShellFolder functions take a pidl as a parameter. A pidl is a pointer to an array of item IDs. Each item ID in the list represents a level in the shell namespace. Take this path, for example:

c:\Windows\System\shell32.dll

In this case, the pidl for this path and filename will contain five item IDs, one for each level in the shell hierarchy:

My Computer
  c:\
    Windows
      System
        shell32.dll

A pidl by itself is practically useless. Put another way, you cannot inspect a pidl with the C++Builder debugger and hope to find any meaningful information. Instead, you must call the appropriate Win32 API shell function or one of the IShellFolder functions to obtain information about the pidl.

Pidls are without question the most confusing aspect of dealing with the shell. This is true for several reasons. For one, an item ID list is a variable-length structure. This makes it difficult to determine how many items are in the pidl, how to reference a particular item in the pidl, or how to find one item ID relative to another (to find an itemís parent item ID, for example).

Another reason pidls are confusing is that they are created by the shell relative to the parent folder. In the previous example, I said that the pidl would contain five item IDs. This assumes that the parent folder used to obtain the pidl was the desktop folder (Iíll explain the desktop folder a little later). If, however, the parent folder were C:\WINDOWS\SYSTEM, then the pidl would only contain one item, the item ID of the SHELL32.DLL file. With that in mind, Table B lists the types of pidls you may encounter and a description of each.

Table B: Types of pidls returned by the shell.

Pidl TypeDescription

simpleContains a single item ID relative to the parent folder.

complexContains multiple item IDs relative to the parent folder.

fully-qualifiedContains one or more item IDs, but always relative to the desktop folder.

Some shell operations require a fully-qualified pidl in order to work properly. Other shell operations require a simple pidl. You will see examples later on in the article and you will learn more about pidls at that time.

It all starts at the desktop

The first step in enumerating the Windows shell is to obtain an IShellFolder interface for the desktop folder. The desktop folder is the root of all shell folders and, by extension, all items within those folders. You obtain an IShellFolder interface for the desktop folder by calling the SHGetDesktopFolder() function. Hereís how the code looks:

LPSHELLFOLDER DesktopFolder;
SHGetDesktopFolder(&DesktopFolder);

Obviously, this step is simple. Once you have the folder object for the desktop folder you can move on to enumerating the folders and items contained within the shell.

Enumerating folders

After obtaining a pointer to the desktop folder you must call EnumObjects() to obtain a pointer to an IEnumIDList interface for the folder. The call to EnumObjects() looks like this:

LPENUMIDLIST Enum;
DWORD EnumFlags = SHCONTF_FOLDERS;
DesktopFolder->EnumObjects(
  Handle, EnumFlags, &Enum);

The EnumObjects() function takes three parameters. The first parameter is the window handle that will serve as the parent for any dialogs that Windows displays during the enumeration. For example, if you attempt to enumerate the folder representing a floppy drive and there is no diskette in the drive, Windows will automatically display a message indicating that the drive is not ready.

The second parameter is a set of flags that determine how the enumeration operates. In this example, only the SHCONTF_FOLDERS constant is used. This will cause the shell to only enumerate objects that are themselves folders. Other constants for the flags parameter include SHCONTF_NONFOLDERS (for items that are not folders) and SHCONTF_INCLUDEHIDDEN (for enumerating hidden files and folders). If you wanted to enumerate all objects in a folder, then, you would declare the EnumFlags variable like this:

DWORD EnumFlags = 
  SHCONTF_FOLDERS | SHCONTF_NONFOLDERS |
  SHCONTF_INCLUDEHIDDEN;

The final parameter for EnumObjects() is the address of a variable that will contain a pointer to the IEnumIDList interface if the call succeeds. You must check the return value of EnumObjects() to see if the call succeeded. If you do not, you will get an access violation when you attempt to use the pointer if EnumObjects() fails.

Once you have a pointer to an IEnumIDList interface you can enumerate all of the objects in the parent folder (the desktop in this case). This is done by calling the Next() function of IEnumIDList. Hereís an example:

DWORD Result = Enum->Next(1, &Pidl, 0);

The first parameter of this function is the number of items you wish to retrieve. In theory, you should be able to specify any number of items to retrieve. In reality, though, I have never seen an example that specifies anything other than 1 for this parameter. Further, passing a value greater than 1 appears to have no effect. The second parameter is a pointer to an item ID list. If the call to Next() is successful, this parameter will contain the pidl for an item in the folder. Iíll explain what to do with this pidl in a later section. The third parameter is a pointer to a DWORD that, on success, will return the number of items enumerated. If you are not interested in the number of items enumerated you can set this parameter to NULL. Next() will return NOERROR (0) on success, S_FAIL (1) if there are no more items to enumerate, or an error code if an error occurs.

Typically you will enumerate objects using a while loop:

LPITEMIDLIST Pidl;
DWORD Result = Enum->Next(1, &Pidl, 0);
while (Result != S_FALSE) {
  if (Result != NOERROR)
    break;
  // do something with Pidl
  SHGetMalloc(&Malloc);
  Malloc->Free(Pidl);
  Malloc->Release();
  Result = Enum->Next(1, &Pidl, 0);
}
Enum->Release(); 

The first line of code in the while loop checks the value of Result to see if some error occurred during enumeration. For example, an error may occur if a particular folder is no longer valid. If an error occurs, the loop is terminated. Naturally, you must do something with the pidl returned by the Next() function. I will address that in the next section. After the pidl is processed, the shellís memory allocator is used to free the pidl returned by the Next() function. If you do not perform this step, your program will leak memory. When the loop terminates, the memory allocated for the IEnumIDList interface is freed using the Release() function.

Putting the pidl to good use

Once you have a pidl you can use it to obtain information about the folder item that the pidl represents. The information you can obtain from a pidl includes:

There are two ways to obtain this information. One way is to use the functions of IShellFolder. The other way is to use the shellís SHGetFileDetails() function. In this section I will explain how to get the display name and path using IShellFolderís GetDisplayNameOf() and GetAttributesOf() functions.

Getting the display name

The display name of the item is obtained using the GetDisplayNameOf() function. GetDisplayNameOf() takes three parameters. The first parameter is the pidl of the item for which you want to obtain the display name. The second parameter is used to specify flags that determine the way the display name will be returned. The third parameter is a pointer to a STRRET structure. STRRET will contain the display name if the function returns successfully.

The flags parameter is specified by type. The SHGDN_NORMAL value is used to indicate that the pidl should be evaluated relative to the desktop. The SHGDN_INFOLDER value is used when you want the display name relative to the parent folder, and not relative to the desktop. In addition to these values, you can also specify modifier values. The only modifier of significance is SHGDN_FORPARSING. When this value is present in the flags parameter, the shell will return the path (and filename if applicable) rather than the display name. When the SHGDN_NORMAL flag is used alone, the shell will return the display name of an item as you see it in Explorer.

Hereís how a call to GetDisplayNameOf() might look:

STRRET StrRet;
StrRet.uType = STRRET_CSTR;
ParentFolder->GetDisplayNameOf(
    Pidl, SHGDN_NORMAL, &StrRet);

This looks easy enough, but the real work comes in extracting the display name from the STRRET structure. Here is the declaration for STRRET:

typedef struct _STRRET
{
  UINT uType;
  union
  {
    LPWSTR pOleStr;
    LPSTR  pStr;
    UINT   uOffset;
    char   cStr[MAX_PATH];
  } DUMMYUNIONNAME;
} STRRET, *LPSTRRET;

A display name can be returned in one of three forms. Table C lists the constants that represent the different forms and a description of each. You can specify the way that you want the string returned by setting the uType member to one of the values in Table C prior to calling GetDisplayNameOf(). However, this is often an exercise in futility, because Windows will determine how the string is returned. After the call to GetDisplayNameOf() you must check the value of the uType member to see how Windows returned the string. For example:

String DisplayName;
switch (StrRet.uType) {
  case STRRET_CSTR :
    DisplayName = StrRet.cStr;
    break;
  case STRRET_WSTR :
    DisplayName = WideCharToString(StrRet.pOleStr);
    break;
  case STRRET_OFFSET :
    DisplayName = ((char*)Pidl) + StrRet.uOffset;
}

It has been my experience that Windows most often returns the string as an offset into the pidl.

Table C: STRRET display type constants.

Type ConstantDescription

STRRET_WSTRThe string is returned in pOleStr as a wide string.

STRRET_OFFSETThe string is embedded in the item ID list. The uOffset member contains the offset value.

STRRET_CSTRThe string is returned in cStr as a null-terminated string.

The SHGetFileInfo() function provides an easier way of getting the display name for an item. However, it is not as flexible as GetDisplayNameOf(). For example, SHGetFileInfo() will give you only the display name as displayed in Explorer. It cannot give you the path of a particular item. SHGetFileInfo() does, however, give you other important information about a pidl. I will discuss SHGetFileInfo() in more detail next month when I explain how to get display icons for shell items.

Getting the attributes

When I speak of getting the attributes, I am not talking about the file attributes. Rather, I am talking shell item attributes. The list of possible attributes are listed under the IShellFolder::GetAttributesOf() topic on the Microsoft Developer Network (MSDN) CD. If you do not have MSDN you can find the attribute constants in SHLOBJ.H. The entire list of attributes is too long to list here, but Table D shows the most commonly used attributes.

Table D: The most commonly used shell item attributes.

Attribute ConstantDescription

SFGAO_CANCOPYThe item can be copied.

SFGAO_CANMOVEThe item can be moved.

SFGAO_CANRENAMEThe item can be renamed.

SFGAO_CANDELETEThe item can be deleted.

SFGAO_LINKThe item is a shortcut.

SFGAO_SHAREThe item is shared (for shared drives, printers, and folders).

SFGAO_FOLDERThe item is a folder that may contain other items.

SFGAO_FILESYSTEMThe item is part of the file subsystem.

SFGAO_HASSUBFOLDERThe item has subfolders.

SFGAO_REMOVABLEThe item has removable media.

SFGAO_COMPRESSEDThe item is in a compressed folder.

You retrieve item attributes using the IShellFolder::GetAttributesOf() function. GetAttributesOf() takes three parameters. The first parameter is used to specify the number of items for which you want to obtain the attributes. This parameter is usually set to 1. The second parameter is the address of the pidl representing the item within the folder. The final parameter is the address of a DWORD variable that will contain the attributes of the item if GetAttributesOf() is successful. You request specific attributes by setting the attributes variable prior to calling GetAttributesOf(). For example:

DWORD Attr = SFGAO_FOLDER;
ParentFolder->GetAttributesOf(
  1, (LPCITEMIDLIST*)&Pidl, &Attr);
if ((Attr & SFGAO_FOLDER)==SFGAO_FOLDER)
  // itís a folder

This code shows how to determine if a particular item in a folder is itself a folder. You can request as many attributes as you want for a single call to GetAttributesOf(). For example, letís say you wanted to know if the given item was a folder in the file system and whether or not the folder was compressed. In that case you would set up the Attr variable like this:

DWORD Attr = SFGAO_FOLDER | 
  SFGAO_FILESYSTEM | SFGAO_COMPRESSED;

After calling GetAttributesOf() you can check the value of Attr to see if a specific attribute exists.

There is one important aspect of attributes that you need to be aware of. You may be tempted to simply set the Attr variable to all available attribute constants. Doing so, however, will dramatically slow down the call to GetAttributesOf(). If you are enumerating the entire file system this will mean that your enumeration will be very slow. It is best to set the Attr variable to only those attributes you need for a given type of enumeration.

Getting an IShellFolder from a pidl

Another thing you may want to do with a pidl is to obtain an IShellFolder interface for the folder that the pidl represents. To do this, you call the BindToObject() function of IShellFolder. Given a pidl and a parent folder you obtain an IShellFolder interface for the pidl like this:

ParentFolder->BindToObject(Pidl, 
  0, IID_IShellFolder, (void**)&Folder);

If the call to BindToObject() is successful, the return value will be NOERROR and the Folder variable will contain the address of the interface. BindToObject() will fail if the shell item represented by the pidl is not a folder object, or if the pidl is not a direct child of the parent folder.

The example program

The example program for this article can be found in ENUMSHELL.BPR. The main unitís header and source code are shown in Listings A and B. This program enumerates the shell using parameters set on the main form. The parameters include whether you want to enumerate folders or non-folder items, and how many levels to enumerate.

The results are displayed in a memo on the main form. The display name is shown in the memo, followed by the full path.

The Enumerate() method of the main form uses recursion to enumerate additional levels, obtaining an IShellFolder interface for each new folder as it goes. Bear in mind that if you are on a network, the example program may appear to hang when enumerating network drives more than two levels deep, and if you are enumerating non-folder items. Figure A shows the main form when the application is run.

Figure A

The example program enumerates the shell given the specified number of levels and the enumeration options.

In order to keep the code as basic as possible, the example program does not contain features that you may need in your applications. For example, before enumerating a particular drive you should check the drive to see if has removable media and whether the media is present. In the case of network drives, you should check to see if the drive is connected before attempting to enumerate that drive. These are not required features, as the shell will display the appropriate dialog when it encounters these situations. Still, you may want to handle these situations yourself rather than depending on the shell.

Conclusion

Enumerating the Windows shell is not trivial, especially if you are approaching it without the benefit of this article. This article explained how to use IShellFolder and IEnumIDList to enumerate the shell. By using the techniques in this article you can add shell enumeration features to your applications.

Next month I will explain how to get display icons for shell items, and how to display the context menu for a shell item.

Listing A: MAINU.H

#ifndef MainUH
#define MainUH

#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ExtCtrls.hpp>

class TForm1 : public TForm
{
__published:
  TMemo *Memo1;
  TGroupBox *GroupBox1;
  TCheckBox *EnumFoldersCb;
  TCheckBox *EnumNonFoldersCb;
  TButton *EnumBtn;
  TEdit *NumLevelsEdit;
  TLabel *Label1;
  TButton *StopBtn;
  void __fastcall EnumBtnClick(TObject *Sender);
  void __fastcall StopBtnClick(TObject *Sender);
private:    
  LPMALLOC Malloc;
  int NumLevels;
  bool Abort;
  void Enumerate(IShellFolder* ParentFolder,
    bool EnumNonFolders, int levels);
  String GetDisplayName(LPITEMIDLIST Pidl,
    IShellFolder* ParentFolder,
    DWORD type = SHGDN_NORMAL);
public:    
  __fastcall TForm1(TComponent* Owner);
  __fastcall ~TForm1();
};

extern PACKAGE TForm1 *Form1;

#endif

Listing B: MAINU.CPP

#include <vcl.h>
#pragma hdrstop

#include <shellapi.h>
#include <shlobj.h>
#include "MainU.h"

#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)
  : TForm(Owner)
{
  // Get the shell's IMalloc interface.
  SHGetMalloc(&Malloc);
}

__fastcall TForm1::~TForm1()
{
  // Release the IMalloc interface.
  Malloc->Release();
}

void TForm1::Enumerate(IShellFolder* ParentFolder,
  bool EnumNonFolders, int Levels)
{
  LPITEMIDLIST Pidl;
  LPENUMIDLIST Enum;
  // Set up the enumeration flags based on the
  // main form's enum options check boxes.
  DWORD EnumFlags = SHCONTF_FOLDERS;
  if (EnumNonFolders)
    EnumFlags |= SHCONTF_NONFOLDERS;
  DWORD Result = ParentFolder->EnumObjects(
        Handle, EnumFlags, &Enum);
  // Error - return.
  if (Result != NOERROR)
    return;
  String DisplayName;
  // Get the pidl for the first item in the folder.
  Result = Enum->Next(1, &Pidl, 0);
  while (Result != S_FALSE) {
    int CurrentLevel = Levels;
    // If result is something other than
    // NOERROR then some error occured.
    if (Result != NOERROR)
      break;
    // Get the "normal" display name.
    DisplayName =
      GetDisplayName(Pidl, ParentFolder);
    // Add spaces to the string so each level
    // is indented.
    String PadStr = String().StringOfChar(
      ' ', (NumLevels - CurrentLevel) * 4);
    PadStr = PadStr + DisplayName;
    // Now get the full path using the
    // SHGDN_FORPARSING constant.
    DisplayName = GetDisplayName(
      Pidl, ParentFolder, SHGDN_FORPARSING);
    // Add it to the end of the string and
    // display the full string in the memo.
    Memo1->Lines->Add(
      PadStr + " (" + DisplayName + ")");
    CurrentLevel--;
    // See if this shell item is a folder.
    DWORD Attr = SFGAO_FOLDER;
    ParentFolder->GetAttributesOf(
      1, (LPCITEMIDLIST*)&Pidl, &Attr);
    if ((CurrentLevel > 0)
        && (Attr & SFGAO_FOLDER) == SFGAO_FOLDER) {
      LPSHELLFOLDER Folder;
      // Get the IShellFolder for the pidl.
      int res = ParentFolder->BindToObject(Pidl,
        0, IID_IShellFolder, (void**)&Folder);
      if (res == NOERROR) {
        // Recurse by calling Enumerate again.
        Enumerate(
          Folder, EnumNonFolders, CurrentLevel);
        // Free the memory for the new folder.
        Folder->Release();
      }
    }
    // Free the memory for the pidl returned
    // by the Enum->Next() function.
    Malloc->Free(Pidl);
    // If the user pressed the Stop button then
    // stop enumerating.
    Application->ProcessMessages();
    if (Abort)
      break;
    // Get a pidl for the next item in the folder.
    Result = Enum->Next(1, &Pidl, 0);
  }
  // Free the IEnumIDList interface.
  Enum->Release();
}

void __fastcall
TForm1::EnumBtnClick(TObject *Sender)
{
  StopBtn->Enabled = true;
  Abort = false;
  Memo1->Cursor = crHourGlass;
  Memo1->Lines->Clear();
  // Start with the desktop folder.
  LPSHELLFOLDER DesktopFolder;
  SHGetDesktopFolder(&DesktopFolder);
  // Set the number of levels.
  NumLevels = NumLevelsEdit->Text.ToIntDef(2);
  // Start enumerating. Enumerate is a recursive
  // function so we only call it once from here.
  Enumerate(DesktopFolder,
    EnumNonFoldersCb->Checked, NumLevels);
  // Free the IShellFolder for the desktop folder.
  DesktopFolder->Release();
  StopBtn->Enabled = false;
  Memo1->Cursor = crDefault;
}

String TForm1::GetDisplayName(LPITEMIDLIST Pidl,
  IShellFolder* ParentFolder, DWORD type)
{
  String DisplayName;
  STRRET StrRet;
  // Request the string as a char* although
  // Windows will likely ignore the request.
  StrRet.uType = STRRET_CSTR;
  // Call GetDisplayNameOf() to fill in the
  // STRRET structure.
  ParentFolder->GetDisplayNameOf(
    Pidl, type, &StrRet);
  // Extract the string based on the value
  // of the uType member of STRRET.
  switch (StrRet.uType) {
    case STRRET_CSTR :
      DisplayName = StrRet.cStr;
      break;
    case STRRET_WSTR :
      DisplayName =WideCharToString(StrRet.pOleStr);
      break;
    case STRRET_OFFSET :
      DisplayName = ((char*)Pidl) + StrRet.uOffset;
  }
  // Return the result.
  return DisplayName;
}

void __fastcall TForm1::StopBtnClick(TObject *Sender)
{
  // User clicked the Stop button so set the
  // Abort flag.
  Abort = true;
  StopBtn->Enabled = false;
  Memo1->Cursor = crDefault;
}