TeeGrid’s TVirtualData class implements a virtual data pattern that decouples the grid’s rendering engine from the underlying data source. This design provides several key benefits:
Data Source Flexibility
Support any data source by implementing a single interface
Lazy Loading
Retrieve only visible cells on-demand for optimal performance
Large Datasets
Handle millions of rows without loading all data into memory
Framework Agnostic
Same data adapter works across VCL, FMX, and Lazarus
Generic adapter using RTTI for arrays, lists, and objects.Supported Types:
TArray<T> - Static arrays
TList<T> - Generic lists
TObjectList<T> - Object lists
Single records/objects
2D arrays (TArray<TArray<T>>)
RTTI-Based Column Creation:
procedure TVirtualDataRtti.AddColumns(const AColumns: TColumns);var RttiType: TRttiType; Member: TRttiMember; Column: TColumn;begin RttiType := Context.GetType(FTypeInfo); // Iterate fields and properties for Member in RttiType.GetFields + RttiType.GetProperties do if IsVisible(Member) then // Check visibility level begin Column := AColumns.Add; Column.Header.Text := Member.Name; LinkTo(Column, Member, TypeOf(Member)); end;end;
Value Access:
function TVirtualData<T>.AsString(const AColumn: TColumn; const ARow: Integer): String;var Value: TValue; Member: TRttiMember; P: Pointer;begin P := GetPointerOf(AColumn, ARow); // Get data pointer Member := MemberOf(AColumn); // Get RTTI member Value := ValueOf(Member, P); // Read value via RTTI Result := Value.ToString; // Convert to stringend;
unit MyVirtualData;interfaceuses Tee.GridData, Tee.Grid.Columns, Tee.Painter;type TMyVirtualData = class(TVirtualData) private FDataSource: TMyDataSource; protected function Empty: Boolean; override; function KnownCount: Boolean; override; public constructor Create(ADataSource: TMyDataSource); procedure AddColumns(const AColumns: TColumns); override; function AsString(const AColumn: TColumn; const ARow: Integer): String; override; function AutoWidth(const APainter: TPainter; const AColumn: TColumn): Single; override; function Count: Integer; override; procedure Load(const AColumns: TColumns); override; procedure SetValue(const AColumn: TColumn; const ARow: Integer; const AText: String); override; end;implementationconstructor TMyVirtualData.Create(ADataSource: TMyDataSource);begin inherited Create; FDataSource := ADataSource;end;function TMyVirtualData.Empty: Boolean;begin Result := (FDataSource = nil) or (FDataSource.RecordCount = 0);end;function TMyVirtualData.KnownCount: Boolean;begin // Return True if Count is known, False for streaming data Result := True;end;function TMyVirtualData.Count: Integer;begin if FDataSource = nil then Result := 0 else Result := FDataSource.RecordCount;end;function TMyVirtualData.AsString(const AColumn: TColumn; const ARow: Integer): String;var FieldName: string;begin // Extract field identifier from TagObject FieldName := TMyField(AColumn.TagObject).Name; // Retrieve value from data source Result := FDataSource.GetValue(FieldName, ARow);end;procedure TMyVirtualData.AddColumns(const AColumns: TColumns);var I: Integer; Col: TColumn; Field: TMyField;begin AColumns.Clear; for I := 0 to FDataSource.FieldCount - 1 do begin Field := FDataSource.Fields[I]; Col := AColumns.Add; Col.Header.Text := Field.DisplayName; Col.TagObject := Field; // Store field reference // Set alignment based on field type if Field.IsNumeric then begin Col.TextAlignment := TColumnTextAlign.Custom; Col.TextAlign.Horizontal := THorizontalAlign.Right; end; end;end;procedure TMyVirtualData.Load(const AColumns: TColumns);var I: Integer; Field: TMyField;begin // Associate existing columns with fields for I := 0 to AColumns.Count - 1 do begin Field := FDataSource.FindField(AColumns[I].Header.Text); if Field <> nil then AColumns[I].TagObject := Field; end;end;function TMyVirtualData.AutoWidth(const APainter: TPainter; const AColumn: TColumn): Single;var I: Integer; S: String; W: Single;begin Result := 0; // Sample first 100 rows for width calculation for I := 0 to Min(99, Count - 1) do begin S := AsString(AColumn, I); W := APainter.TextWidth(S); if W > Result then Result := W; end; // Add padding Result := Result + 20;end;procedure TMyVirtualData.SetValue(const AColumn: TColumn; const ARow: Integer; const AText: String);var Field: TMyField;begin Field := TMyField(AColumn.TagObject); FDataSource.SetValue(Field.Name, ARow, AText); // Notify grid of change Repaint;end;end.
AsString is called for every visible cell on every paint. Optimize it ruthlessly:
// BAD - slowfunction AsString(const AColumn: TColumn; const ARow: Integer): String;begin Result := Format('%.2f', [CalculateComplexValue(ARow)]);end;// GOOD - cache resultsfunction AsString(const AColumn: TColumn; const ARow: Integer): String;begin if not FCache.TryGetValue(ARow, Result) then begin Result := Format('%.2f', [CalculateComplexValue(ARow)]); FCache.Add(ARow, Result); end;end;
Cache the count if it’s expensive to calculate:
private FCount: Integer; FCounted: Boolean;function Count: Integer;begin if not FCounted then begin FCount := ExpensiveCountCalculation; FCounted := True; end; Result := FCount;end;procedure Refresh;begin FCounted := False; // Invalidate cache inherited;end;
Don’t scan all rows for width calculation:
function AutoWidth(const APainter: TPainter; const AColumn: TColumn): Single;const SampleSize = 100;var I, Step: Integer; S: String; W: Single;begin Result := 0; if Count <= SampleSize then Step := 1 else Step := Count div SampleSize; I := 0; while I < Count do begin S := AsString(AColumn, I); W := APainter.TextWidth(S); if W > Result then Result := W; Inc(I, Step); end;end;
// Built-in calculation supportvar Total: Double;begin Total := TeeGrid1.Data.Calculate(TeeGrid1.Columns[2], TColumnCalculation.Sum); Average := TeeGrid1.Data.Calculate(TeeGrid1.Columns[2], TColumnCalculation.Average);end;// Implement in your TVirtualData:function Calculate(const AColumn: TColumn; const ACalculation: TColumnCalculation): TFloat;var I: Integer; Value: TFloat;begin case ACalculation of TColumnCalculation.Sum: begin Result := 0; for I := 0 to Count - 1 do Result := Result + AsFloat(AColumn, I); end; // ... other calculations end;end;