4

I'm trying to create a simple example of using IFileOperation to delete the files in a given directory, to include in the answer to another q for comparison with other methods.

Below is the code of my MRE. It successfully creates 1000 files in a subdirectory off C:\Temp and then attempts to delete them in the DeleteFiles method. This supposedly "easy" task fails but I'm not sure exactly where it comes off the rails. The comments in the code show what I'm expecting and the actual results. On one occasion, instead of the exception noted, I got a pop-up asking for confirmation to delete an item with an odd name which was evidently an array of numbers referring to a shell item, but my attempt to capture it using Ctrl-C failed;

I'm fairly sure I'm either missing a step or two, misusing the interfaces involved or both. My q is, could anybody please show the necessary corrections to the code to get IFileOperation.DeleteItems() to delete the files in question, as I am completely out of my depth with this stuff? I am not interested in alternative methods of deleting these files, using the shell interfaces or otherwise.

procedure TForm2.DeleteFiles;
var
  iFileOp: IFileOperation;
  iIDList : ItemIDList;
  iItemArray : IShellItemArray;
  iArray : Array[0..1] of ItemIDList;
  Count : DWord;
begin
  iFileOp := CreateComObject(CLSID_FileOperation) as IFileOperation;

  iIDList := ILCreateFromPath(sPath)^;

  //  IFileOperation.DeleteItems seems to require am IShellItemArray, so the following attempts
  //  to create one
  //  The definition of SHCreateShellItemArrayFromIDLists
  //  seems to require a a zero-terminated array of ItemIDLists so the next steps
  //  attempt to create one

  ZeroMemory(@iArray, SizeOf(iArray));
  iArray[0] := iIDList;
  OleCheck(SHCreateShellItemArrayFromIDLists(1, @iArray, iItemArray));

  //  Next test the number of items in iItemArray, which I'm expecting to be 1000
  //  seeing as the CreateFiles routine creats that many

  OleCheck(iItemArray.GetCount(Count));
  Caption := IntToStr(Count);  //  Duh, this shows Count to be 1, not the expected 1000

  OleCheck(iFileOp.DeleteItems(iItemArray));
  OleCheck( iFileOp.PerformOperations );
  // Returns Exception 'No object for moniker'

end;

procedure TForm2.Button1Click(Sender: TObject);
begin
  DeleteFiles;
end;

procedure CreateFiles;
var
  i : Integer;
  SL : TStringList;
  FileName,
  FileContent : String;
begin

  SL := TStringList.Create;
  try
    if not (DirectoryExists(sPath)) then
      MkDir(sPath);

    SL.BeginUpdate;
    for i := 0 to 999 do begin
      FileName := Format('File%d.Txt', [i]);
      FileContent := Format('content of file %s', [FileName]);
      SL.Text := FileContent;
      SL.SaveToFile(sPath + '\' + FileName);
    end;
    SL.EndUpdate;
  finally
    SL.Free;
  end;
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
  CreateFiles;
end;
MartynA
  • 30,454
  • 4
  • 32
  • 73

1 Answers1

7

You are leaking the memory returned by ILCreateFromPath(), you need to call ILFree() when you are done using the returned PItemIDList.

Also, you should not be dereferencing the PItemIDList. SHCreateShellItemArrayFromIDLists() expects an array of PItemIDList pointers, but you are giving it an array of ItemIDList instances.

Try this instead:

procedure TForm2.DeleteFiles;
var
  iFileOp: IFileOperation;
  iIDList : PItemIDList;
  iItemArray : IShellItemArray;
  Count : DWord;
begin
  iFileOp := CreateComObject(CLSID_FileOperation) as IFileOperation;

  iIDList := ILCreateFromPath(sPath);
  try
    OleCheck(SHCreateShellItemArrayFromIDLists(1, @iIDList, iItemArray));
  finally
    ILFree(iIDList);
  end;

  //  Next test the number of items in iItemArray, which I'm expecting to be 1000
  //  seeing as the CreateFiles routine creates that many

  OleCheck(iItemArray.GetCount(Count));
  Caption := IntToStr(Count);  //  Duh, this shows Count to be 1, not the expected 1000

  OleCheck(iFileOp.DeleteItems(iItemArray));
  OleCheck( iFileOp.PerformOperations );
  // Returns Exception 'No object for moniker'
end;

That being said, even if this were working correctly, you are not creating an IShellItemArray containing 1000 IShellItems for the individual files. You are creating an IShellItemArray containing 1 IShellItem for the C:\Temp subdirectory itself.

Which is fine if your goal is to delete the whole folder. But in that case, I would suggest using SHCreateItemFromIDList() or SHCreateItemFromParsingName() instead, and then pass that IShellItem to IFileOperation.DeleteItem().

But, if your goal is to delete the individual files without deleting the subdirectory as well, then you will have to either:

  • get the IShellFolder interface for the subdirectory, then enumerate the relative PIDLs of its files using IShellFolder.EnumObjects(), and then pass the PIDLs in an array to SHCreateShellItemArray().

  • get the IShellFolder interface of the subdirectory, then query it for an IDataObject interface using IShellFolder.GetUIObjectOf(), and then use SHCreateShellItemArrayFromDataObject(), or just give the IDataObject directly to IFileOperation.DeleteItems().

  • get an IShellItem interface for the subdirectory, then query its IEnumShellItems interface using IShellItem.BindToHandler(), and then pass that directly to IFileOperation.DeleteItems().

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Many thanks, greatly appreciated. I'll explore your alternative suggestions a bit later, but meanwhile this has obviously answered my q so +1. – MartynA Oct 29 '19 at 20:20