Finding files, part 2

by Mark G. Wiseman

In part 1 of this series of articles, I introduced two classes, TFFData and TFindFile, that can be used to find files on a hard drive. Ultimately I will use these classes to develop a utility program that will delete the unnecessary files left by a C++Builder project. I would like to continue my discussion of the main class, TFindFile, in this article. I will also talk a little about other uses for TFindFile and another approach you might consider for finding files.

 

An updated TFindFile

Since writing part 1, I made a small change to the source code for TFindFile. I added a simple inline function, IsRunning(). This function simply returns true if TFindFile is currently searching for files and false if it is not. The updated code for TFindFile is in Listings A and B.

As you may recall, I suggested that TFindFiles might be rewritten to run in a separate thread. Although, I haven’t done this, I do call the ProcessMessages() function of TApplication several places in the code for TFindFile. ProcessMessages() could be viewed as a simple form of multithreading. In either case I found it useful to test if TFindFile was actually running. What if a user of our utility program clicked on the program window’s close button? The code segment below shows how we might use IsRunning().

// findfile is an instance of TFindFile

void __fastcall TMainForm::FormCloseQuery(
  TObject *Sender, bool &CanClose)
{
  if (findFile.IsRunning() == false) return;

  if (Application->MessageBox(
    "Stop search and exit?",
    "Find Files", MB_YESNO) == IDYES)
      findFile.Stop();
  else
    CanClose = false;
}

Searching subfolders

The Find() function in the TFindFile class searches for a file pattern that is passed to it in a String argument. If the SearchSubfolders property is set to true, Find() will also search all subfolders.

The SearchSubfolders property is the public interface for the private bool variable searchSubfolders. After searching the starting folder for files, Find() tests searchSubfolders and if it is true, Find() will continue searching into any subfolders of the starting folder.

The first thing Find() does when searching subfolders, is to create a TFFStack object named dirStack. You will recall from part 1 that using TFFStack allows us to search through subfolders without using recursion.

Find() then enters a while loop that will run as long as stop is false. Within the while loop, Find() calls the private function FindDirs(), passing in the current folder and a reference to dirStack. The FindDirs() function uses the Windows API to find all folders in the current folder and pushes each one of these folders onto the dirStack.

Find() then tests dirStack to see if it is empty. If dirStack is empty, then there are no more subfolders to search and Find() will break from the while loop, set stop equal to true and return.

If dirStack is not empty, Find() pops the next subfolder off of dirStack, sets the current folder to that subfolder and then calls FindFiles() to continue the search.

 

Skipping folders

I have included a property, OnSearchFolder in TFindFile. This property hold a closure, a pointer to a function of the typedef TSearchFolder. Here is that typedef:

typedef void __fastcall 
  (__closure  *TSearchFolder)
  (TFindFile *Sender, String folderName,
  bool &skip);

If a function has been assigned to OnSearchFolder, the Find() function will call it before it searches a folder. When Find() calls the function, it sets the skip variable equal to false. When the function returns, Find() checks skip and if skip is true, Find() will not search the folder.

The ability to skip a folder can be very useful. Remember, we are going to end up with a program that deletes files from C++Builder project folders. On my system, most C++Builder projects are stored in subfolders of “c:\\Projects”. If my current project is very large, I may not want to delete the files for that project each night when I run the utility. If the current project resides in a subfolder named “Current”, I could use this code to skip that subfolder.

void __fastcall 
TMainForm::OnSearchFolder(
  TFindFile *Sender, 
  String folderName, bool &skip)
{
  if (folderName == 
      "c:\\Projects\\Current\\") 
    skip = true;
}

I can also use the OnSearchFolder closure function to display the name of the current folder being searched.

void __fastcall 
TMainForm::OnSearchFolder(
  TFindFile *Sender, 
  String folderName, bool &skip)
{
  StatusBar->Panels->Items[0]->Text = 
    folderName;
}

Finding files

So, what happens when the Find() function of TFindFile finds a file? I have included another property, OnFileFound, that holds a closure for a function of the typedef TFileFound. Here is that typedef:

