Posts from February 2012

FireMonkey Grids: Basics, Custom Cells and Columns

MonkeyStyler uses a couple of heavily customised grid columns. I spent some of last week finishing off the styling for them. The whole process has taught me a lot about FireMonkey grids. Prompted by this question on StackOverflow I though now would be a good time to share some of what I have learnt.

So, in this post I’m going to cover the following:

  • How FireMonkey grids work;
  • How to create a custom cell class;
  • How to create a custom column class;

Our StackOverflow poster wanted to know how to set the alignment and color of a grid cell, so I’ll concentrate on those issues in this post. We’ll create an app which has a grid showing two columns. The first will display the row index, the second financial figures, which are right aligned and with a background which changes to red when the figure is negative. I’ll cover most of that today with the styling issues in a future post.

FireMonkey Grids

Grids in FireMonkey are much more flexible than those in the VCL. But, of course with flexibility comes complexity. Fortunately, once you’ve learnt a few basics the rest comes fairly easy.

A FireMonkey TGrid is simply a container, adapted to contain children of type TColumn. A column is simply a container for a series of cells. We’ll get to the cells later.

To start with we’ll create an app which contains a TGrid. In the Delphi form designer, right click on the empty grid and select the Element Editor. Here, add a TColumn and back out. This will be a basic column in which we’ll display the row index.

On to the code, here’s the interface:

type
  TForm1 
= class(TForm)
    
Grid1TGrid;
    
Column1TColumn;
    
procedure FormCreate(SenderTObject);
    
procedure FormDestroy(SenderTObject);
  private
  protected
    
ValueColumnTColumn;
    
DataTList<Single>;
    
procedure PopulateData;
  public
  
end


Data is a list which we populate with random values in PopulateData.

FormCreate sets up our Data values, and also creates and adds another TColumn to the grid. We’ll create this in code so it’s easier to play around with. Note that, as always in FireMonkey, we simply assign it’s Parent property to the grid to assign it to the appropriate container.

procedure TForm1.FormCreate(SenderTObject);
begin
  Data 
:= TList<Single>.Create;

  
PopulateData;
  
Grid1.RowCount := RowCount;

  
ValueColumn := TColumn.Create(Grid1);
  
ValueColumn.Parent := Grid1;
end

Go back to the form editor and create a method for the grids OnGetValue event:

procedure TForm1.Grid1GetValue(SenderTObject; const ColRowInteger;
  var 
ValueVariant);
begin
  
if Col 0 then
    Value 
:= Row
  
else if Col 1 then
    Value 
:= Data[Row];
end

What happens here? The TColumn has a number of cells to display data but it only creates as many cells as it has visible rows. I.e. if a grid has a RowCount of 100, but only 20 cells are visible then only 20 cells are created. Whenever you scroll the grid each cell gets recycled. Because of this a grid cell cannot store any data between refreshes. (Or, more accurately, it should not store data because if it does it will be displaying the wrong data).

So, every time a cell is displayed the grid calls the OnGetValue event for that cell. OnGetValue passes the column and row indexes for the cell (the virtual column and row indexes) and our event handler needs to return the value to display in the Value variant parameter.

Run the above and you’ll see something like:

(Aside: the grid appears to have a few display issues if you don’t show headers, hence why I’ve kept the headers here).

Custom Columns

Ignoring the rounding issues (we’re using Singles for simplicity here), you can see that the column of supposedly financial figures really needs to be right aligned (we’ll come to rounding and formating later).

We’re using a TColumn for the figures, which is a container for TTextCell. The definition for TTextCell is:

TTextCell = class(TEdit)
  
end


So, it’s just an edit control with the parent set to the column. Haha. Easy. To get our control right aligned we just need to set the TEdit’s TextAlign property, which we can assign when the cell is created.

Each cell is created in the columns CreateCellControl virtual method. Look for the method in TColumn and we see:

function TColumn.CreateCellControlTStyledControl;
begin
  Result 
:= TTextCell.Create(Self);
  
TTextCell(Result).OnTyping := DoTextChanged;
  
TTextCell(Result).OnExit := DoTextExit;
end

So, we’ll create our own custom column and override the CreateCellControl to do our work for us:

type TFinancialColumn = class(TColumn)
  protected
    function 
CreateCellControlTStyledControl;override;
  
end;
    
    ...
    
function 
TFinancialColumn.CreateCellControlTStyledControl;
begin
  Result 
:= inherited;
  
TEdit(Result).TextAlign := TTextAlign.taTrailing;
end

Update the FormCreate to create TFinancialColumn and we see:

Custom Cells

But we’ve now taken things as far as they can go with the built in TTextCell. It’s time to create our own cell class so we can really get things looking how we want.

Look again at the definition of TColumn.CreateCellControl. Note that it returns an object descending from TStyledControl. A TStyledControl is the parent of any control which can have styling applied. In other words, any visual control in FireMonkey. FireMonkey already does that with TCheckCell, TProgressCell and TImageCell for check boxes, progress bars and images.

And you can extend this to create custom cells using any control you like. You could go really freaky and use a list box or tree view within a cell. You probably wouldn’t want to, but FireMonkey is flexible enough to do it if you want. And all FireMonkey controls can own - be containers for - other controls. So you can create a cell which is made up of multiple controls.

