IOUtils.pas – OO File System Access in Delphi 2010

Delphi has long had various functions to let you work with the file system, but one of the new features of Delphi 2010 are some nice wrappers for working with files, paths and directories.

IOUtils.pas is the file in question, and it’s definitely worth a look. Each time I’ve had a look in there during the Field Test I’ve found more interesting stuff to play with. While I’m still exploring it, I thought I’d post a few examples to show you the kind of things you can get up to.

Firstly, something easy:

procedure TForm2.Button2Click(Sender: TObject);  
var   
  Path : string; 
begin 
  if not TDirectory.Exists(edtPath.Text) then 
    Caption := 'Invalid Path' 
  else 
    Caption := edtPath.Text;
 
  ListBox1.Clear;

  for Path in TDirectory.GetFiles(edtPath.Text, edtFilter.Text) do 
    Listbox1.Items.Add(Format('Name = %s', [Path])); 
end;

First we check if the path provided in the edtPath edit box exists. Assuming it does, we then use a for..in statement to get back each file in that directory that matches the filter passed in as the second parameter.

If you look a little further in IOUtils.pas, you’ll see TDirectory has a type declared in it called a TFilterPredicate:

TFilterPredicate = reference to function(const Path: string; const SearchRec: TSearchRec): Boolean;

One of the places it is used is in an overloaded version of GetFiles, like so:

procedure TForm2.Button1Click(Sender: TObject); 
var 
  Path : string; 
  FilterPredicate : TDirectory.TFilterPredicate; 
begin 
  if not TDirectory.Exists(edtPath.Text) then 
    Caption := 'Invalid Path' 
  else 
    Caption := edtPath.Text;

  ListBox1.Clear;

  FilterPredicate := function(const Path: string; const SearchRec: TSearchRec): Boolean 
                     begin 
                       Result := (TPath.MatchesPattern(SearchRec.Name, edtFilter.Text, False)) AND 
                                 (SearchRec.Attr = faArchive); 
                     end;

  for Path in TDirectory.GetFiles(edtPath.Text, FilterPredicate) do 
    Listbox1.Items.Add(Format('Name = %s', [Path])); 
end;

We implement the TFilterPredicate anonymous method to check that each file matches a wildcard pattern and that it also has its Archive flag set. We can then pass it into our call to TDirectory.GetFiles and our code in the for..in statement will only be called for files that match our criteria.

This is all fine for a single directory, but what if we want to walk over the full tree of directories? Easy, just alter our code to look like this:

procedure TForm2.Button3Click(Sender: TObject);
var
  Path, Directory : string;
  FilterPredicate : TDirectory.TFilterPredicate;
begin
  if not TDirectory.Exists(edtPath.Text) then
    Caption := 'Invalid Path'
  else
    Caption := edtPath.Text;

  ListBox1.Clear;

  FilterPredicate := function(const Path: string; const SearchRec: TSearchRec): Boolean
                     begin
                       Result := (TPath.MatchesPattern(SearchRec.Name, edtFilter.Text, False)) AND 
                                 (SearchRec.Attr = faArchive);
                     end;

  for Directory in TDirectory.GetDirectories(edtPath.Text, TSearchOption.soAllDirectories, nil) do
    for Path in TDirectory.GetFiles(Directory, FilterPredicate) do
      Listbox1.Items.Add(Format('Name = %s', [Path]));
end;

We add another for..in statement before the existing one. This one for each Directory in TDirectory.GetDirectories. The second parameter indicates whether we only want to get the top level directories in the path we specify, or whether we want to do all directories , all the way down. In this case I’m specifying the latter. The Third parameter is actually another TDirectory.TFilterPredicate, so we could do further filtering of the directories that match, however I’m passing in nil so all directories will match. Just keep your wits about you while testing, and don’t test it with c:\ as I did

One thing to note, TDirectory.GetDirectories returns child directories of the path we’ve specified (and their child directories, etc if you’ve specified TSearchOption.soAllDirectories), but obviously not the path we pass in itself. That probably sounds obvious, but in this example we’ve subtly altered the behaviour a little, because the files in the directory we specify are not checked anymore. Now you’re aware of this, I’m sure you can figure out how to deal.

