Delphi Programming

and software in general.

Monday, July 28, 2008

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.