Take a look at the property editor grid for MonkeyStyler in the screenshot below from the the preview images blog post and you’ll see that the values column contains:

  • A TColorBox for color previews (the Fill.Color property);
  • A TEdit (editable properties);
  • A TCheckBox (boolean properties);
  • A TButton (for the animation menu button and image)

with code to set each item’s Visible property depending on the data type.

TFinancialCell

But, enough showing off, lets get back to our financial column. The TTextCell is just a TEdit. Not much customisation we can do there. What we’ll do is create our cell as a TStyledControl which will contain an alClient aligned TEdit.

type TFinancialCell = class(TStyledControl)
  protected
    
FEditTEdit;
    
procedure SetData(const ValueVariant); override;
  public
    
constructor Create(AOwnerTComponent); override;
  
end;

...

constructor TFinancialCell.Create(AOwnerTComponent);
begin
  inherited
;
  
FEdit := TEdit.Create(Self);
  
FEdit.Parent := Self;
  
FEdit.Align := TAlignLayout.alClient;
  
FEdit.TextAlign := TTextAlign.taTrailing;
end

Now, look at the SetData method, which is the Setter for the Data property. Data is defined in TFMXObject, the parent of all FireMonkey objects. Earlier we saw how the grid fetches data for each cell by called the OnGetValue event, well after it’s fetched the data it passes it on to the cell objects Data property. So, we override the SetData method to get the data we need to display.

procedure TFinancialCell.SetData(const ValueVariant);
var 
FSingle;
begin
  inherited
;
  
:= Value;
  
FEdit.Data := Format('%m'[F]);
end

Before we can run things we need to update the columns CreateCellControl to reflect our new cell class:

function TFinancialColumn.CreateCellControlTStyledControl;
begin
  Result 
:= TFinancialCell.Create(Self);
  
TTextCell(Result).OnTyping := DoTextChanged;
  
TTextCell(Result).OnExit := DoTextExit;
end

Now things are looking much better:

Round up

So, we have a grid column which is formatting the data how we want. This article is already long enough, so I won’t bore you any longer, other than to say that I will return at some future point to show you how you can use styles to format the grid cells we created today.

Enjoy.

GridBasics.zip - Download the project sources.

Full source:

unit Main;

interface

uses
  System
.SysUtilsSystem.TypesSystem.UITypesSystem.ClassesSystem.Variants,
  
FMX.TypesFMX.ControlsFMX.FormsFMX.DialogsGenerics.Collections,
  
FMX.GridFMX.LayoutsFMX.EditFMX.ListBox;

type TFinancialCell = class(TStyledControl)
  protected
    
FEditTEdit;
    
procedure SetData(const ValueVariant); override;
  public
    
constructor Create(AOwnerTComponent); override;
  
end;

type TFinancialColumn = class(TColumn)
  protected
    function 
CreateCellControlTStyledControl;override;
  
end;

type
  TForm1 
= class(TForm)
    
Grid1TGrid;
    
Column1TColumn;
    
procedure FormCreate(SenderTObject);
    
procedure FormDestroy(SenderTObject);
    
procedure Grid1GetValue(SenderTObject; const ColRowInteger;
      var 
ValueVariant);
  private
  protected
    
ValueColumnTFinancialColumn;
    
DataTList<Single>;
    
procedure PopulateData;
  public
  
end;

var
  
Form1TForm1;

implementation

{$R 
*.fmx}

const RowCount 20;
{ TForm1 }

procedure TForm1
.FormCreate(SenderTObject);
begin
  Data 
:= TList<Single>.Create;

  
PopulateData;
  
Grid1.RowCount := RowCount;

  
ValueColumn := TFinancialColumn.Create(Grid1);
  
ValueColumn.Parent := Grid1;
end;

procedure TForm1.FormDestroy(SenderTObject);
begin
  Data
.Free;
end;

procedure TForm1.Grid1GetValue(SenderTObject; const ColRowInteger;
  var 
ValueVariant);
begin
  
if Col 0 then
    Value 
:= Row
  
else if Col 1 then
    Value 
:= Data[Row];
end;

procedure TForm1.PopulateData;
var 
IInteger;
begin
  
for := 1 to RowCount do
    
Data.Add((Random(10000)-1000)/100);
end;

{ TFinancialColumn }

function TFinancialColumn.CreateCellControlTStyledControl;
begin
  Result 
:= TFinancialCell.Create(Self);
  
TTextCell(Result).OnTyping := DoTextChanged;
  
TTextCell(Result).OnExit := DoTextExit;
end;

{ TFinancialCell }

constructor TFinancialCell
.Create(AOwnerTComponent);
begin
  inherited
;
  
FEdit := TEdit.Create(Self);
  
FEdit.Parent := Self;
  
FEdit.Align := TAlignLayout.alClient;
  
FEdit.TextAlign := TTextAlign.taTrailing;
end;

procedure TFinancialCell.SetData(const ValueVariant);
var 
FSingle;
begin
  inherited
;
  
:= Value;
  
FEdit.Data := Format('%m'[F]);
end;

end