Reusable Grid View – part 3

Let’s take a closer look at how to hijack a pristine TStringGrid and make it our playground without inheriting the grid directly. The VCL class is well designed for reuse and expose properties and event handlers that we easily can grab hold of. The OnDrawCell event is of particular interest to us. That’s a good spot to redefine how the grid retrieve and render it’s content.

You will be assimilated
This is pretty self explanatory, but what we basically do is hook the draw routine and set up the appropriate number of rows and columns, and their respective default widths.

procedure TGridViewController.Refresh;
ix : Integer;
if not Assigned(Grid)
then Exit;

if not IsEmpty
then begin // Set Visitor's drawing handler, row and column count
Grid.Enabled := True;
Grid.OnDrawCell := DrawCell;
Grid.RowCount := RowCount + FixedRows;
Grid.ColCount := Count;
for ix := 0 to Count - 1 // Set column widths
do begin
Grid.ColWidths[ix] := (Objects[ix] as TGridViewColumn).Width;
else begin // disable drawing handler, set rows/cols to 1 and fill in 1 "blank"
Grid.Enabled := False;
Grid.OnDrawCell := nil;
Grid.RowCount := 1;
Grid.ColCount := 1;
Grid.ColWidths[0] := 0;
Grid.Cells[0,0] := defaultEmptyCell;
if Grid.RowCount > 1
then begin // Reset header and Row positions in case empty grid overrode them.
Grid.FixedRows := FixedRows;
Grid.Row := FixedRows;
Grid.Invalidate; // ensure that the grid is refreshed

So this is where all the action is… err… actually, not much happens here. Instead we delegate the actual rendering to the column instance.

procedure TGridViewController.DrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
(Objects[aCol] as TGridViewColumn).DrawCell(Grid.Canvas, aRow, Rect, State);

The column instance then again divide the draw into two sections. The outer DrawCell does basic housekeeping of canvas color settings and a little color trickery (a feature that snuck in while I was having fun – my bad).

procedure TGridViewColumn.DrawCell(Canvas: TCanvas; aRow: Integer; Rect: TRect;
State: TGridDrawState);
FG, ofg,
BG, obg : TColor;
with Canvas
do begin

if (State = []) // Not focused/selected/etc
then begin
FG := ofg;
BG := obg;

if aRow > 0 // Not a title line
then begin
ForeColor(aRow, FG);
BackColor(aRow, BG);

if ofg FG then Font.Color:=FG;
if obg BG then Brush.Color:=BG;

DrawCellInner(Canvas, aRow - Controller.FixedRows, Rect, State); // The business happens here

if (State = [])
then begin
if ofg FG then Font.Color:=ofg;
if obg BG then Brush.Color:=obg;

The actual content rendering happens in DrawCellInner. Note that we do an adjustment related to grid layout here. If you look at the line where DrawCellInner is called, we subtract FixedRows from aRow, ensuring that our data is zero offset based.

procedure TGridViewColumn.DrawCellInner(Canvas: TCanvas; aRow: Integer;
Rect: TRect; State: TGridDrawState);
CellText : String;
w : Integer; // String width
txtAdj : Integer; // old adjust mode
if aRow < 0
then CellText := Title
else CellText := FormattedText(aRow);
with Canvas
do begin
case FAlign of
taCenter : begin
if (w>(Rect.Right-Rect.Left))
then w := 2
else w := Round(((Rect.Right - Rect.Left) - w) / 2);
TextRect(Rect, Rect.Left + w,Rect.Top + 1, CellText);
taRightJustify : begin
txtAdj:=SetTextAlign(Handle, ta_Top or ta_Right);
TextRect(Rect, Rect.Right - 3,Rect.Top + 1, CellText);
SetTextAlign(Handle, txtAdj);
else // taLeftJustify
TextRect(Rect, Rect.Left+2,Rect.Top+1, CellText);

In the inner draw routine we first figure out if we are drawing the title row or a data row. If it is a data row, we call the TGridViewColumn.FormattedText method.

function TGridViewTextColumn.FormattedText(aRow: Integer): String;
Result := GetTextMethod(aRow);

Does GetTextMethod look familiar? It is the procedure variable which holds the method we implement in our descendant viewcontroller class and pass on when we set up the column. That is the method which actually retrieve the data from the underlying data structure as described in part 2 (GetNumber, GetNumberSquared).

The rest of DrawCellInner measures the retrieved text and ensure it is adjusted as desired and rendered within the cell region.

Magic Colors
Back to ForeColor and BackColor in the outer draw. This is the closest we come to bells and whistles. To make the grid look more sophisticated, I added support for overriding the colors as well as a very simple default rowshading algorithm.

procedure TGridViewColumn.ForeColor(aRow: Integer; var Color: TColor);
if Assigned(FGetForeColorMethod)
then GetForeColorMethod(aRow, Color)
else if DefaultForeColor clNone
then Color := DefaultForeColor;

procedure TGridViewColumn.BackColor(aRow: Integer; var Color: TColor);
if Assigned(FGetBackColorMethod)
then GetBackColorMethod(aRow, Color)
else if DefaultBackColor clNone
then Color := DefaultBackColor;

if Assigned(FGetShadeColorMethod)
then GetShadeColorMethod(aRow, Color);

Both these methods start in the same way. The default color have been retrieved from the grid itself in the outer draw. First we check if a color method have been plugged in, if not – we check if a column color override has been set.

For the background color routine, there is another feature – a pluggable shading method. The default row shader looks like this…

procedure TGridViewController.DefaultRowShader(const aRow:Integer; var Color:TColor);
if ShadeRows and (not Odd(aRow))
then begin
Color := Graphics.ColorToRGB(Color); // Translate theme/system colors to RGB
if ((Color and $FFFFFF) > $888888) // Lazy man's luminosity adjustment
then Color := Color - $080808
else Color := Color + $080808;

Too much flexibility? Well, maybe I broke the KISS rule, but no rule without exceptions šŸ™‚ Now I can have static or procedural color adjustments per row as well as per column.

Next: Reusable Grid goes code complete and come with examples.

Stay tuned.

Leave a Reply

Please log in using one of these methods to post your comment: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.