typedef void __fastcall 
  (__closure *TFileFound)
  (TFindFile *Sender, String fileName,
  String foldername, TFFData data);

This is where the rubber meets the road. When the Find() function finds a file that matches the pattern we have specified, it will call the function held in the OnFileFound property, passing the file’s name and the TFFData object associated with the file. We can have this function do anything we want with this file. For instance, lets delete each file that is found.

void __fastcall TMainForm::OnFileFound(
  TFindFile *Sender, String fileName, 
  String folderName, TFFData data)
{
  if (data.IsFolder()) return;

  String filePath = 
    folderName + fileName;
  DeleteFile(filePath);
}

Notice two things about how TFindFile works: first, the file that is found may actually be a folder that matches the file pattern specified in the call to Find(). You need to test for this.

Second, a copy of the TFFData associated with the files is passed in as an argument. This can be very useful. For example, using information contained in TFFData, we could easily modify our function to delete only those files of size 0 bytes:

void __fastcall TMainForm::OnFileFound(
  TFindFile *Sender, String fileName, 
  String folderName, TFFData data)
{
  if (data.IsFolder()) return;

  String filePath = 
    folderName + fileName;
  if (data.GetSize() == 0) 
    DeleteFile(filePath);
}

In every program where you use TFindFile, you will almost certainly have to write a function of typdef TFileFound and assign it to the OnFileFound property of TFindFile. Otherwise, there is not really a good reason to include TFindFile.

 

Other uses

In this series of articles, I will use TFindFile for one specific purpose – deleting certain C++Builder project files. However, I designed TFindFile to be used in any kind of program that needs to search a drive for files.

As hinted at above you could use TFindFile to scan an entire hard drive to find all files of size zero, list the files for the user and allow them to decide which files should be deleted.

Along with some file compression code, you could use TFindFile to write a custom backup program. What I am trying to say, is there are lots of ways you can use TFindFile. Don’t just think of it as part of our utility program.

 

Possible changes

There are modifications you might want to make to TFindFile. As I’ve already mentioned, you might want to rewrite it to use a separate thread.

Also, instead of using the Windows API functions for finding files, you could use the Windows Shell functions. This approach might offer faster searching and you might be able to search for things other than files (e.g. computers and printers).

There is one modification that I have seriously consider making to TfindFile. I would like for TFindFile to accept wild cards for drives, both on the local machine and on the network. So, instead of calling Find() for each drive on a computer or network, you would only have to call Find() once. For example:

// Current implementation of TFindFile

findFile.Find("c:\\*.*");
findFile.Find("d:\\*.*");
findFile.Find("e:\\*.*");

