Overview
In this chapter, you will learn how to build a non-visual component. In
particular, I will emphasize the following topics:
- Descending directly from TComponent
- Using and designing non-visual components
- Using FindFirst and FindNext to iterate through the
files in directories
- Creating your own stacks
- Pushing and popping items off a stack
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:
- A program for iterating through directories to delete files that I no
longer need.
- A program for "touching" all the files in a series of directories so that
they have the same date.
- A file backup utility for comparing two directory structures to be sure
they are identical. This same program will also produce scripts that copy
files from one directory structure to the other if a mismatch occurs.
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:
- You might need to tweak several options before you use this object. In
particular, you need to decide whether you want to have the lists of
directories and files that you find saved to memory in a TStringList.
Letting the programmer decide these matters by clicking a property can go a
long way toward presenting a clean, easy-to-use interface for an object.
- The TFindDirs object has two features that can be accessed
through the Events page. Specifically, custom event handlers can be
notified every time a new file or directory has been found. However,
constructing an event handler manually can be confusing, particularly if you
don't know which parameters will be passed to the functions involved. If you
place a component on the Component Palette, you do not need to guess about how
to handle an event. All it takes is a quick click on the Events page,
and the event handler is created for you automatically!
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.
|