TOCBACKFORWARD

Charlie Calvert's C++ Builder Unleashed

- 24 -

Creating Non-Visual Components

Overview

In this chapter, you will learn how to build a non-visual component. In particular, I will emphasize the following topics:

In particular, you will look at one reusable non-visual component called TFindDirs, which is used in a program that will iterate through a series of directories and will archive the names of the files found there into a database. You could, for example, use the program to iterate over all the files on several zip discs of CD-ROMs. You would then have one database that could be searched when you're looking for files that might be located on any of a number of different discs.

Why Create Non-Visual Components?

The program shown in this chapter uses the TFindDirs component to illustrate the concept of component reuse. Write the TFindDirs component once, and you can reuse it in multiple programs. The big bonus here is that TFindDirs is a component and thereby makes a relatively complex task simple enough that you can perform it by just dropping an object onto a form and plugging it into your program.

This short chapter is presented primarily to promote the idea of turning objects that you might not usually think of as components into components. You can easily see why an edit control makes a good component, but the fact that an object used to iterate through directories would make a good component is less obvious. In fact, I first turned this object into a component on a whim. Once I had it around, however, I found that I could plug it into all kinds of applications to aid with one task or another.

Here are some of my applications that use this component:

My point here is that writing these utilities became easy after I had the TFindDirs component in place. The core functionality for each of these programs was easy to implement because it was based on a component. As you will see, in just a few seconds you can create a program that uses the component. After you have that much functionality in place, you can easily add more features.

When Should Non-Visual Objects Become Components?

Whether you should turn a particular object into a component is not always clear. For example, the TStringList object has no related component and cannot be manipulated through visual tools. The question then becomes "Why have I taken the TFindDirs object and placed it on the Component Palette?"

As you'll discover, the advantages of placing TFindDirs on the Component Palette are two-fold:

Creating a component also has the enormous advantage of forcing, or at least encouraging, programmers to design a simple interface for an object. After I have placed an object on the Component Palette, I always want to ensure that the user can hook it into his or her program in only a few short seconds. I am therefore strongly inclined to create a simple, easy-to-use interface. If I don't place the component on the Component Palette, then I find it easier to slip by with a complex interface that takes many lines of code for myself and others to utilize. To my mind, good components are not only bug free, but also very easy to use.

The SearchDirs Program

In this section, you will find the SearchDirs program, which can be used to iterate through the subdirectories on a hard drive looking for files with a particular name or file mask. For example, you could look for *.cpp or m*.cpp or ole2.hpp. This program will put the names of all the files that match the mask into a database, so you can search for the files later.

The SearchDirs program depends on the TFindDirs component, which ships with this book. To use this component, you must first load it onto the Component Palette, using the techniques described in the preceding few chapters. In general, all you have to do is choose Component | Install and then click the Add button. Browse the Utils subdirectory that ships with this book. There you will find the FindDirs2.cpp unit. Add it to CMPLIB32.CCL, and you are ready to build the SearchDirs program. As usual, you might want to run the BuildObjs project once before installing the component. The source for this program is shown in Listings 24.1 through 24.8. A screen shot of the program is shown in Figure 24.1. You need to make sure the CUnleashed alias is in place before running the program. This is a standard Paradox alias that points at the data directory off the root directory where the files from the CD that accompany this book are installed. See both the text right after the listings and also the readme file for more information about the alias.

FIGURE 24.1. A screen shot of the main form of the SearchDirs program.

The point here is that TFindDirs, like TTable and TQuery, is a non-visual component. TFindDirs completes your introduction to the basic component types by showing you how to build nonvisual components. You already know how to build visual components. After you understand how to build non-visual components, most of the power of BCB will be open to you. The TFindDirs component is also important because it shows you how to create custom event handlers.

Listing 24.1. The header for the main form for the SearchDirs program.

///////////////////////////////////////

// Main.h

// 
SearchDirs

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef MainH

#define MainH

#include <vcl\Classes.hpp>

#include <vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include 
<vcl\Menus.hpp>

#include <vcl\ComCtrls.hpp>

#include <vcl\DBGrids.hpp>

#include <vcl\Grids.hpp>

#include <vcl\DBCtrls.hpp>

#include <vcl\ExtCtrls.hpp>

#include "FindDirs2.h"



class TForm1 : public 
TForm

