Replacing SHFileOperation With IFileOperation To Process Multiple File Operations In One Call (Delphi Code)

delphi-ifileoperation
Starting with Windows Vista (and Windows Server 2008) the Windows API arsenal has a new and more powerful way of performing file operations like copy, move, delete and similar file and folder shell actions. The old (but still valid) SHFileOperation function is now replaced by the IFileOperation interface.
The IFileOperation exposes methods to copy, move, rename, create, and delete Shell items as well as methods to provide progress and error dialogs.

If you’ve used to working with SHFileOperation, and have been hit (for example) by the MAX_PATH problem, a solution can be found in IFileOperation. Here’s how to use IFileOperation in Delphi and ensure non existing folders are created in a copy operation.

The IFileOperation has many advantages over the SHFileOperation function, like being able to perform different operations in one call. You can delete a few files, copy some more, rename a folder, apply properties to a file – all in one operation. SHFileOperation can only do one operation at a time: copy, move, rename, or delete.

In this post I’ll concentrate on copying files. To make it simple the copy operation will copy a single file from its location to a destination folder and have the file name changed at the destination.

SHFileOperation

While you can locate lots of examples on how to use the SHFileOperation in Delphi, and the Jedi library also has it nicely wrapped in a component, here’s a short code to show you how to copy a file:

uses ShellApi;

function CopyFileSHFileOperation(const srcFile, destFile : string) : boolean;
var
  shFOS : TShFileOpStruct;
