Developing data-aware components
Part II: A data-aware Pushbutton Calendar component

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 component

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.

View of the GtroDBPushButtonCalendar 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 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;

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.

Property Editor

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".

How to use

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:

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.

Main action

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

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.

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;

How it works

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. 

Initialization of the component

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.

Changing the month that is displayed

Figure 2 - View of the TGtroDBPushButtonCalendar
at the center of a panel

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:

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.

Inserting and deleting records of the dataset

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: 

  1. for each column of the calendar, put the name of the day of the week in the top row;
  2. 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.

Conclusion

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.  

Warning!
This code was developed for the pleasure of it. Anyone who decides to use it does so at its own risk and agrees not to hold the author responsible for its failure.


Questions or comments?
E-Mail
Last modified: September 4th 2014 00:30:26. []