{

__published:

  TMainMenu *MainMenu1;

  TMenuItem *File1;

  TMenuItem *Counter1;

  TMenuItem *N1;

  TMenuItem *Exit1;

  TMenuItem *Run1;

  TDBGrid *FileNamesGrid;

  TMenuItem *Options1;

  TMenuItem *Delete1;

  TMenuItem 
*DisableGrids1;

  TPanel *Panel2;

  TEdit *Edit1;

  TEdit *Edit2;

  TMenuItem *N2;

  TMenuItem *PickArchive1;

  TLabel *Label1;

  TLabel *Label2;

  TStatusBar *StatusBar1;

  TDBGrid *DirNamesGrid;

  TFindDirs *FindDirs1;

  void __fastcall 
Button1Click(TObject *Sender);

  void __fastcall Exit1Click(TObject *Sender);

  void __fastcall Delete1Click(TObject *Sender);

  void __fastcall PickArchive1Click(TObject *Sender);

  void __fastcall OnFoundDir(AnsiString NewDir);

  void 
__fastcall FindDirs1FoundFile(AnsiString NewDir);

private:

  int FStartLevel;

  AnsiString FCurrentRoot;

  AnsiString FDiskName;

  void StartRun();

  void EndRun();

public:  

  virtual __fastcall TForm1(TComponent* Owner);

};



extern TForm1 
*Form1;



#endif



Listing 24.2. The main form for the SearchDirs program.

///////////////////////////////////////

// Main.cpp

// SearchDirs


// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#pragma hdrstop

#include "Main.h"

#include "DiskArchive.h"

#include "FindDirsDMod.h"

#pragma resource "*.dfm"

TForm1 *Form1;




__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

}



int GetLevel(AnsiString Source)

{

  BOOL Done = False;

  int i = 0;

  char S[500];



  strcpy(S, Source.c_str());



  strtok(S, "\\");

  while (!Done)

  {

    
if (strtok(NULL, "\\") == NULL)

      Done = True;

    else

      i += 1;

  }

  return i;

}



void TForm1::StartRun()

{

  DirNamesGrid->DataSource = NULL;

  FileNamesGrid->DataSource = NULL;

  Screen->Cursor = 
(Controls::TCursor)crHourGlass;

  FindDirs1->StartString = Edit1->Text;

  FDiskName = Edit2->Text;



  FStartLevel = GetLevel(FindDirs1->StartDir);

  DMod->DiskNamesTable->Insert();

  
DMod->DiskNamesTable->FieldByName("DiskName")->AsString = FDiskName;

  DMod->DiskNamesTable->Post();

}



void TForm1::EndRun()

{

  Screen->Cursor = (Controls::TCursor)crDefault;

  DirNamesGrid->DataSource = 
DMod->DirNamesSource;

  FileNamesGrid->DataSource = DMod->FileNamesSource;

}



void __fastcall TForm1::Button1Click(TObject *Sender)

{

  if (MessageBox((HWND)Handle, Edit1->Text.c_str(), "Make Run?", MB_YESNO) == ID_YES)

  
{

    StartRun();

    FindDirs1->Run();

    EndRun();

  }

}



void __fastcall TForm1::Exit1Click(TObject *Sender)

{

  Close();

}



void __fastcall TForm1::Delete1Click(TObject *Sender)

{

  if (MessageBox((HWND)Handle, 
"Delete", "Delete Dialog", MB_YESNO) == ID_YES)

    DMod->CascadingDelete();        

}



void __fastcall TForm1::PickArchive1Click(TObject *Sender)

{

  ArchiveForm->ShowModal();    

}



void __fastcall 
TForm1::OnFoundDir(AnsiString NewDir)

{

  int i;

  

  StatusBar1->SimpleText = NewDir;

  StatusBar1->Update();



  FCurrentRoot = NewDir;



  i = GetLevel(FCurrentRoot);

  DMod->DirNamesTable->Insert();

  
DMod->DirNamesTable->FieldByName("DirName")->AsString = NewDir;

  DMod->DirNamesTable->FieldByName("ALevel")->AsInteger = i;

  DMod->DirNamesTable->FieldByName("DiskCode")->AsInteger =

  
DMod->DiskNamesTable->FieldByName("Code")->AsInteger;

  DMod->DirNamesTable->Post();

}



void __fastcall TForm1::FindDirs1FoundFile(AnsiString NewDir)

{

  AnsiString Temp;

  

  if (FCurrentRoot.Length() == 0)

    Temp 
= FindDirs1->StartDir;

  else

    Temp = FCurrentRoot;



  DMod->FileNamesTable->Insert();

  DMod->FileNamesTable->FieldByName("Directory")->AsString = Temp;

  
DMod->FileNamesTable->FieldByName("FileName")->AsString =

    ExtractFileName(NewDir);

  DMod->FileNamesTable->Post();

}



Listing 24.3. The header for the programs data module.

///////////////////////////////////////

// FindDirsDMod.h

// SearchDirs

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef FindDirsDModH

#define FindDirsDModH

#include 
<vcl\Classes.hpp>

#include <vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\DB.hpp>

#include <vcl\DBTables.hpp>



class TDMod : public TDataModule

{

__published: 

  
TDatabase *FileData1;

  TTable *DirNamesTable;

  TTable *FileNamesTable;

  TTable *DiskNamesTable;

  TTable *DiskTypeTable;

  TDataSource *DirNamesSource;