begin
  ZeroMemory(@shFOS, SizeOf(TShFileOpStruct));

  shFOS.Wnd := Application.MainForm.Handle;

  shFOS.wFunc := FO_COPY;

  shFOS.pFrom := PChar(srcFile + #0);
  shFOS.pTo := PChar(destFile + #0);

  //Do not ask the user to confirm the creation of a
  //new directory if the operation requires one to be created.
  shFOS.fFlags := FOF_NOCONFIRMMKDIR;

  result := SHFileOperation(shFOS) = 0;
end;

There’s one nice feature of the SHFileOperation: if the destination folder does not exist the function will create it! The FOF_NOCONFIRMMKDIR flag ensures no dialogs are presented to the user by Windows asking if the destination folder should be created.

IFileOperation

Here’s how you can copy a file using the IFileOperation interface

uses ActiveX, ComObj, ShlObj;;

function CopyFileIFileOperation(const srcFile, destFile : string) : boolean;
//works on Windows >= Vista and 2008 server
var
  r : HRESULT;
  fileOp: IFileOperation;
  siSrcFile: IShellItem;
  siDestFolder: IShellItem;
  destFileFolder, destFileName : string;
begin
  result := false;

  destFileFolder := ExtractFileDir(destFile);
  destFileName := ExtractFileName(destFile);

  //init com
  r := CoInitializeEx(nil, COINIT_APARTMENTTHREADED or COINIT_DISABLE_OLE1DDE);
  if Succeeded(r) then
  begin
    //create IFileOperation interface
    r := CoCreateInstance(CLSID_FileOperation, nil, CLSCTX_ALL, IFileOperation, fileOp);
    if Succeeded(r) then
    begin
      //set operations flags
      r := fileOp.SetOperationFlags(FOF_NOCONFIRMATION OR FOFX_NOMINIMIZEBOX);
      if Succeeded(r) then
      begin
        //get source shell item
        r := SHCreateItemFromParsingName(PChar(srcFile), nil, IShellItem, siSrcFile);
        if Succeeded(r) then
        begin
          //get destination folder shell item
          r := SHCreateItemFromParsingName(PChar(destFileFolder), nil, IShellItem, siDestFolder);

          //add copy operation
          if Succeeded(r) then r := fileOp.CopyItem(siSrcFile, siDestFolder, PChar(destFileName), nil);
        end;

        //execute
        if Succeeded(r) then r := fileOp.PerformOperations;

        result := Succeeded(r);

        OleCheck(r);
      end;
    end;

    CoUninitialize;
  end;
end;

Note that the PerformActions method executes actions added by calling individual methods like CopyItem(s), DeleteItem(s) and alike.

However, there’s one BIG problem in using IFIleOperation: in a copy action, if the destination folder does not exist the action will fail!

This is true even if FOF_NOCONFIRMMKDIR is set using the SetOperationFlags method.

Note the line where SHCreateItemFromParsingName is used for the second time:

//get destination folder shell item
r := SHCreateItemFromParsingName(PChar(destFileFolder), nil, IShellItem, siDestFolder);

It creates and initializes the shell item for the destination folder, and if this folder does not exist the call would fail.

IFileOperation.CopyItem + Force Destination Directory

The solution is to be found in the second parameter: const pbc: IBindCtx; This is a pointer to a bind context used to pass parameters to the parsing function (in this case SHCreateItemFromParsingName). We can use the binding context to force SHCreateItemFromParsingName not to query the file system – rather to just use what we provide. And what we will provide is a complex mixture of WIN32_FIND_DATA structure (specifying FILE_ATTRIBUTE_DIRECTORY), and instance of an object implementing IFileSystemBindData interface.

Here’s the full code, and the implementation of the required interface.

uses ActiveX, ComObj, ShlObj;

function CopyFileIFileOperationForceDirectories(const srcFile, destFile : string) : boolean;
//works on Windows >= Vista and 2008 server
var
  r : HRESULT;
  fileOp: IFileOperation;
  siSrcFile: IShellItem;
  siDestFolder: IShellItem;
  destFileFolder, destFileName : string;
  pbc : IBindCtx;
  w32fd : TWin32FindData;
  ifs : TFileSystemBindData;
begin
  result := false;

  destFileFolder := ExtractFileDir(destFile);
  destFileName := ExtractFileName(destFile);

  //init com
  r := CoInitializeEx(nil, COINIT_APARTMENTTHREADED or COINIT_DISABLE_OLE1DDE);
  if Succeeded(r) then
  begin
    //create IFileOperation interface
    r := CoCreateInstance(CLSID_FileOperation, nil, CLSCTX_ALL, IFileOperation, fileOp);
    if Succeeded(r) then
    begin
      //set operations flags
      r := fileOp.SetOperationFlags(FOF_NOCONFIRMATION OR FOFX_NOMINIMIZEBOX);
      if Succeeded(r) then
      begin
        //get source shell item
        r := SHCreateItemFromParsingName(PChar(srcFile), nil, IShellItem, siSrcFile);
        if Succeeded(r) then
        begin
          //create binding context to pretend there is a folder there
          if NOT DirectoryExists(destFileFolder) then
          begin
            ZeroMemory(@w32fd, Sizeof(TWin32FindData));
            w32fd.dwFileAttributes := FILE_ATTRIBUTE_DIRECTORY;
            ifs := TFileSystemBindData.Create;
            ifs.SetFindData(w32fd);
            r := CreateBindCtx(0, pbc);
            r := pbc.RegisterObjectParam(STR_FILE_SYS_BIND_DATA, ifs);
          end
          else
            pbc := nil;

          //get destination folder shell item
          r := SHCreateItemFromParsingName(PChar(destFileFolder), pbc, IShellItem, siDestFolder);

          //add copy operation
          if Succeeded(r) then r := fileOp.CopyItem(siSrcFile, siDestFolder, PChar(destFileName), nil);
        end;

        //execute
        if Succeeded(r) then r := fileOp.PerformOperations;

        result := Succeeded(r);

        OleCheck(r);
      end;
    end;

    CoUninitialize;
  end;
end;

Here’s the TFileSystemBindData, IFileSystemBindData interface implementation:

type
  TFileSystemBindData = class (TInterfacedObject, IFileSystemBindData)
    fw32fd: TWin32FindData;

    function SetFindData(var w32fd: TWin32FindData): HRESULT; stdcall;
    function GetFindData(var w32fd: TWin32FindData): HRESULT; stdcall;
  end;
...
function TFileSystemBindData.GetFindData(var w32fd: TWin32FindData): HRESULT;
begin
  w32fd:= fw32fd;
  Result := S_OK;
end;

function TFileSystemBindData.SetFindData(var w32fd: TWin32FindData): HRESULT;
begin
  fw32fd := w32fd;
  Result := S_OK;
end;

Finally, the usage goes like:

//works even if "d:\f1\f2\f3\" does not exist!
CopyFileIFileOperationForceDirectories('c:\somefile.png', 'd:\f1\f2\f3\copiedfile.png');

That’s it, now you can use IFIleOperation instead of SHFileOperation. Of course, you need to make sure your code runs on at least Windows Vista or Windows Server 2008. You can use the TOSVersion to check the operating system your code runs on.

7 thoughts on “Replacing SHFileOperation With IFileOperation To Process Multiple File Operations In One Call (Delphi Code)

  1. Joe

    Hi Žarko, thanks a lot for your posting on IFileOperation.
    Could that code be expanded to copy folders (including all subfolders) instead of single files?

    Reply
    1. zarkogajic Post author

      @Joe:

      Just use:

      CopyFileIFileOperationForceDirectories(‘c:\sourcefolder’, ‘d:\destination’);

      And all files and subfolders of “c:\sourcefolder” would be copied into “d:\destination”.

      Reply
  2. Dieter

    This code is very useful and I would try to use it in my program, but how can I use a progressbar with it that was embedded in the application gui and not as a dialogue?

    Reply
  3. Thorsten

    Thanks of the interesting article! Are there any performance difference doing the same operations using SHFileOperations and using IFileOperations? I am very interested in timing results. Thanks

    Reply
  4. Martin

    TWin32FindData is defined in WinApi.Windows, so this should be included in the uses statement:
    uses ActiveX, ComObj, ShlObj, Windows;

    Reply

Leave a Reply to Henrik C Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.