An important idea related to datasets in Delphi is that they represent a set of data, regardless of where this data comes from. A SQL server and a local file are examples of traditional datasets, but you can use the same technology to show a list of a system's users, a list of a folder's files, the properties of objects, XML-based data, and so on.
As an example, the second dataset presented in this chapter is a list of files. I've built a generic dataset based on a list of objects in memory (using a TObjectList), and then derived a version in which the objects correspond to a folder's files. The example is simplified by the fact that it is a read-only dataset, so you might find it more straightforward than the previous dataset.
Some of the ideas presented here were discussed in an article I wrote for the Borland Community website, published in June 2000 at the URL bdn.borland.com/article/0,1410,20587,00.html.
The generic list-based dataset is called TMdListDataSet and contains the list of objects, a list that is created when you open the dataset and freed when you close it. This dataset doesn't store the record data within the buffer; rather, it saves in the buffer only the position in the list of the entry corresponding to the record's data. This is the class definition:
type TMdListDataSet = class (TMdCustomDataSet) protected // the list holding the data FList: TObjectList; // dataset virtual methods procedure InternalPreOpen; override; procedure InternalClose; override; // custom dataset virtual methods function InternalRecordCount: Integer; override; procedure InternalLoadCurrentRecord (Buffer: PChar); override; end;
You can see that by writing a generic custom data class, you can override a few virtual methods of the TDataSet class and of this custom dataset class, and have a working dataset (although this is still an abstract class, which requires extra code from subclasses to work). When the dataset is opened, you have to create the list and set the record size, to indicate you're saving the list index in the buffer:
procedure TMdListDataSet.InternalPreOpen; begin FList := TObjectList.Create (True); // owns the objects FRecordSize := 4; // an integer, the list item id end;
Further derived classes at this point should also fill the list with objects.
Like the ClientDataSet, my list dataset keeps its data in memory. However, using some smart techniques, you can also create a list of fake objects and then load the actual objects only when you are accessing them.
Closing is a matter of freeing the list, which has a record count corresponding to the list size:
function TMdListDataSet.InternalRecordCount: Integer; begin Result := fList.Count; end;
The only other method saves the current record's data in the record buffer, including the bookmark information. The core data is the position of the current record, which matches the list index (and also the bookmark):
procedure TMdListDataSet.InternalLoadCurrentRecord (Buffer: PChar); begin PInteger (Buffer)^ := fCurrentRecord; with PMdRecInfo(Buffer + FRecordSize)^ do begin BookmarkFlag := bfCurrent; Bookmark := fCurrentRecord; end; end;
The derived directory dataset class has to provide a way to load the objects in memory when the dataset is opened, to define the proper fields, and to read and write the value of those fields. It also has a property indicating the directory to work on—or, to be more precise, the directory plus the file mask used for filtering the files (such as c:\docs\*.txt):
type TMdDirDataset = class(TMdListDataSet) private FDirectory: string; procedure SetDirectory(const NewDirectory: string); protected // TDataSet virtual methods procedure InternalInitFieldDefs; override; procedure SetFieldData(Field: TField; Buffer: Pointer); override; function GetCanModify: Boolean; override; // custom dataset virtual methods procedure InternalAfterOpen; override; public function GetFieldData(Field: TField; Buffer: Pointer): Boolean; override; published property Directory: string read FDirectory write SetDirectory; end;
The GetCanModify function is another virtual method of TDataSet, used to determine if the dataset is read-only. In this case, it returns False. You don't have to write any code for the SetFieldData procedure, but you must define it because it is an abstract virtual method.
Because you are dealing with a list of objects, the unit includes a class for those objects. In this case, the file data is extracted from a TSearchRec buffer by the TFileData class constructor:
type TFileData = class public ShortFileName: string; Time: TDateTime; Size: Integer; Attr: Integer; constructor Create (var FileInfo: TSearchRec); end; constructor TFileData.Create (var FileInfo: TSearchRec); begin ShortFileName := FileInfo.Name; Time := FileDateToDateTime (FileInfo.Time); Size := FileInfo.Size; Attr := FileInfo.Attr; end;
This constructor is called for each folder while opening the dataset:
procedure TMdDirDataset.InternalAfterOpen; var Attr: Integer; FileInfo: TSearchRec; FileData: TFileData; begin // scan all files Attr := faAnyFile; FList.Clear; if SysUtils.FindFirst(fDirectory, Attr, FileInfo) = 0 then repeat FileData := TFileData.Create (FileInfo); FList.Add (FileData); until SysUtils.FindNext(FileInfo) <> 0; SysUtils.FindClose(FileInfo); end;
The next step is to define the fields of the dataset, which in this case are fixed and depend on the available directory data:
procedure TMdDirDataset.InternalInitFieldDefs; begin if fDirectory = '' then raise EMdDataSetError.Create ('Missing directory'); // field definitions FieldDefs.Clear; FieldDefs.Add ('FileName', ftString, 40, True); FieldDefs.Add ('TimeStamp', ftDateTime); FieldDefs.Add ('Size', ftInteger); FieldDefs.Add ('Attributes', ftString, 3); FieldDefs.Add ('Folder', ftBoolean); end;
Finally, the component has to move the data from the list object referenced by the current record buffer (the ActiveBuffer value) to each field of the dataset, as requested by the GetFieldData method. This function uses either Move or StrCopy, depending on the data type, and it does some conversions for the attribute codes (H for hidden, R for read-only, and S for system) extracted from the related flags and used to determine whether a file is a folder. Here is the code:
function TMdDirDataset.GetFieldData (Field: TField; Buffer: Pointer): Boolean; var FileData: TFileData; Bool1: WordBool; strAttr: string; t: TDateTimeRec; begin FileData := fList [Integer(ActiveBuffer^)] as TFileData; case Field.Index of 0: // filename StrCopy (Buffer, pchar(FileData.ShortFileName)); 1: // timestamp begin t := DateTimeToNative (ftdatetime, FileData.Time); Move (t, Buffer^, sizeof (TDateTime)); end; 2: // size Move (FileData.Size, Buffer^, sizeof (Integer)); 3: // attributes begin strAttr := ' '; if (FileData.Attr and SysUtils.faReadOnly) > 0 then strAttr  := 'R'; if (FileData.Attr and SysUtils.faSysFile) > 0 then strAttr  := 'S'; if (FileData.Attr and SysUtils.faHidden) > 0 then strAttr  := 'H'; StrCopy (Buffer, pchar(strAttr)); end; 4: // folder begin Bool1 := FileData.Attr and SysUtils.faDirectory > 0; Move (Bool1, Buffer^, sizeof (WordBool)); end; end; // case Result := True; end;
The tricky part in writing this code was figuring out the internal format of dates stored in date/time fields. This is not the common TDateTime format used by Delphi, and not even the internal TTimeStamp, but what is internally called the native date and time format. I've written a conversion function by cloning one I found in the VCL code for the date/time fields:
function DateTimeToNative(DataType: TFieldType; Data: TDateTime): TDateTimeRec; var TimeStamp: TTimeStamp; begin TimeStamp := DateTimeToTimeStamp(Data); case DataType of ftDate: Result.Date := TimeStamp.Date; ftTime: Result.Time := TimeStamp.Time; else Result.DateTime := TimeStampToMSecs(TimeStamp); end; end;
With this dataset available, building the demo program (shown in Figure 17.7) was just a matter of connecting a DBGrid component to the dataset and adding a folder-selection component, the sample ShellTreeView control. This control is set up to work only on files, by setting its Root property to C:\. When the user selects a new folder, the OnChange event handler of the ShellTreeView control refreshes the dataset:
procedure TForm1.ShellTreeView1Change(Sender: TObject; Node: TTreeNode); begin MdDirDataset1.Close; MdDirDataset1.Directory := ShellTreeView1.Path + '\*.*'; MdDirDataset1.Open; end;
If your version of Windows has problems with the sample shell controls available in Delphi, you can use the DirDemoNoShell version of the example, which uses the old-fashioned Windows 3.1–compatible Delphi file controls.