// Wouldn't this be nice!
// Search c, d and e drives
FindFile.Find("?:\\*.*);

Making a modification to allow drive wild cards on a single computer should be easy. Wild cards for network drives would be harder, but still do-able.

 

Conclusion

This concludes my discussion of TFindFile. In my next article, I am going to show you the code for actually deleting C++Builder project files. The approach I use for finding the files, using TFindFile of course, may be a little surprising. As you will see, I only call the Find() functions with this pattern, “*.*”. Read my next article to find out why.

 

Listing A: FindFile.h

#ifndef FindFileH
#define FindFileH

#include "FFData.h"

class TFFStack;

typedef void __fastcall (__closure *TFileFound)
  (TFindFile *Sender, String fileName,
   String foldername, TFFData data);
typedef void __fastcall (__closure *TSearchFolder)(TFindFile *Sender, 
  String folderName, bool &skip);

class TFindFile
{
  public:
    TFindFile();

    void Find(String filePattern);
    void Stop();
    bool IsRunning();

    __property bool SearchSubfolders = 
     {read = searchSubfolders, 
       write = searchSubfolders};
    __property TFileFound OnFileFound = 
     {read = FOnFileFound, write = FOnFileFound};
    __property TSearchFolder OnSearchFolder = 
     {read = FOnSearchFolder, 
       write = FOnSearchFolder};

  private:
    void FindFiles(String filePattern);
    void FindDirs(String baseDir, TFFStack &stack);

    TFileFound FOnFileFound;
    TSearchFolder FOnSearchFolder;

    bool stop;
    bool searchSubfolders;
};

inline TFindFile::TFindFile()
{
  FOnFileFound = 0;
  FOnSearchFolder = 0;

  stop = true;
  searchSubfolders = false;
}

inline void TFindFile::Stop()
{
  stop = true;
}

inline bool TFindFile::IsRunning()
{
  return(stop == false);
}


#endif   // FindFilesH

 

Listing B: FindFile.cpp.

 

#include <vcl.h>
#pragma hdrstop

#include "FindFile.h"


class TFFStack
{
  public:
    TFFStack();
    ~TFFStack();

    void Push(AnsiString name);
    AnsiString Pop();

    void Empty();

    bool Contains(String name);
    bool IsEmpty();

  private:
    TStringList *list;
};

inline TFFStack::TFFStack()
{
  list = new TStringList;
  list->Sorted = true;
  list->Duplicates = dupIgnore;
}

inline TFFStack::~TFFStack()
{
  delete list;
}

inline void TFFStack::Push(AnsiString name)
{
  list->Add(name);
}

inline AnsiString TFFStack::Pop()
{
  AnsiString temp = list->Strings[0];
  list->Delete(0);
  return(temp);
}

inline void TFFStack::Empty()
{
  list->Clear();
}

inline bool TFFStack::Contains(String name)
{
  int index;
  return(list->Find(name, index));
}

inline bool TFFStack::IsEmpty()
{
  return(list->Count == 0);
}


// -------------------------------------------

void TFindFile::Find(String filePattern)
{
  stop = false;

  String curDir = ExtractFilePath(
    ExpandFileName(filePattern));
  String fileName = ExtractFileName(filePattern);

  bool skip = false;
  if (FOnSearchFolder) 
    FOnSearchFolder(this, curDir, skip);
  if (FOnFileFound && skip == false) 
    FindFiles(curDir + fileName);

  if (searchSubfolders)
  {
    TFFStack dirStack;

    while (stop == false)
    {
      Application->ProcessMessages();

      FindDirs(curDir, dirStack);
      if (dirStack.IsEmpty()) break;

      curDir = dirStack.Pop();

      skip = false;
      if (FOnSearchFolder) 
        FOnSearchFolder(this, curDir, skip);
      if (skip) continue;

      if (FOnFileFound) FindFiles(curDir + fileName);
    }
  }

  stop = true;
}

void TFindFile::FindDirs(
  String baseDir, TFFStack &stack)
{
  TFFData ffData;

  String dirPattern = baseDir + "*.*";

  HANDLE handle = FindFirstFile(
    dirPattern.c_str(), &ffData.data);
  if (handle != INVALID_HANDLE_VALUE)
  {
    bool found = true;

    while (found == true && stop == false)
    {
      Application->ProcessMessages();

      if (ffData.IsFolder() && 
          ffData.GetName() != "." && 
          ffData.GetName() != "..")
        stack.Push(
          baseDir + ffData.GetName() + "\\");

      found = (FindNextFile(handle, &ffData.data) != 0);
    }

    FindClose(handle);
  }
}

void TFindFile::FindFiles(String filePattern)
{
  TFFData ffData;

  HANDLE handle = FindFirstFile(
    filePattern.c_str(), &ffData.data);
  if (handle != INVALID_HANDLE_VALUE)
  {
    bool found = true;

    while (found == true && stop == false)
    {
       Application->ProcessMessages();

       if (FOnFileFound && 
           ffData.GetName() != "." && 
           ffData.GetName() != "..")
         FOnFileFound(this, ffData.GetName(),
           ExtractFilePath(filePattern), ffData);
       found = (FindNextFile(
         handle, &ffData.data) != 0);
    }

    FindClose(handle);
  }
}


#pragma package(smart_init)