In this part, we develop a data-aware calendar that requires a genuine datalink.
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 mechanics of 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 to 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 new 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.
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 it or you derive a new one yourself.
The TDataLink class
The following declaration of the TDataLink class is extracted from the db.pas unit of Delphi 6. I have added comments explaining the role of most of the methods and properties. Expand it below.
TDataLink = class(TPersistent) private ... protected procedure ActiveChanged; virtual; // called whenever the datasource the TDataLink is attached to becomes active or inactive procedure CheckBrowseMode; virtual; procedure DataEvent(Event: TDataEvent; Info: Longint); virtual; // responds to various events that occur while working with data procedure DataSetChanged; virtual; procedure DataSetScrolled(Distance: Integer); virtual; // called whenever the current record in the dataset changes procedure EditingChanged; virtual; // called when the editing state of the TDataLink changes procedure FocusControl(Field: TFieldRef); virtual;// called as a result of Field.FocusControl function GetActiveRecord: Integer; virtual; function GetBOF: Boolean; virtual; function GetBufferCount: Integer; virtual function GetEOF: Boolean; virtual; function GetRecordCount: Integer; virtual; procedure LayoutChanged; virtual; // called when the layout of the attached dataset changes (e.g. column added) function MoveBy(Distance: Integer): Integer; virtual; procedure RecordChanged(Field: TField); virtual; // called when the current record is edited or when the record's text has changed procedure SetActiveRecord(Value: Integer); virtual; procedure SetBufferCount(Value: Integer); virtual; procedure UpdateData; virtual; // called immediately before a record is updated in the database property VisualControl: Boolean read FVisualControl write FVisualControl; public constructor Create; destructor Destroy; override; function Edit: Boolean; // puts the TDatalink's attached dataset into edit mode function ExecuteAction(Action: TBasicAction): Boolean; dynamic; function UpdateAction(Action: TBasicAction): Boolean; dynamic; procedure UpdateRecord; // sets or returns the current record within the TDatalink's buffer window property Active: Boolean read FActive; // returns true when the data link is connected to an active datasource. property ActiveRecord: Integer read GetActiveRecord write SetActiveRecord; // sets or returns the current record within the TDatalink's buffer window. property BOF: Boolean read GetBOF; property BufferCount: Integer read FBufferCount write SetBufferCount; property DataSet: TDataSet read GetDataSet; // the dataset the TDataLink is attached to. This is a shortcut to DataSource.DataSet. property DataSource: TDataSource read FDataSource write SetDataSource; // sets or returns data source control the TDataLink is attached to. property DataSourceFixed: Boolean read FDataSourceFixed write FDataSourceFixed; // used to prevent the data source for the TDataLink from being changed property Editing: Boolean read FEditing; // Returns true if the datalink is in edit mode property Eof: Boolean read GetEOF; property ReadOnly: Boolean read FReadOnly write SetReadOnly; // determines if the TDataLink is read only property RecordCount: Integer read GetRecordCount // returns the approximate number of records in the attached dataset end;
All the virtual methods are called by the DataEvent protected method, which is a sort of window procedure for a data source, triggered by several data events. These events originate in the dataset, fields, or data source, and are generally applied to a dataset. The DataEvent() method of the dataset component dispatches the events to the connected data sources. Each data source calls the NotifyDataLinks() method to forward the event to each connected data link, and then the data source triggers its own OnDataChange or OnUpdateData event.
The mechanism for having the TDataLink object communicate with a component is to override its virtual procedures. This is done in its descendants and in particular in its most important sub-class, 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.
The TFieldDatalink class
The TFieldDataLink class inherits the capabilities of the TDatalink class and provides a data-aware windowed control a link to a TField object by using its FieldName property. Declaring the TFieldDatalink object as an internal object of our component allow us to make its DataSource and FieldName properties available to the users of our component. 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;
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.
There was no need to elaborate much about the datalink as we could use a Delphi-provided datalink but the field of the dataset that we must link to contains a date. A custom property editor was therefore required for the DataField property in order to avoid selecting fields that cannot contain date formats. We derived the TGtroDBPushButtonCalendarDataFieldEditor field editor from the TStringProperty class: this process is detailed in a separate article entitled "A data-aware Treeview front-end component".
The source code of this component can be downloaded here. This code contains three data-aware components contained in a package called gtrodblib6.dpk. Once it is compiled and installed on Delphi 6, the component (and the other two) becomes available in the gtro pane of the component palette. From there, it can be dropped on a form or on a frame. The component is then displayed on the form or the frame and you give it the name that you want. The object inspector displays the published its properties and event handlers. The most important are:
- DataSource which must be connected to a dataset;
- FieldName which must be connected to a date field of the dataset(ftDate or ftTime);
- MainActions is set to either maClick or maDblClick;
The main action of the component is to create or delete a record of the dataset corresponding to the date that is clicked or double-clicked on the calendar. Once a record is created, its other fields can be used for whatever you want.
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 corresponding record of the data set if it exists TriggerOnCalendarDateChange; if MainActions = maClick then ActionOnRecord; end
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.
procedure TGtroDBPushButtonCalendar.ActionOnRecord; // Produces the main action of the component (inserting or deleting records) 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;
The component is declared as a descendant of TCustomGrid as follows:
TGtroDBPushButtonCalendar = class(TCustomGrid) private FDataLink: TFieldDataLink; // no need for a special datalink ... procedure ActionOnRecord; function DaysPerMonth(AYear, AMonth: Integer): Integer; function IsLeapYear(AYear: Integer): Boolean; function RowColToDate(ACol, ARow: Word): TDateTime; procedure TriggerOnCalendarDateChange; procedure UpdateCalendarData; // sets internal data protected procedure Click; override; procedure DblClick; override; procedure DataChange(Sender: TObject); procedure DrawCell(ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState); override; procedure WMSize(var Message: TWMSize); message WM_SIZE; public constructor Create(AOwner: TComponent); override; destructor Destroy; override procedure NextMonth; procedure NextYear; procedure PrevMonth; procedure PrevYear; property ChangeAction: Boolean read FChangeAction write SetChangeAction; property OwnsRecord: Boolean read FOwnsRecord; property Day: word read FDay write SetDay; property Month: Word read FMonth write SetMonth; property Year: Word read FYear write SetYear; published ... // publish the properties of TCustomGrid property CalendarDate: TDateTime read FDate write SetCalendarDate; property DataSource: TDataSource read GetDataSource write SetDataSource; property FieldName: string read GetFieldName write SetFieldName; property MainActions: eMainActions read FMainActions write FMainActions; property SelCellBkgColor: TColor read FSelCellBkgColor write FSelCellBkgColor; property OnCalendarDateChange: TPBCalendarDateChangeEvent read FOnCalendarDateChange write FOnCalendarDateChange; end;
In this declaration, for the sake of brievety, I have removed the publication of the TCustomGrid properties as well as the read and write methods of the new properties.
When the component is created. the datalink is created and the OnDataChange event handler is assigned to the method DataChange() as follows:
procedure TGtroDBPushButtonCalendar.UpdateCalendarData; // Sets the cell data // Called by ActionOnRecord, DataChange, SetMonth, SetYear, Create var i, j: Integer; Number: Integer; DateRecherche: TDateTime; begin DecodeDate(FDate, FYear, FMonth, FDay); FFirstOfMonth:= EncodeDate(FYear, FMonth, 1); FOffSet:= DayOfWeek(FFirstOfMonth)-2; FDaysInMonth:= DaysPerMonth(FYear, FMonth); FLastOfMonth:= FFirstOfMonth + FDaysInMonth - 1; FDataLink.OnDataChange:= nil; // disables the OnDataChange event handler try // initialize the rows and columns of the calendar for i:= 0 to ColCount-1 do // for each column if FLongFlag then FCells[i, 0]:= LongDayNames[i+1] else FCells[i, 0]:= ShortDayNames[i+1]; for i:= 0 to ColCount-1 do for j:= 1 to RowCount-1 do begin Number:= (i - FOffSet) + (j-1)*7; if Number = FDay then begin FSelectedCell.Left:= i; FSelectedCell.Right:= i; FSelectedCell.Top:= j; FSelectedCell.Bottom:= j; end; FHasRecord[i, j]:= False; if (Number <= 0) or (Number > FDaysInMonth) then FCells[i, j]:= '' else begin FCells[i, j]:= IntToStr(Number); if (FDataLink.DataSource <> nil) and (FDataLink.Field <> nil) then begin FDataLink.DataSet.DisableControls; try DateRecherche:= RowColToDate(i, j); if FDataLink.DataSource.DataSet.Locate(FieldName, DateRecherche, ) then FHasRecord[i, j]:= True; // if a corresponding record exists if Number = FDay then FOwnsRecord:= FHasRecord[i, j]; finally FDataLink.DataSet.EnableControls; end; // try...finally end; // ...if end; // ...else end; // ....for, for finally if (FDataLink.DataSource <> nil) and (FDataLink.Field <> nil) then FDataLink.DataSource.DataSet.Locate(FieldName, FDate, ); FDataLink.OnDataChange:= DataChange; TriggerOnCalendarDateChange; end; // try...finally end;
that prepares to draw the calendar. In this method, the rows and columns of the calendar are initialized: the first row is filles with the names of the days (from Sunday to Saturday) wheras the other rows are filles fith the number of the day. The dataset is then examined and the days that have corresponding records are highlighted. The calendar is displayed when the Invalidate statement of the constructor is executed.
The component has built-in public methods to push the displayed month forward or backward by one month of by one year. These methods are:
- NextMonth() - pushes the displayed month forward by one month;
- NextYear() - pushes the displayed month forward by one year
- PreviousMonth() - pushes the displayed month backward by one month;
- PreviousYear() - pushes the displayed month backward by one year;
but they must be called by the host application. They change the public properties Month and Year by one and, through their write methods, call the UpdateCalendarData() method, the code of which appears above in order to display a new month. In Figure 2, the component is shown on a panel and the external buttons that effect the changes of month are displayed just above the component. Here, days 1, 17 and 19 of May 2005 are highlighted meaning that the event associated with the component has occurred on these dates.
The action of inserting or deleting a record in the dataset 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, clicking or double-clicking on it causes the record to be 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.
This component was inspired by the calendar-like display provided by a piece of software that I used some years ago under Windows 3.1 and that was called "Calendar Creator Plus". Using this display, the user could select dates by pushing button corresponding to each date on this display and the calendar would display features related to the selected dates.
This component has been designed to highlight those dates when a given event has occurred. In order to link the calendar to a dataset, we had to develop a datalink and publish the DataSource and the FieldName properties. In order to restrict the selection of the FieldName property to a date field, we haave develop a field editor. The rest of the article has described how the component can be used and how it works.