  TDataSource *FileNamesSource;

  TDataSource *DiskNamesSource;

  TDataSource 
*DiskTypeSource;

  TIntegerField *DirNamesTableALEVEL;

  TStringField *DirNamesTableDIRNAME;

  TIntegerField *DirNamesTableDISKCODE;

  TIntegerField *FileNamesTableCODE;

  TStringField *FileNamesTableDIRECTORY;

  TStringField 
*FileNamesTableFILENAME;

  TIntegerField *FileNamesTableDIRCODE;

  TIntegerField *DirNamesTableCODE;

  TQuery *DeleteFileNames;

  TQuery *DeleteDirNames;

  TQuery *DeleteDiskNames;

  TAutoIncField *DiskTypeTableCode;

  TStringField 
*DiskTypeTableDiskType;

  TIntegerField *DiskTypeTableDiskTypeCode;

  TAutoIncField *DiskNamesTableCode;

  TStringField *DiskNamesTableDiskName;

  TIntegerField *DiskNamesTableType;

  void __fastcall DModCreate(TObject *Sender);

  

  

  

  


private:

public:

  virtual __fastcall TDMod(TComponent* Owner);

  void CascadingDelete(void);

};



extern TDMod *DMod;



#endif



Listing 24.4. The data module for the program.

///////////////////////////////////////

// FindDirsDMod.cpp

// SearchDirs

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#pragma hdrstop

#include 
"FindDirsDMod.h"

#pragma resource "*.dfm"



TDMod *DMod;



__fastcall TDMod::TDMod(TComponent* Owner)

        : TDataModule(Owner)

{

}



void __fastcall TDMod::DModCreate(TObject *Sender)

{

  DirNamesTable->Open();

  
FileNamesTable->Open();

  DiskNamesTable->Open();

  DiskTypeTable->Open();

}



void TDMod::CascadingDelete(void)

{

  DirNamesTable->First();

  DeleteFileNames->Prepare();

  while (!DirNamesTable->Eof)

  {

    
DeleteFileNames->ExecSQL();

    DirNamesTable->Delete();

  }

  FileNamesTable->Refresh();



  DirNamesTable->Refresh();



  int i = DiskNamesTableCode->Value;

  DeleteDiskNames->Params->Items[0]->AsInteger = i;

  
DeleteDiskNames->ExecSQL();

  DiskNamesTable->Refresh();

}

Listing 24.5. The header for the DiskArchive form.

///////////////////////////////////////

// DiskArchive.h

// SearchDirs

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef DiskArchiveH

#define DiskArchiveH

#include <vcl\Classes.hpp>


#include <vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\DBGrids.hpp>

#include <vcl\Grids.hpp>

#include <vcl\ExtCtrls.hpp>

#include <vcl\DBCtrls.hpp>

#include 
<vcl\Buttons.hpp>



class TArchiveForm : public TForm

{

__published: 

  TDBGrid *DBGrid1;

  TPanel *Panel1;

  TDBNavigator *DBNavigator1;

  TBitBtn *BitBtn1;

private:

public:

  virtual __fastcall TArchiveForm(TComponent* Owner);

};




extern TArchiveForm *ArchiveForm;



#endif



Listing 24.6. The DiskArchive form. This form is used only for displaying data. It has no custom code.

///////////////////////////////////////

// DiskArchive.cpp

// SearchDirs

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#pragma hdrstop

#include "DiskArchive.h"

#include 
"FindDirsDMod.h"

#pragma resource "*.dfm"



TArchiveForm *ArchiveForm;



__fastcall TArchiveForm::TArchiveForm(TComponent* Owner)

    : TForm(Owner)

{

}



Listing 24.7. The header for the TFindDirs components.

///////////////////////////////////////

// FindDirs2.h

// SearchDirs

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef 
FindDirs2H

#define FindDirs2H

#ifndef ComCtrlsHPP

#include <vcl\ComCtrls.hpp>

#endif



struct TDirInfo

{

  TSearchRec SearchRec;

  AnsiString CurDirectory;

};



class TDirStack : public TList

{

public:

  TDirStack(): TList() {}; 

  
void Push(TDirInfo *DirInfo);

  TDirInfo *Pop();

};



typedef void __fastcall (__closure *TFoundDirEvent)(AnsiString NewDir);



class TFindDirs : public TComponent

{

private:

  #ifdef DEBUG_FIND_DIRS

  FILE *fp;

  #endif

  AnsiString 
FStartString;     // Unchanged string passed in by user.

  AnsiString FFileExt;

  AnsiString FStartDir;        // The directory where the search starts

  AnsiString FCurDirectory;    // the current directory

  AnsiString FFileMask;        // The 
file mask of files to search for

  AnsiString FSearchString;    // Combine last three into a search string

  TDirStack *FDirStack;        // Stack of directories in the current dir

