During the development of a database-driven personal information manager, I had to develop a data-aware treeview control to be used as an interface to one of the dataset of the database.
Introduction
The amount of power Delphi gives to a developer is amazing. If you want to build a database application? No problem, just choose from many of the data access options Delphi provides (BDE, ADO, dbExpress, IBExpress, etc.) then pick your database (Paradox, Access, Interbase, SQL Server, etc). If you need to build a shell extension? Again no problem! Delphi provides the easiest access to Windows API as possible. A graphical software? A multimedia application?
The strength of Delphi lies in its Visual Component Library (VCL), a set of powerful controls/components grouped into a hierarchical library. Each control is built to provide some special functionality in an application. It is very common to drop a TButton, TEdit or any of the VCL controls on a form (and even a TForm is a component) and operate with properties, methods and events.
It is simple and easy but Delphi offers more than that. In the Delphi programming environment, one can also design and implement genuine components and add them to the component palette thus extending the VCL. It is a very effective way to reuse code that is detailed in many text books.
Database functionality
Delphi's core database functionality is accomplished by two groups of components, namely data-access components and data-aware components. Data-access components encapsulate the interface to a database server whereas the data-aware components are user interface components that can interface with data-access components.
Delphi's data-aware components are components that normally reside on a Standard palette tab but have been modified to display and manipulate the content of data in a dataset (table or query). The choice of controls is determined by how we want to present the information and how we want to let users browse (and manipulate - add or edit) through the records of datasets. DBEdit and DBMemo, for example, are used to represent an individual record from a dataset. The DBGrid, on the other hand, is generally used when representing the contents of an entire dataset. Since all the data-aware controls are counterparts to the standard Windows controls - with a few extra properties, building a functional database application should be a relatively familiar task.
All the data-aware components share one common property: Data Source. Simply put, the DataSource component provides a mechanism to hook dataset components to the visual data-aware components that display the data. Generally, one will need one datasource component for each dataset component to present a link to one or more data-aware controls.
Approach
In the forthcoming section, we will develop three data-aware controls ranked here in increasing order of development complexity (rather than interest or usefulness):
- Component # 1 -TGtroDBPushButtonCalendar - a data-aware control that provides a calendar interface to a data set. We will introduce the notion of data links;
- Component # 2 -TGtroDBDateTimePicker - a data-aware control replicating the behaviour of the TDateTimePicker of the Win32 pane of the component palette. We will introduce property editors as it requires a property editor for its DataField property.
- Component # 3 -TGtroDBTreeView - a data-aware control that provides a hierarchical interface to a data set.. It comes in third place as it requires both property editors and the development of a genuine data link as it communicates with several data fields.
Component # 1
A data-aware push-button calendar control
This data-aware visual control was required to highlight special days in a database-driven personnal information manager then under development. The component TGtroDBPushButtonCalendar is a class (derived from TCustomGrid) that can be used to implement a special purpose calendar whose main action is to associate or disassociate a record in a dataset with the date selected in the calendar.
The code that generates the internal mechanicsof the calendar is essentially that of the TCalendar component found on the Sample pane of the component palette. Clicking on a date in the calendar automatically selects that date whereas changes in the calendar trigger the OnChange event. Public methods NextMonth, NextYear, PrevMonth and PrevYear are available du move the calendar forward and backward by one month or by one year. CalendarDate, a TDateTime type property has the value of the date selected by the user in the calendar.As shown in Figure 1, our now component looks exactly like the TCalendar component.
We derived the new component from the TCustomGrid class and copied most of the code from the Calendar.pas unit so that we could publicize only those properties that we wanted to be public or published.
We wanted to provide storage for the state of the calendar and, in the context of a database-driven personnal information manager, such storage had to be a data set. To make the component data-aware, we only needed to link it to a data set. This is what we will do in the next section.
Data link
Generally, database programs connect to some data-aware control through a DataSource component, and then connect the DataSource component to a data set, usually a TTable or a TQuery. The connection between a data-aware control and the TDataSource is called a data link, and is represented programmatically by an object of class TDataLink declared and implemented in DB.pas. Though TDataLink is not technically an abstract class, it is seldom used directly. Either you use one of the Delphi-provided data link classes derived from TDataLink ( TFieldDataLink and TGridDatalink) or you derive a new one yourself.
The most important class derived from the TDataLink class is the TFieldDataLink class which is used by data-aware controls that relate to single fields of a data set. This is the class that we need here as we will link only the CalendarDate property of the calendar component to one field of a data set.
We will declare the TFieldDataLink object as an internal object of the component and will make the DataSource and the DataField properties of the data link available to the user of the new component.
The TFieldDatalink class
TFieldDataLink is derived from the TDatalink class. Through this class, the data-aware component can then link to a TField object by using the FieldName property of the TFieldDataLink and can link to a data set by using the DataSource property of the TFieldDataLink. The mechanism used to make these properties behave as if they were genuine properties of the data-aware component is as shown in the Get and Set methods of the DataSource shown hereunder:
function TGtroDBPushButtonCalendar.GetDataSource: TDataSource;
begin Result:= FDataLink.DataSource; end;
and
procedure TGtroDBPushButtonCalendar.SetDataSource(DataSource: TDataSource);
begin FDataLink.DataSource:= DataSource; end;
These properties appear in the Object Inspector where they can be modified at design time. They can also be changed programmatically during execution.
Click or double-click
This main action is executed either with a click or a double-click depending on the setting of the MainActions property (maClick or maDblClick). If an associate record does not exists when the calendar cell is activated, one is created. If one exists already, it is deleted from the dataset.
procedure TGtroDBPushButtonCalendar.Click;
begin inherited Click;
CalendarDate:= RowColToDate(Col, Row);
RowColToRecord(Col, Row); // activates the corrsponding record of the data set if it exists TriggerOnCalendarDateChange; if MainActions = maClick then ActionOnRecord;
end;
or
procedure TGtroDBPushButtonCalendar.DblClick;
begin if MainActions = maDblClick then ActionOnRecord;
inherited;
end;
In the Click method, CalendarDate is set to its value by the RowColToDate method that translate the position of the click on the calendar into a date. (the Day, Month and Year public properties are set at the same time). It searches the record of the data set associated with the date of the click and activates it if it exists. It then triggers the OnCalendarDataChange event before executing the ActionOnRecord method if the MainActions property is set to maClick. If it is set to maDblClick, this action is performed by the DblClick method.
Inserting and deleting records of the data set
As shown in the code above, the action of inserting or deleting a record in the underlying data set is triggered either by a click or by a double-click. The gist of the action occurs in the ActionOnRecord method, the code of which is presented hereunder:
procedure TGtroDBPushButtonCalendar.ActionOnRecord;
begin if Row > 0 then begin if FHasRecord[Col, Row] then FDataLink.DataSet.Delete else begin FDataLink.OnDataChange:= nil;
try FDataLink.DataSet.Append; FDataLink.DataSet.FieldByName(FieldName).AsDateTime:= CalendarDate; FDataLink.DataSet.Post; finally FDataLink.OnDataChange:= DataChange; UpdateCalendarData; // sets internal data Invalidate; // redraws the calendar FOwnsRecord:= FHasRecord[Col, Row]; end; // try...finally end; // else end; // if Row ... end;
FHasRecord is an array that contains, for each cell of the calendar, a boolean value which is true when the cell has to be highlighted (there is a record corresponding to it in the data set). If it is true, such record is deleted. Otherwise, one is created, its date field is set to the data of the calendar, the internal data of the calendar is set and the calendar is redrawn.
Setting the internal data of the calendar is performed by the UpdateCalendarData method which does what follows:
- for each column of the calendar, put the name of the day of the week in the top row;
- for each cell of the calendar, writes the day of the month, determine if there is a record of the data set corresponding to the date of the cell and sets FHasRecord accordingly (true if one exists) .
Finally the calendar is redrawn on the screen.
Drawing the calendar
The calendar shows one month at a time and the highlighted dates are saved in a data set.
Component # 2
A data-aware DateTimePicker control
The TDateTimePicker component is a visual component designed specifically for entering dates or times. In dmComboBox date mode, it resembles a list box or combo box, except that the drop-down list is replaced with a calendar illustration; users can select a date from the calendar. Dates or times can also be selected by scrolling with Up and Down arrows and by typing.
It is derived from the TCommonCalendar class, a base class used when creating custom controls that represent dates in a calendar-like format. It is declared in the ComCtrl.pas unit and, unlike most components of the VCL, it is not derived from a TCustomDateTimePicker class that would set methods and properties as protected thus enabling descendant class to publicize or publish them (visibility cannot be reduced).
We will begin our study of data-aware components by implementing a data-aware DateTimePicker that we will derive from the TDateTimePicker class of the VCL.
type TGtroDBDateTimePicker = class(TDateTimePicker)
...
end;
This new component adds only one capability to the TDateTimePicker component: the Date property will be linked to a date field in a data set thus modifying this field in a non-permanent way (the user must do an explicit post to make it permanent) through its OnClose event (the OnChange event will have no effect).
How do we link our component to a dataset?
Our declaration of the new component now stands as follows:
TGtroDBDateTimePicker = class(TDateTimePicker)
private FCloseAction: TCloseActions; FDataLink: TFieldDataLink; // we link only one field // delegation of TFieldDataLink properties to TGtroDBDateTimePicker ... public constructor Create(Owner: TComponent); override;
destructor Destroy; override;
published property CloseAction: TCloseActions read FCloseAction write FCloseAction;
property DataField: string read GetDataField write SetDataField;
property DataSource: TDataSource read GetDataSource write SetDataSource;
property ReadOnly: boolean read GetReadOnly write SetReadOnly Default true;
end;
where the highlighted variable is our internal data link object.
As shown above, the new component has four new published properties: CloseAction, DataField, DataSource and ReadOnly. They are essentially properties of the data link that are made available to the user of the component through their Get and Set methods.
function TGtroDBDateTimePicker.GetDataSource: TDataSource;
begin Result:= FDataLink.DataSource; end;
and
procedure TGtroDBDateTimePicker.SetDataSource(const Value: TDataSource);
begin if not (FDataLink.DataSourceFixed and (csLoading in ComponentState)) then FDataLink.DataSource:= Value; if Value <> nil then Value.FreeNotification(self);
end;
The OnDataChange event handler of the TFieldDataLink object takes specific action when there is a change to the contents of the field. For thsi component, it is assigned to the method called DataChange. Its code follows:
procedure TGtroDBDateTimePicker.DataChange(Sender: TObject);
var DT: TDateTime; begin if Assigned(FDataLink.Field) then DT:= FDataLink.Field.AsDateTime
else DT:= Now;
if Kind = dtkTime then Time:= DT
else Date:= DT;
end;
Notifications received by the component
procedure TGtroDBDateTimePicker.CNNotify(var Message: TWMNotify);
begin inherited; // do what DateTimePicker does before ... with Message, NMHdr^ do begin case code of DTN_DATETIMECHANGE: if CloseAction = dtnChange then UpdateData(Self); // the system time has changed DTN_CLOSEUP: if CloseAction = dtnCloseUp then UpdateData(Self);
end; //...case end; //...with end;
Actions
procedure TGtroDBDateTimePicker.UpdateData(Sender: TObject);
begin try FDataLink.OnDataChange:= nil;
FDataLink.DataSet.Edit;
FDataLink.Field.AsDateTime:= Date;
finally FDataLink.OnDataChange:= DataChange; end;
end;
Property Editor
The Object Inspector provides default editing for all types of properties but on occasion, it may be necessary to provide a special editor for specific properties. It is the case here for the DataField property as we want to avoid selecting fields that cannot contain date formats. In fact, the selection rule is as follows:
- if Kind = dtkDate then fields of type ftDateTime and ftDate can be accessed;
- if Kind = dtkTime then fields of type ftDateTime and ftTime can be accessed.
Component # 3
A data-aware TreeView control
Conclusion
Appendices
References