Using the Windows Shell to enumerate files

In the course of devising a ShellTreeView derived component for a backup program, the objective of which is to enumerate the files contained in folders of the Windows Shell, two methods were considered for this enumeration: the Find and the Shell method. The Find method is documented in the above article whereas the Shell method will be described hereunder.

We will briefly described the Windows shell, the IShellFolder interface that provides access to it, and how it can be used to enumerate the files in a folder. With this preparation, we will then describe the code we used for the enumaration of the files contained in several folders.

The Windows Shell

The Microsoft® Windows® user interface (UI) provides users with access to a wide variety of objects necessary for running applications and managing the operating system. The most numerous and familiar of these objects are the folders and files that reside on computer disk drives. There are also a number of virtual objects that allow the user to do tasks such as sending files to remote printers or accessing the Recycle Bin. The Shell organizes these objects into a hierarchical namespace structure, and provides users and applications with a consistent and efficient way to access and manage objects.

One of the primary responsibilities of the Shell is managing and providing access to the wide variety of objects that make up the system. The most numerous and familiar of these objects are the folders and files that reside on computer disk drives. However, the Shell manages a number of nonfile system, or virtual objects, as well. The union of the file and non-file objects forms what is called the Namespace. It is a superset of the file system, a hierarchical structure, the nodes of which are called folder objects.

Using IShellFolder

As a matter of fact, the Windows shell is an automation server that provides its services through a certain number of COM interfaces. In its duties as the manager of the file system, the Windows shell provides IShellFolder interfaces, one for each node of the namespace.

The first step in using a folder object is to retrieve a pointer to its IShellFolder interface. In addition to providing access to the object's other interfaces, IShellFolder exposes a group of methods that handle a number of common tasks:

IShellFolder = interface(IUnknown)
  [SID_IShellFolder]
function ParseDisplayName(hwndOwner: HWND;
  pbcReserved: Pointer; lpszDisplayName: POLESTR; out pchEaten: ULONG;
  out ppidl: PItemIDList; var dwAttributes: ULONG): HResult; stdcall;
function EnumObjects(hwndOwner: HWND; grfFlags: DWORD; out EnumIDList: IEnumIDList): HResult; stdcall;
function BindToObject(pidl: PItemIDList; pbcReserved: Pointer;
  const riid: TIID; out ppvOut): HResult; stdcall;
function BindToStorage(pidl: PItemIDList; pbcReserved: Pointer;
  const riid: TIID; out ppvObj): HResult; stdcall;
function CompareIDs(lParam: LPARAM;pidl1, pidl2: PItemIDList): HResult; stdcall;
function CreateViewObject(hwndOwner: HWND; const riid: TIID; out ppvOut): HResult; stdcall;
function GetAttributesOf(cidl: UINT; var apidl: PItemIDList; var rgfInOut: UINT): HResult; stdcall;
function GetUIObjectOf(hwndOwner: HWND; cidl: UINT; var apidl: PItemIDList;
  const riid: TIID; prgfInOut: Pointer; out ppvOut): HResult; stdcall;
function GetDisplayNameOf(pidl: PItemIDList; uFlags: DWORD; var lpName: TStrRet): HResult; stdcall;
function SetNameOf(hwndOwner: HWND; pidl: PItemIDList; lpszName: POLEStr;
  uFlags: DWORD; var ppidlOut: PItemIDList): HResult; stdcall;
end;

To retrieve a pointer to a namespace object's IShellFolder interface, one must first call the function SHGetDesktopFolder. This function returns a pointer to the IShellFolder interface of the namespace root, the desktop. Once you have the desktop's IShellFolderinterface, there a variety of ways to proceed.

If you already have the PIDL of the folder you are interested infor instance, by calling SHGetFolderLocationyou can retrieve its IShellFolder interface by calling the desktop's BindToObject method. If you have the path of a file system object, you must first obtain its PIDL by calling the desktop's ParseDisplayName method and then call BindToObject.

Enumerating the content of a folder

The first thing you usually want to do with a folder is to find out what it contains. You must first call the folder's EnumObjects method. The folder will create a standard OLE enumeration object and return its IEnumIDList interface. This interface exposes four standard methodsClone, Next, Reset, and Skipthat can be used to enumerate the contents of the folder.