  TFoundDirEvent FOnFoundDir;

  TFoundDirEvent FOnFoundFile;

  
void GetAllFiles(AnsiString *StartDir);

  void FoundAFile(TSearchRec *FileData);

  void FoundADir(TSearchRec *FileData);

  void __fastcall Initialize(void);

  void SetupSearchString();

  void GetNextDirectory();

  BOOL SetupFirstDirectory();


protected:

  __fastcall virtual ~TFindDirs();

  virtual void ProcessFile(TSearchRec FileData, AnsiString FileName);

  virtual void ProcessDir(TSearchRec FileData, AnsiString DirName);

  virtual void __fastcall SetStartString(AnsiString 
AStartString);

public:

  virtual __fastcall TFindDirs(TComponent *AOwner)

    : TComponent(AOwner) { FDirStack = NULL; FOnFoundDir = NULL; }

  virtual __fastcall TFindDirs(TComponent *AOwner, AnsiString AStartString);

  virtual void Run(void);

  
__property AnsiString StartDir = {read = FStartDir};

  __property AnsiString CurDirectory = {read = FCurDirectory};

__published:

  __property AnsiString StartString={read=FStartString, write=SetStartString};

  __property TFoundDirEvent 
OnFoundFile={read=FOnFoundFile, write=FOnFoundFile};

  __property TFoundDirEvent OnFoundDirf={read=FOnFoundDir, write=FOnFoundDir};

};



class TFindDirsList : public TFindDirs

{

private:

  TStringList *FFileList;

  TStringList *FDirList;


protected:

  __fastcall virtual ~TFindDirsList();

  virtual void ProcessFile(TSearchRec FileData, AnsiString FileName);

  virtual void ProcessDir(TSearchRec FileData, AnsiString DirName);

public:

  virtual __fastcall TFindDirsList(TComponent 
*AOwner): TFindDirs(AOwner) {}

  virtual __fastcall TFindDirsList(TComponent *AOwner, AnsiString AStartString);

__published:

  __property TStringList *FileList = {read = FFileList, nodefault};

  __property TStringList *DirList = {read = FDirList, 
nodefault};

};



#endif



Listing 24.8. The main source file for the TFindDirs component.

///////////////////////////////////////

// 
FindDirs2.cpp

// SearchDirs

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#include <vcl\ComCtrls.hpp>

#pragma hdrstop

#include "FindDirs2.h"





// -- TDirStack ----------------



void 
TDirStack::Push(TDirInfo *DirInfo)

{

  Add(DirInfo);

}



TDirInfo *TDirStack::Pop()

{

  void *Temp = Items[0];

  Delete(0);

  return (TDirInfo *)Temp;

}



// -- TFindDirs ----------------



__fastcall TFindDirs::TFindDirs(TComponent 
*AOwner, AnsiString AStartString)

: TComponent(AOwner)

{

  SetStartString(AStartString); // Don't set data store directly!

  FDirStack = NULL;

  FOnFoundDir = NULL;  

}



void __fastcall TFindDirs::SetStartString(AnsiString AStartString)

{

  
FStartString = AStartString; 

  FStartDir = ExtractFilePath(FStartString);

  FFileExt = ExtractFileExt(FStartString);

}



void __fastcall TFindDirs::Initialize(void)

{

  #ifdef DEBUG_FIND_DIRS

  if ((fp = fopen("c:\searchdirs.txt", 
"w+")) == NULL)

  {

    ShowMessage("Can't open debug file");

  }

  #endif

  if (FDirStack)

    delete FDirStack;

  FDirStack = new TDirStack;

  FCurDirectory = "";

  FFileMask = "*.*"; 

  #ifdef 
DEBUG_FIND_DIRS

  fprintf(fp, "%s %s %s \n", FStartDir.c_str(), FFileMask.c_str(), FFileExt.c_str());

  #endif

}



__fastcall TFindDirs::~TFindDirs()

{

  #ifdef DEBUG_FIND_DIRS

  fclose(fp);

  #endif

}



void 
TFindDirs::ProcessFile(TSearchRec FileData, AnsiString FileName)

{

  if (FOnFoundFile != NULL)

    FOnFoundFile(FileName);

  #ifdef DEBUG_FIND_DIRS

  fprintf(fp, "File found: %s\n", FileName);

  #endif

}



void 
TFindDirs::ProcessDir(TSearchRec FileData, AnsiString DirName)

{

  if (FOnFoundDir != NULL)

    FOnFoundDir(DirName);

  #ifdef DEBUG_FIND_DIRS

  fprintf(fp, "Dir found: %s\n", DirName);

  #endif

}



void 
TFindDirs::FoundADir(TSearchRec *FileData)