There’s a whole bunch more that can be done, such as encrypt files, move directories, etc. IOUtils looks to be a really powerful unit, and is well worth a little playing to get familiar with it.

16 Comments

  • Twitter Comment


    Malcolm Grovesさん「IOUtils.pas – OO File System Access in Delphi 2010」[link to post] これイイヨ!。ようやくDelphiのファイル操作も今風の構文で出来るようになったのね。

    Posted using Chat Catcher

  • Twitter Comment


    IOUtils.pas – OO File System Access in Delphi 2010 [link to post]

    Posted using Chat Catcher

  • […] Malcolm Groves blogs about some one stuff in the RTL to handle files, paths, and directories.  […]

  • That’s really interesting. I hope you tell us more about TFile and TPath too.

    Regards

  • Maybe I’m somehow pedantic in matter of “language semantic”

    I can’t see the point in declaring CLASS functions and procedures like this:
    TDirectory.Exists(…)

    Well, if I’m to declare a var of the type TDirectory and I want to use such functions, it would look like this:

    var
    MyDir: TDirectory;
    Begin
    MyDir := TDirectory.Create(‘C:\Temp\’); // don’t know if this kind of constructor exists, though. Just want to initialize MyDir with a root path
    If MyDir.Exists(‘D:\Whatever\’) then…

    It just doesn’t make any sence IMO.

    Too me, such class functions (“Exists”…) misuse the point of a strong typed language.

    just my 2c 🙂

  • I think that IOUtils contains the .NET System.IO namespace’s classes. The class/method names are the same.

  • LOÏS : Not sure I understand how this misuses strong typing, but I get that some people have an issue with using class methods like this. For me, the value of the enumerators and the filter predicates outweighs any squeemishness I might have.

    LittleGopher : Yes, it does seem to borrow some concepts, although on a quick review it’s not identical. Nothing wrong with that, good ideas are worth copying, and I’d hate to see a completely different API for no good reason other than to be different. Thanks for the tip though, I’m now reading some interesting System.IO examples that I might try and adapt.

  • […] OO File System Access in Delphi 2010 […]

  • First congratulations. This is a quite good step towards the real OOP Delphi. I hope we will see soon stuff like TInt32, TString, etc.

    I have the following two questions/requests:

    1. Please do NOT declare the TFile, TDirectory, TPath classes as SEALED.

    I hope you haven’t since in this way we will be able to extend them with whatever needed for IO handling like MD5, CRC, Compression, etc.

    I am so sick with the MS .NET having declared most of their stuff either ‘internal’ or ‘final’.

    Do not repeat their mistakes.

    2. Add as much as possible from the following

    TFile.Size
    TFile.GetVersion
    TFile.Exists
    TFile.StartsWith(TByteArray)
    TFile.Copy
    TFile.Rename
    TFile.Move
    TFile.Delete
    TFile.Read
    TFile.TReadAllText
    TFile.GetFullFileName
    TFile.GetFileName
    TFile.GetTitle
    TFile.GetExtension
    TFile.GetInvalidFileChars

    TDirectory.Exists
    TDirectory.Copy
    TDirectory.Rename
    TDirectory.Delete
    TDirectory.ListFiles
    TDirectory.Size
    TDirectory.Create

    // Provides a platform-specific character used to separate directory levels
    // in a path string that reflects a hierarchical file system organization.
    const DirectorySeparatorChar: Char = SysUtils.PathDelim;

    // Provides a platform-specific array of characters that cannot be specified
    // in path string arguments passed to members of the Path class.
    const InvalidPathChars: array[0..8] of Char = (‘\’, ‘/’, ‘:’, ‘*’, ‘?’, ‘”‘, ”, ‘|’);

    // A platform-specific separator character used to separate path strings in
    // environment variables.
    const PathSeparator: Char = SysUtils.PathSep;

    // Provides a platform-specific volume separator character.
    const VolumeSeparatorChar: Char = SysUtils.DriveDelim;

    // Changes the extension of a path string.
    class function ChangeExtension(const Path: TFileName; const Extension: string): TFileName; static;

    // Combines two path strings.
    class function Combine(Path1, Path2: string): string; overload; static;
    class function Combine(Paths: array of string): string; overload; static;

    // Returns the extension of the specified path string.
    class function GetExtension(const Path: TFileName): string; static;

    // Returns the file name and extension of the specified path string.
    class function GetFileName(const Path: TFileName): string; static;

    // Returns the directory information for the specified path string.
    class function GetDirectoryName(const Path: TFileName): string; static;

    // Returns the file name of the specified path string without the extension.
    // By given c:\abba\MyProfile.txt only MyProfle will be returned
    class function GetFileNameWithoutExtension(const Path: TFileName): string; static;

    // Returns the path of the current system’s temporary folder.
    class function GetTempPath: TFileName; static;

    // Creates a uniquely named, zero-byte temporary file on disk and returns
    // the full path of that file.
    class function GetTempFileName(const Prefix: string = ”): TFileName; static;

    // Gets the root directory information of the specified path.
    class function GetPathRoot(const Path: TFileName): string; static;

    class function SafeFileName(const FileTitle: string): string; static;

    class function SafeDirectoryName(const DirectoryName: string): string; static;

    // builds list of files of specified folder
    class procedure GetFiles(const Directory, Mask: string; const List: TStringList;
    const Recursive: Boolean = false); static;

    // Deletes all files within a directory which apply to the specified filter
    // Example DeleteFileList(‘c:\’, ‘*boot*.log’);
    // Returns the number of deleted files
    class function DeleteFileList(const Directory, Filter: string;
    const Recursive: Boolean = false): Integer; static;

    class function SafeCreateDir(const Dir: string): string; static;

    class function MinimizeName(Wnd: HWND; const Filename: string): string; static;

  • […] Появились классы для работы с файлами и папками. Наконец-то. Читаем у Malcolm Groves. […]

  • Just read this post now. Thanks for the examples Malcolm, this certainly makes file and directory access up to date.

    It’s interesting that TDirectory etc are declared as records – using the new richness that D2009 provides with record methods etc. What is the thinking behind using a record structure rtaher than a class?

    BTW, The Embarcadero QA team have let the IOUtils unit slip through without a copyright header. What’s with that – are the headers updated manually ?

  • This example and/or functionality totally sucks.

    It does not process the files in the main directory.

  • > This example and/or functionality totally sucks.

    Thanks for your constructive and encouraging comment, Skybuck. A real pleasure to have you reading my blog.

    > It does not process the files in the main directory.

    Brilliantly observed. Quite the detective, no? That’d be that part in the text where I wrote:

    “in this example we’ve subtly altered the behaviour a little, because the files in the directory we specify are not checked anymore.”

  • I just have tried to play with the new unit in Delphi 2010 IOUtils.pas, and found they put all the methods inside Records (TFile, TPath, TDirectory) as class functions and procedure.
    I am wondering: is there any benefit of doing that inside records instead of classes? How would it affect regarding memory consuming or performance improvement?

  • Thanks Malcolm for implementing almost all of my recommendations. Maybe just a considence but I managed to deprecate almost all of my unt_Path, unt_Directory, unt_File methods.

    What I still miss is:

    class function TDirectory.IsReservedName(DirectoryName: string): Boolean;
    begin
    DirectoryName := LowerCase(DirectoryName);
    if (DirectoryName = ‘con’) or
    (DirectoryName = ‘aux’) or
    (DirectoryName = ‘prn’) or
    (DirectoryName = ‘nul’) or
    (DirectoryName = ‘lpt0’) or
    (DirectoryName = ‘lpt1’) or
    (DirectoryName = ‘lpt2’) or
    (DirectoryName = ‘lpt3’) or
    (DirectoryName = ‘lpt4’) or
    (DirectoryName = ‘lpt5’) or
    (DirectoryName = ‘lpt6’) or
    (DirectoryName = ‘lpt7’) or
    (DirectoryName = ‘lpt8’) or
    (DirectoryName = ‘lpt9’) or
    (DirectoryName = ‘com0’) or
    (DirectoryName = ‘com1’) or
    (DirectoryName = ‘com2’) or
    (DirectoryName = ‘com3’) or
    (DirectoryName = ‘com4’) or
    (DirectoryName = ‘com5’) or
    (DirectoryName = ‘com6’) or
    (DirectoryName = ‘com7’) or
    (DirectoryName = ‘com8’) or
    (DirectoryName = ‘com9’) then
    Result := True
    else
    Result := False;
    end;

    2. class function GetTempFileName(const Prefix: string = ‘tmp’): TFileName; static;
    GetTempFileName to have a Prefix parameter

    3. Add “Safety Belt” file name creation
    ///
    /// Returns file name which is valid. If invalid characters are supplied in the fileName
    /// parameter they are removed
    ///
    // Url : http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=464878&SiteID=1
    class function SafeFileName(const FileTitle: string): string; static;

    4. Minimize Name

    class function TPath.MinimizeName(Wnd: HWND; const Filename: string): string;
    { func to shorten the long path name with an ellipses ‘…’ to fit }
    { in whatever control is passed to the Wnd parameter. }
    { Usage: Panel1.Caption := MinimizePathName(Panel1.Handle, DirectoryOutline1.Directory) }
    { This will shorten the path if necessary to fit in Panel1. }
    procedure CutFirstDirectory(var S: string);
    var
    Root: Boolean;
    P: Integer;
    begin
    if S = ‘\’ then
    S := ”
    else
    begin
    if S[1] = ‘\’ then
    begin
    Root := True;
    Delete(S, 1, 1);
    end
    else
    Root := False;
    if S[1] = ‘.’ then
    Delete(S, 1, 4);
    P := Pos(‘\’,S);
    if P 0 then
    begin
    Delete(S, 1, P);
    S := ‘…\’ + S;
    end
    else
    S := ”;
    if Root then
    S := ‘\’ + S;
    end;
    end;

    function GetTextWidth(DC: HDC; const Text: String): Integer;
    var
    Extent: TSize;
    begin
    if GetTextExtentPoint(DC, PWideChar(Text), Length(Text), Extent) then
    Result := Extent.cX
    else
    Result := 0;
    end;

    var
    Drive,
    Dir,
    Name: string;
    R: TRect;
    DC: HDC;
    MaxLen: integer;
    OldFont, Font: HFONT;
    begin
    Result := FileName;

    if Wnd = 0 then
    Exit;

    DC := GetDC(Wnd);
    if DC = 0 then
    Exit;

    Font := HFONT(SendMessage(Wnd, WM_GETFONT, 0, 0));
    OldFont := SelectObject(DC, Font);
    try
    GetWindowRect(Wnd, R);
    MaxLen := R.Right – R.Left;

    Dir := ExtractFilePath(Result);
    Name := ExtractFileName(Result);

    if (Length(Dir) >= 2) and (Dir[2] = ‘:’) then
    begin
    Drive := Copy(Dir, 1, 2);
    Delete(Dir, 1, 2);
    end
    else
    Drive := ”;

    while ((Dir ”) or (Drive ”)) and (GetTextWidth(DC, Result) > MaxLen) do
    begin
    if Dir = ‘\…\’ then
    begin
    Drive := ”;
    Dir := ‘…\’;
    end
    else
    if Dir = ” then
    Drive := ”
    else
    CutFirstDirectory(Dir);
    Result := Drive + Dir + Name;
    end;
    finally
    SelectObject(DC, OldFont);
    ReleaseDC(Wnd, DC);
    end;
    end;

    I hope some of those suggestions can make their way in XE3?

  • […] bisher die ganzen Datei-Operationen(auf Existenz Prüfen, Suchen, Löschen, etc.) noch prozedural. IOUtils.pas wird das wohl ändern. Ein Schritt in die richtige Richtung, wie ich […]

Join the Discussion

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>