The basic procedure for enumerating a folder's contents is:

  1. Call the folders EnumObjects method to retrieve a pointer to an enumeration object's IEnumIDList interface.
  2. Pass an unallocated >PIDL to Next. Next takes care of allocating the PIDL, but the application must deallocate it when it is no longer needed. When Next returns, the PIDL will contain just the object's item ID and the terminating NULL characters. In other words, it is a single-level PIDL, relative to the folder, not a fully-qualified PIDL.
  3. Repeat step 2 until Next returns S_FALSE to indicate that all items have been enumerated.
  4. Call IEnumIDList.Release to release the enumeration object.

This is about all that we need of the Windows shell in order to provide our component the capabilities that it needs.

The Shell method

This second method was considered for the production of the list of files to be backed up as I believed that it would be faster than the "find" method. Unfortunately, it was not.

Production of the list of files

As the intent of the component is to provide a way for the user to select the files that are to be archived as well as providing evidence of selection, this component produces a list of the files located in a given folder when an expanded folder is checked in the tree view. When a collapsed folder is checked, all its content as well as the content of all its descendents is selected for archiving.

In the second method that I have experimented with, this is accomplished as follows:

procedure TGtroCustomCheckShellTreeView.CheckIt1(const Node: TTreeNode; State: Boolean);
var
  Path: string;
  Level: Integer;
  ANode: TTreeNode;
begin
  FList.Clear;
  Self.Items.BeginUpdate;
  try
    SetNodeChecked(Node, State); // set the checkbox to its new state
    GetCheckedFiles(Node, FList); // navigate the file content of the node
    if (not Node.Expanded) then  // navigate all descendant nodes
    begin
      Level:= Node.Level;
      Node.Expand(True); // absolutely necessary in order to access the subnodes
      ANode:= Node.GetFirstChild;
      while (ANode <> nil) and (Level < ANode.Level)  do
      begin
        GetCheckedFiles(ANode, FList);
        SetNodeChecked(ANode, State);
        ANode:= ANode.GetNext; // get next child node
      end;
      Node.Collapse(True);
    end;
  finally
    Self.Items.EndUpdate;
  end; // try...finally
  if Assigned(FOnCheckChanged) then FOnCheckChanged(Self, Node, State, FList);
end;

When an expanded node is selected, this method is not that different from the "find" method. It exhibits differences when a collapsed node is checked. For each child node, the content of the node is enumerated with the GetCheckedFiles method and the child node is marked as checked or unchecked depending on the value of the argument State

The GetCheckedFiles method extracts the TShellFolder object pointed to by the Data member of the node and it obtains a reference on its IEnumIDList interface. This interface is then used to enumerate the files contained in the folder. Depending on the value of the argument State, files are added or deleted from the list.

procedure TGtroCustomCheckShellTreeView.GetCheckedFiles(ANode: TTreeNode;
  State: Boolean; List: TStrings);
var
  Folder: TShellFolder;
  HR: HRESULT;
  pIDList: PItemIDLIst;
  EnumList: IEnumIDList;
  NumIDs: LongWord;
  refIShellFolder: IShellFolder;
  AFolder: TShellFolder;
begin
  //Use the node to obtain content
  Folder:= TShellFolder(ANode.Data); // TShellFolder of the Node
  
  // Obtain reference on IEnumIDList interface
  HR:= Folder.ShellFolder.EnumObjects(Application.Handle, SHCONTF_NONFOLDERS + SHCONTF_INCLUDEHIDDEN, EnumList);
  if HR <> 0 then Exit;
  
  // Enumerate content
  while EnumList.Next(1, pIDList, NumIDs) = S_OK do
  begin
   refIShellFolder:= GetIShellFolder(Folder.ShellFolder, pIDList);
    AFolder:= TShellFolder.Create(Folder, pIDList, refIShellFolder);
    try
     if State then // add the file to the list
       List.Add(AFolder.PathName)
      else // remove the file from the list
       List.Delete(List.IndexOf(AFolder.PathName));
    finally
     AFolder.Free;
    end; // try...finally
  end; // while EnumList.Next
end;

Conclusion

 IWhen I started this task, I believed that using the Windows shell directly to enumerate files contained in folders was the fastest method  or at least, would be faster that using sequences of  FindFirst(), FindNext() and FindClose() functions. I was wrong and I found it from testing of the time used by both methods to enumerate files in several scenarios.. As a consequence, I dit not use the Windows shell method but decided to show how the method worked.

Warning!
This code was developed for the pleasure of it. Anyone who decides to use it does so at its own risk and agrees not to hold the author responsible for its failure.


Questions or comments?
E-Mail
Last modified: September 3rd 2014 13:28:15. []