{

  AnsiString FullName;

  #ifdef DEBUG_FIND_DIRS

  fprintf(fp, "Dir found: %s\n", FileData->Name);

  #endif

  if ((FileData->Name != ".") &&

     (FileData->Name 
!= ".."))

  {

    TDirInfo *DirInfo = new TDirInfo;

    DirInfo->CurDirectory = AnsiString(FCurDirectory + FileData->Name + "\\");

    DirInfo->SearchRec = *FileData;

    #ifdef DEBUG_FIND_DIRS

    fprintf(fp, 
"DirInfo: %s\n", DirInfo->SearchRec.Name);

    fflush(fp);

    #endif

    FDirStack->Push(DirInfo);

  }

}



///////////////////////////////////////

// FoundAFile

///////////////////////////////////////

void 
TFindDirs::FoundAFile(TSearchRec *FileData)

{

  AnsiString FullName;



  if ((FFileExt == ".*") ||

      (UpperCase(ExtractFileExt(FileData->Name)) == UpperCase(FFileExt)))

  {

    FullName = FStartDir + FCurDirectory + 
FileData->Name;

    ProcessFile(*FileData, FullName);

  }

}



///////////////////////////////////////

// GetAllFiles

///////////////////////////////////////

void TFindDirs::GetAllFiles(AnsiString *StartDir)

{

  TSearchRec FileData;

  int 
Info;



  Info = FindFirst(StartDir->c_str(), faDirectory, FileData);

  while (Info == 0)

  {

    if (FileData.Attr == faDirectory)

      FoundADir(&FileData);

    else

      FoundAFile(&FileData);

    Info = FindNext(FileData);

  
}

  FindClose(&FileData.FindData);

}



///////////////////////////////////////

// SetupSearchString

///////////////////////////////////////

void TFindDirs::SetupSearchString()

{

  FSearchString = FStartDir + FCurDirectory + FFileMask;

  
#ifdef DEBUG_FIND_DIRS

  fprintf(fp, "FSearchString: %s \n", FSearchString);

  #endif

}



///////////////////////////////////////

// GetNextDirectory

///////////////////////////////////////

void TFindDirs::GetNextDirectory()

{

  
TDirInfo *FDirInfo = FDirStack->Pop();

  FCurDirectory = FDirInfo->CurDirectory;

  #ifdef DEBUG_FIND_DIRS

  fprintf(fp, "Next Directory: %s\n", FCurDirectory);

  fflush(fp);

  #endif

  ProcessDir(FDirInfo->SearchRec, FStartDir 
+ FCurDirectory);

  delete FDirInfo;

}



BOOL TFindDirs::SetupFirstDirectory()

{

  TSearchRec FileData;

  AnsiString SearchStr = FStartDir + FFileMask;



  int Info = FindFirst(SearchStr.c_str(), faDirectory, FileData);

  
FindClose(&FileData.FindData);

  if (Info == 0)

  {

    TDirInfo *DirInfo = new TDirInfo;

    DirInfo->CurDirectory = FCurDirectory;

    FileData.Name = FStartDir;

    DirInfo->SearchRec = FileData;

    FDirStack->Push(DirInfo);

    
return TRUE;

  }

  else

    return FALSE;

}



///////////////////////////////////////

// Run: FindFilesAndDirs

///////////////////////////////////////

void TFindDirs::Run(void)

{

  BOOL FDone = False;

  BOOL FirstTime = TRUE;



  
Initialize();

    

  if (!SetupFirstDirectory())

  {

    ShowMessage("Invalid Search String");

    return;

  }



  while (!FDone)

  {

    SetupSearchString();

    if (!FirstTime)

      GetAllFiles(&FSearchString);

    if 
(FDirStack->Count > 0)

      GetNextDirectory();

    else

      FDone = True;

    FirstTime = FALSE;

  }

  FDirStack->Free();

  FDirStack = NULL;

}



///////////////////////////////////////

// TFindDirsList //////////////////////


///////////////////////////////////////



__fastcall TFindDirsList::TFindDirsList(TComponent *AOwner,

  AnsiString AStartDir): TFindDirs(AOwner, AStartDir)

{

  FFileList = new TStringList;

  FFileList->Sorted = True;

  FDirList = new 
TStringList;

  FDirList->Sorted = True;

}



__fastcall TFindDirsList::~TFindDirsList()

{

  FFileList->Free();

  FDirList->Free();

}



void TFindDirsList::ProcessFile(TSearchRec FileData, AnsiString FileName)

{

  
FFileList->Add(FileName);

}



void TFindDirsList::ProcessDir(TSearchRec FileData, AnsiString DirName)

{

  FDirList->Add(DirName);

}



namespace Finddirs2

{

  void __fastcall Register()

  {

    TComponentClass classes[2] = 
{__classid(TFindDirs),

      __classid(TFindDirsList)};

    RegisterComponents("Unleash", classes, 1);

  }

}

