5 Minute Snack: Creating columns with calculated field values on demand with TTMSFNCGrid
As mentioned, I am in the process of preparing a video tutorial about FNC controls. The next video will be about TTMSFNCGrid, a bindable and datasource capable datagrid that will behave the same way in any framework and any platform.
The amount of properties, methods and events is immense and thus you can only get to know the control by using it in your projects. One of these projects involved dislaying additional information for the user that is based on the columns displayed in the grid.
I will simplify my actual example as such:
In the grid we allow the user to enter a start date and an end date for a project in the first two columns of each row. The third column will display the number of days between these dates. Pretty simple task, but no grid in this would has predefined calculations like this.
We have several approaches in Delphi. One approach would be to create an TFDMemTable with two fields and an additional calculated field for the number of days. This dataset can then be bound to the grid.
However, this is a simple example and for a real-life project we might not want to change our datasets for a visual finesse.
Thus, we simply want to use the grid control to display that info for us, but we are not interested in persisting this information or use it anywhere else.
After having created a VCL Forms Application I dropped a TTMSFNCGrid on the form and defined the number of Columns (ColumnCount ) and Rows (RowCount ) using their respective properties. I also set FixedColumns and FixedRows to 1 . Set DefaultColumnWidth to 120 and DefaultRowHeight to 24 .
In order to add rows, I add a button of type TTMSFNCButton .
The form should look something like this:
You might add anchors and other eye candy as you please.
Next, we have to allow for the user to add rows to the grid. For this, we need to implement the GridCanAppendRow-event:
procedure TForm1.GridCanAppendRow(Sender: TObject; ARow: Integer; var Allow: Boolean); begin // allow that rows can be added Allow := True; end;
Pretty simple, as we want to allow a row to be added without any restrictions. Of course, you could limit the number of total rows by stating:
Allow := Grid.RowCount < 22;
This will allow for 20 user-rows to be added. In case you ever add a footer you need to consider that row as well.
The button does the actual adding of the row:
procedure TForm1.btAddClick(Sender: TObject); begin // append a row Grid.RowCount := Grid.RowCount +1; end;
Unlike the VCL a change of rows does not delete the content of the grid. The FNC grid is pretty smart and notices that you simply want to add a row and keeps the data in place.
If you ran the example right now, you would already be able to enter data and add rows. However, there is absolutely no user comfort entering the data. Also the difference in days is not being calculated.
Double-click the grid and amend some properties for the columns:
We select the etDatePicker for the columns the user enters the dates into. Furthermore, we can set ReadOnly to true for the calculated column.
In order for the user to be able to navigate the grid nicely we set some additional keyboard options. We want to the user be able to press enter to go to the next column and add a row in case the last column of the row has been reached. Furthermore, we want the user to be able to use Tab to navigate between the columns:
For this we have the properties Grid.Keyboard.EnterKeyHandling and Grid.Keyboard.TabKeyHandling as seen in the screenshot above.
The only thing missing is the calculation. The grid offers the event OnGetCellData :
procedure TForm1.GridGetCellData(Sender: TObject; ACol, ARow: Integer; var CellString: string); var s, e : TDateTime; begin // only for column 3 // not for fixed row if ( ARow > 0 ) and (ACol = 3) then begin CellString := ''; // if date is valid proceed if TryStrToDate( Grid.Cells[ 1, ARow ], s ) then begin // if date is valid, calculate difference if TryStrToDate( Grid.Cells[ 2, ARow ], e ) then begin CellString := IntToStr( DaysBetween( e, s ) ); end; end; end; end;
The implementation is pretty straight forward. If there is no valid data, the column stays empty. Otherwise, we return the number of days in the provided variable named CellString .
Running the example yields a very comfortable user experience with the desired result: