Now that you've been introduced to TField objects and seen an example of their run-time use, it is time to build a simple example based on the declaration of field objects at design time using the Fields editor. We can start again from the first example we built, GridDemo, and add a calculated field. The COUNTRY.DB database table we are accessing has both the population and the area of each country, so we can use this data to compute the population density.
To build the new example, named Calc, select the Table component in the form, and open the Fields editor. In this editor, choose the Add command, and select some of the fields. (I've decided to include them all.) Now select the Define command, and enter a proper name and data type (TFloatField) for the new calculated field, as you can see in Figure 13.8.
The definition of a calculated field in the Calc example
Warning It is obvious that as you create some field components at design time using the Fields editor, the fields you skip won't get a corresponding object. What might not be obvious is that the fields you skip will not be available even at run time, with Fields or FieldByName. When a program opens a table at run time, if there are no design-time field components, Delphi creates field objects corresponding to the table definition. If there are some design-time fields, however, Delphi uses those fields without adding any extra ones.
Of course, we also need to provide a way to calculate the new field. This is accomplished in the OnCalcFields event of the Table component, which has the following code (at least in a first version):
procedure TForm2.Table1CalcFields(DataSet: TDataSet); begin
Table1PopulationDensity.Value := Table1Population.Value / Table1Area.Value; end;
Calculated fields are computed for each record and recalculated each time the record is loaded in an internal buffer, invoking the OnCalcFields event over and over again. For this reason, a handler of this event should be extremely fast to execute, and cannot alter the status of the dataset, by accessing different records. A more time-efficient (but less memory-efficient) version of a calculated field is provided by the ClientDataSet component with "internally calculated" fields, which are evaluated only once, when they are loaded, with the result stored in memory for future requests.
Everything fine? Not at all! If you enter a new record and do not set the value of the population and area, or if you accidentally set the area to zero, the division will raise an exception, making it quite problematic to continue using the program. As an alternative, we could have handled every exception of the division expression and simply set the resulting value to zero: try
However, we can do even better. We can check if the value of the area is defined—if it is not null—and if it is not zero. It is better to avoid using exceptions when you can anticipate the possible error conditions:
if not Table1Area.IsNull and
(Table1Area.Value <> 0) then Table1PopulationDensity.Value := Table1Population.Value / Table1Area.Value else
Table1PopulationDensity.Value := 0;
The code of the Table1CalcFields method above (in each of the three versions) accesses some fields directly. This is possible because I used the Fields editor, and it automatically created the corresponding field declarations, as you can see in this excerpt of the interface declaration of the form: type
TCalcForm = class(TForm) Table1: TTable;
procedure Table1CalcFields(DataSet: TDataset);
Each time you add or remove fields in the Fields editor, you can see the effect of your action immediately in the grid present in the form. Of course, you won't see the values of a calculated field at design time; they are available only at run time, because they result from the execution of compiled Pascal code.
Since we have defined some components for the fields, we can use them to customize some of the visual elements of the grid. For example, to set a display format that adds a comma to separate thousands, we can use the Object Inspector to change the DisplayFormat property of some field components to "###,###,###". This change has an immediate effect on the grid at design time.
The display format I've just mentioned (and used in the previous example) uses the Windows International Settings to format the output. When Delphi translates the numeric value of this field to text, the comma in the format string is replaced by the proper ThousandSeparator character. For this reason, the output of the program will automatically adapt itself to different International Settings. On computers that have the Italian configuration, for example, the comma is replaced by a period.
After working on the table components and the fields, I've customized the DBGrid using its Columns property editor. I've set the Population Density column to read-only and set its ButtonStyle property to cbsEllipsis, to provide a custom editor. When you set this value, a small button with an ellipsis is displayed when the user tries to edit the grid cell. Pressing the button invokes the OnEditButtonClick event of the DBGrid:
procedure TCalcForm.DBGrid1EditButtonClick(Sender: TObject); begin
MessageDlg (Format (
'The population density (%.2n)'#13 + 'is the Population (%.0n)'#13 + 'divided by the Area (%.0n).'#13#13 + 'Edit these two fields to change it.', [Table1PopulationDensity.AsFloat, Table1Population.AsFloat, Table1Area.AsFloat]), mtInformation, [mbOK], 0);
Actually, I haven't provided a real editor, but rather a message describing the situation, as you can see in Figure 13.9, which shows the values of the calculated fields. To create an editor, you might build a secondary form to handle special data entries.
The output of the Calc example. Notice the Population Density calculated column, the ellipsis button, and the message displayed when you select it.
Was this article helpful?