The database aspects of this program are important. You will find the files used by the program in the Data directory on the CD that ships with this book. As I explain in the readme file that accompanies the CD, you should set up an alias called CUnleashed that points to these files. Needless to say, you should recreate the data directory on your hard drive, and should not use the read-only directory found on the CD, but should make sure they've been copied onto your hard drive. The DatabaseName for the TDatabase object used in my version of the program contains the string FileData, so you might get an error about that alias if you try to run the program. However, you do not want to try to fix the FileData alias, rather the one called CUnleashed. The data module for the program is shown in Figure 24.2.

To use the program, first point it to a subdirectory on your hard disk. Then type in a file mask in the edit control at the top of the form. For example, you might type in c:\temp\*.cpp or simply c:\temp\*.*. Be sure to type in the file mask. It would cause an error if you typed I:\ instead of I:\*.*. (In general, the program is not robust enough to check for many user errors.) When you click the button at the bottom of the program, the code iterates through all the directories beneath the one you pointed to and finds all the files that have the extension you designated. The program then places these files in a list database.

FIGURE 24.2. The data module for the SearchDirs program.


NOTE: I tested the TFindDirs component fairly extensively. For instance, I aimed it at the root of my C drive, which contains 1.12GB of space, with all but about 100MB used. The program ran fine against the thousands of files on that drive. I also aimed the component at nested directories containing long filenames, and again it handled the challenge without incident.

One problem I have had with the component involves sharing violations. If the SearchDirs program is iterating through a series of files, and one of which is locked by the file system, then the TFindDirs component will probably raise a rather unsightly access violation. Check the CD and my Web site to see if I have come up with a fix for this problem. In the meantime, you should make sure other programs are closed before running the SearchDirs program, or be sure to aim the program at drives that do not have open files on them.

Note that running the SearchDirs program against huge drives will create truly monolithic Paradox files containing lists of the files found during the search. For instance, after running against my C drive, the main DB file and its index were both more than 9MB in size. I have not tested the program to see what would happen if I ran out of disk space for the DB file during a run of the program.

To use the FindDirs component, you need do nothing more than assign it a string containing the file mask you want to use. You can do so via a property in the Object Inspector; you can insert the information at runtime:

  if (MessageBox((HWND)Handle, Edit1->Text.c_str(), "Make Run?", MB_YESNO) == ID_YES)

  {



    
FindDirs1->StartString = Edit1->Text;

    FindDirs1->Run();

  }

That's all you have to do to use the component, other than respond to events when files or directories are found. You can set up these event handlers automatically, as described in the next section.

Iterating Through Directories with TFindDirs

The SearchDirs program uses the TFindDirs component to iterate through directories. The TFindDirs component sends events to your program whenever a new directory or a new file is found. The events include the name of the new directory or file, as well as information about the size and date of the files the component finds. You can respond to these events in any way you want. For example, this program stores the names in a Paradox file.


NOTE: The SearchDirs program tends to create huge database files fairly quickly. The main problem here is that I need to store long filenames, which means I need to set aside large fields in the table. This problem is severe enough that I am going to eventually need to come up with some kind of custom solution to storing these strings. For now, however, I am just living with some very big DB files on my hard drive.

One possible solution to this problem would be to save all the information from a directory in a TStringList and then save the TStringList as a single string, which is one of the capabilities of this object. I could then save the whole string in a single blob field, which would make a better use of space. When I want to display the directory to a user, I could ask the TStringList to read the string in again and store it in a TMemoryStream.

The SearchDirs program responds as follows when a file with the proper extension is found:

void __fastcall TForm1::FindDirs1FoundFile(AnsiString NewDir)

{

  AnsiString Temp;



  if (FCurrentRoot.Length() == 0)

    Temp = FindDirs1->StartDir;

  else

    Temp = FCurrentRoot;

  
DMod->FileNamesTable->Insert();

  DMod->FileNamesTable->FieldByName("Directory")->AsString = Temp;

  DMod->FileNamesTable->FieldByName("FileName")->AsString =

    ExtractFileName(NewDir);

  
DMod->FileNamesTable->Post();

}

As you can see, the code does nothing more than shove the filename in a table.

To set up this method, you merely have to click the TFindDirs OnFoundFile event in the Object Inspector. The OnFoundFile and OnFoundDir events of TFindDirs are of type TFoundDirEvent:

typedef void __fastcall (__closure *TFoundDirEvent)(AnsiString NewDir);

I created two private events for TFindDirs that are of this type:

TFoundDirEvent FOnFoundDir;

TFoundDirEvent FOnFoundFile;

I then make these events into published properties that can be seen in the Object Inspector:

__property TFoundDirEvent OnFoundFile={read=FOnFoundFile, write=FOnFoundFile};

__property TFoundDirEvent OnFoundDirf={read=FOnFoundDir, 
write=FOnFoundDir};

If you're unclear about what is happening here, study the code in FindDirs2.h, or turn to the section about creating and using events covered in depth in Chapter 4, "Events."

If you have events like this one set up, then you need merely to click them in the Object Inspector, and the outline for your code will be created for you automatically. The following, for example, is the code BCB will produce if you click the OnFoundDir event:

void __fastcall TForm1::FindDirs1FoundFile(AnsiString NewDir)

{

}

The great thing about events is that they not only save you time when you're typing, but they also help to show how to use the component. After you see a method like FindDirs1FoundFile, you don't have to worry about going to the online help to find out how to get the directories found by the component! What you're supposed to do is obvious.

The following code in FoundDirs2.cpp calls the event handlers:

void TFindDirs::ProcessDir(TSearchRec FileData, AnsiString DirName)

{

  if (FOnFoundDir != NULL)

    FOnFoundDir(DirName);

  #ifdef DEBUG_FIND_DIRS

  fprintf(fp, 
"Dir found: %s\n", DirName);

  #endif

}

This code checks to see whether the FOnFoundDir event is set to NULL. If it is not, the event is called.


NOTE: Notice that the code for the ProcessDir method uses conditional compilation to leave you the option of writing debug output to a file. I used this code when I was creating the unit. My goal was to find a way to write out a list of the directories and files found during a run of the program. I could then study the list to make sure my algorithm was working correctly.

Layering Your Objects

TFindDirs has a descendant object called TFindDirsList. Part of the built-in functionality of the TFindDirsList unit is to maintain lists of the files it finds. After you finish searching all the directories, the list is ready for you to do with as you want. This list is kept in a TStringList object, so you can just assign it to the Items property in a list box, as shown in this code excerpt:

ListBox1->Items = FileIterator1->FileList;

This idea of layering your components so that you can create different objects, descending from different parents, under different circumstances, is key to object-oriented design. You don't want to push too much functionality up too high in the object hierarchy; otherwise, you will be forced to rewrite the object to get access to a subset of its functionality. For example, if the BCB developers had not created a TDataSet component but had instead created one component called TTable, they would have had to duplicate that same functionality in the TQuery component. This approach is wasteful. The smart thing to do is to build a component called TDataSet and end its functionality at the point at which the specific attributes of TQuery and TTable need to be defined. That way, TQuery and TTable can both reuse the functionality of TDataSet rather than have to rewrite that same functionality for both objects.

Before I close this section, let me reiterate some key points. The TFindDirs object is the brains of this particular operation. It knows how to iterate through directories, how to find all the files in each directory, and how to notify the user when new directories or files are found. The SearchDirs program is just a wrapper around this core functionality.

Iterating Through Directories

The task of iterating through directories has a simple recursive solution. However, recursion is a slow and time-consuming technique that is also wasteful of stack space. As a result, TFindDirs creates its own stacks and pushes the directories it finds onto them.


NOTE: BCB includes some built-in tools for creating stacks and lists. For example, the TList and TStringList objects are available. I use these tools here because they are simple objects specific to the VCL. Another alternative would have been to use the STL.

You can find the following objects in FindDirs2.h: TDirInfo: This simple structure keeps track of the current directory and of the complete set of information describing a particular file.

TDirStack: I need a place to push each directory after I find it. That leaves me free to iterate through all the files in the directory first and then go back and pop each subdirectory off the stack when I am free to examine it.

TFindDirs: This object provides the ability to iterate through directories.

TFindDirsList: This object adds TStringList objects to TFindDirs. These objects are accessible as properties, and they are maintained automatically by the object. I do not use the TFindDirsList object in the SearchDirs example. However, you'll find it very helpful when you're experimenting with these objects on your own.

To dust off the classic analogy used in these situations, the stacks implemented here are like piles of plates in a kitchen cabinet. You can put one plate down and then add another one to it. When you need one of the plates, you take the first one off either the top or the bottom, depending on whether it's a FIFO or a LIFO stack. Putting a new plate on the top of a stack is called pushing the object onto the stack, and removing a plate is called popping the object off the stack. For more information on stacks, refer to any book on basic programming theory. Books that cover the STL (Standard Template Library) in depth also usually cover this subject in the process.

Look at the implementation of the following FIFO stack:

void TDirStack::Push(TDirInfo *DirInfo)


{

  Add(DirInfo);

}

TDirInfo *TDirStack::Pop()

{

  void *Temp = Items[0];

  Delete(0);

  return (TDirInfo *)Temp;

}

The code is so simple because it is built on top of the TList object that is part of the VCL:

class TDirStack : public TList

{

public:

  TDirStack(): TList() {};

  void Push(TDirInfo *DirInfo);

  TDirInfo *Pop();

};

One look at this simple code, and you can see why I was drawn to the TList object rather than the STL. If I had had any doubt in my mind, then, of course, I would have turned to the VCL, because this book is about BCB, not about the Standard Template Library.

Using FindFirst, FindNext, and FindClose

In this section, I continue the examination of the stacks created in the TFindDirs units. The cores of these stacks are the calls to FindFirst, FindNext, and FindClose that search through directories looking for particular files.

Using FindFirst, FindNext, and FindClose is like typing DIR in a directory at the DOS prompt. FindFirst finds the first file in the directory, and FindNext finds the remaining files. You should call FindClose when you're finished with the process. FindFirst and the others are found in the SysUtils unit.

These calls enable you to specify a directory and file mask, as if you were issuing a command of the following type at the DOS prompt:

dir c:\aut*.bat

This command would, of course, show all files beginning with aut and ending in .bat. This particular command would typically find AUTOEXEC.BAT and perhaps one or two other files.

When you call FindFirst, you pass in three parameters:

extern int __fastcall FindFirst(const System::AnsiString Path,

                                int Attr, TSearchRec &F);

The first parameter contains the path and file mask that specify the files you want to find. For example, you might pass in "c:\\BCB\\include\\vcl\\*.hpp" in this parameter. The second parameter lists the type of files you want to see:

faReadOnly     0x01     Read-only files

faHidden       0x02     Hidden files

faSysFile      0x04     System files

faVolumeID     0x08     Volume ID files

faDirectory    0x10     Directory files

faArchive      
0x20     Archive files

faAnyFile      0x3F     Any file

Most of the time, you should pass in faArchive in this parameter. However, if you want to see directories, pass in faDirectory. The Attribute parameter is not a filter. No matter what flags you use, you will always get all normal files in the directory. Passing faDirectory causes directories to be included in the list of normal files; it does not limit the list to directories. You can use OR to concatenate several different faXXX constants, if you want. The final parameter is a variable of type TSearchRec, which is declared as follows:

struct TSearchRec {

  int Time;

  
int Size;

  int Attr;

  System::AnsiString Name;

  int ExcludeAttr;

  int FindHandle;

  WIN32_FIND_DATAA FindData; };

The most important value in TSearchRec is the Name field, which on success specifies the name of the file found. FindFirst returns zero if it finds a file and nonzero if the call fails. However, I rely heavily on the FindData portion of the record. FindData is the original structure passed back from the operating system. The rest of the fields are derived from it and are presented here in this form so as to present a simple, easy-to-use interface to VCL programmers.

WIN32_FIND_DATA looks like this:

typedef struct _WIN32_FIND_DATA { // wfd

    DWORD dwFileAttributes;

    FILETIME ftCreationTime;

    FILETIME ftLastAccessTime;

    FILETIME ftLastWriteTime;

    DWORD    nFileSizeHigh;

    DWORD    nFileSizeLow;

    DWORD    
dwReserved0;

    DWORD    dwReserved1;

    TCHAR    cFileName[ MAX_PATH ];

    TCHAR    cAlternateFileName[ 14 ];

} WIN32_FIND_DATA;

FindNext works exactly like FindFirst, except that you have to pass in only a variable of type TSearchRec because it is assumed that the mask and file attribute are the same. Once again, FindNext returns zero if all goes well, and a nonzero value if it can't find a file. You should call FindClose after completing a FindFirst/FindNext sequence.

Given this information, here is a simple way to call FindFirst, FindNext, and FindClose:

void 
TFindDirs::GetAllFiles(AnsiString *StartDir)

{

  TSearchRec FileData;

  int Info;

  Info = FindFirst(StartDir->c_str(), faDirectory, FileData);

  while (Info == 0)

  {

    if (FileData.Attr == faDirectory)

      FoundADir(&FileData);

    
else

      FoundAFile(&FileData);

    Info = FindNext(FileData);

  }

  FindClose(&FileData.FindData);

}

That's all I'm going to say about the basic structure of the TFindDirs object. As I said earlier, you can learn more about stacks by studying a book on basic programming data structures. This book, however, is about BCB, so I'm going to move on to a discussion of creating event handlers.

Summary

The SearchDirs program, along with the TFindDirs component, points the way toward an understanding of BCB's greatest strengths. TFindDirs is not a particularly difficult piece of code, but it is sufficiently complex to highlight the fact that you can place almost any kind of logic inside a BCB object. If you want to write multimedia code, or code that enables conversations on a network or simulates the behavior of a submarine, you can write a BCB component or set of components that will encapsulate the logic needed to reach your goal. More importantly, these components can then be placed on the Component Palette and dropped onto a form where they can easily be manipulated through the Object Inspector. Objects help you hide complexity and help you reuse code.

The Object Inspector--and its related property editors and component editors--provide an elegant, easy-to-use interface to any object. Component architectures represent one of the most important tools in programming today, and BCB has by far the best implementation of a component architecture currently available in the C++ market. In fact, the VCL is several orders of magnitude better at creating components than any other existing Windows-based technology.

TOCBACKFORWARD

©Copyright, Macmillan Computer Publishing. All rights reserved.