6

I use FastReport and I need to preview/print Grids with more than 1000 rows and I have some performances problems. Typically I use TfrxCrossObject to prepare my grid because the end user may change the grid presentation (used columns, column's name, size) so I need to have a dynamical print. I tested a simple grid (16 cols x2000 rows) and it needs more than 10 seconds to present the first preview page. Any idea to improve performances ?

EDIT : As said in some answers, the problem is : how to create 'dynamicaly' a grid (with same columns names and sizes that I have on screen) in FastReport without using TFrxCrossObject which seems to be not very efficent. I may admit all solutions like using DataSet or enhancing TfrxCrossObject.

The test code :

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  frxClass, StdCtrls, Grids, frxCross;

type
  TForm1 = class(TForm)
    Button1: TButton;
    StringGrid1: TStringGrid;
    frxCrossObject1: TfrxCrossObject;
    frxReport1: TfrxReport;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure frxReport1BeforePrint(c: TfrxReportComponent);
  end;

var
  Form1: TForm1;

implementation
{$R *.DFM}

procedure TForm1.FormCreate(Sender: TObject);
var
  i, j: Integer;
begin
  for i := 1 to 16 do
    for j := 1 to 2000 do
      StringGrid1.Cells[i - 1, j - 1] := IntToStr(i * j);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  frxReport1.ShowReport;
end;

procedure TForm1.frxReport1BeforePrint(c: TfrxReportComponent);
var
  Cross: TfrxCrossView;
  i, j: Integer;
begin
  if c is TfrxCrossView then
  begin
    Cross := TfrxCrossView(c);
    for i := 1 to 16 do
      for j := 1 to 2000 do
        Cross.AddValue([i], [j], [StringGrid1.Cells[i - 1, j - 1]]);
  end;
end;
end.
Justmade
  • 1,496
  • 11
  • 16
philnext
  • 3,242
  • 5
  • 39
  • 62
  • 1
    I recommend posting this question on the FastReports forums. If you are using AQTime, you could use it to profile your application and at least see WHERE most of that time is going. AQtime is included on some editions of Delphi XE and XE2 so you might have it already. – Warren P Mar 10 '12 at 22:08
  • 3
    try to use ClientDataSet instead of `crossview`. – teran Mar 10 '12 at 23:27
  • 1
    Simplest way to test code performance is to use `GetTickCount` before and after parts of code and compare values. But first, try to use `BeginUpdate`/`EndUpdate` when filling up the StringGrid. – LightBulb Mar 10 '12 at 23:52
  • 1
    I've just tested your example and got 3-4 sec to get 1st page preview, and 9-10 seconds to build entire report. I think it is not bad for 112 pages report. (Q8200@2.3GHz, 4GB RAM) if I exchange `i` and `j` in `AddValue` (to make 16 cols and 2000 rows), and then decrease `i` to 15 (to fit the page), total time becomes about 5 seconds (45 pages) – teran Mar 11 '12 at 06:50
  • @WarrenP I used to post in FastReport forum but, recently, it is less efficient and spamed. And for perf. the problem is not in my code but in FR one, so perf. tools is not really usefull. – philnext Mar 11 '12 at 14:23
  • @Teran ClientDataSet is a good idea but as said I need to have dynamical grid and I don't know how dynamically change the columns with report with ClientDataSet. – philnext Mar 11 '12 at 14:30
  • @LightBulb The perf botleneck is not in my code but in FR one's. BeginUpdate/EndUpdate is a good remark but the pb is not in StringGrid fill. – philnext Mar 11 '12 at 14:32
  • I think my answer do provide a good performance upgrade for printing large grid using Fast Report. Please check if it suit your requirement and accept. Thanks – Justmade Mar 21 '12 at 01:56

2 Answers2

4

The CrossTab have many overhead. Here is a UserDataSet version :

  1. Just drop 1 stringgrid, 1 button, 1 frxReport, 1 frxUserDataSet in the form.

  2. Set the frxUserDataSet events, Form OnCreate and Buttom OnClick as below code.

  3. No need to Design Report or set any properties, All will be set or generated at run-time.

It seems it is faster than the cross-tab version but you need more coding and lost functionality of CrossObject.

Edit : Add some comments and fix PaperWidth mis-calculation.

Edit2 : Add a print-friendly version which split data into pages.

View Friendly version show in 1 single page as the stringgrid :

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, frxClass, Grids, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    StringGrid1: TStringGrid;
    frxReport1: TfrxReport;
    frxUserDataSet1: TfrxUserDataSet;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure frxUserDataSet1Next(Sender: TObject);
    procedure frxUserDataSet1GetValue(const VarName: string; var Value: Variant);
    procedure frxUserDataSet1CheckEOF(Sender: TObject; var Eof: Boolean);
    procedure frxUserDataSet1First(Sender: TObject);
  private
    X, Y, TCol, TRow : Integer;
    IsEof : Boolean;
    CW, CH, PF : Double;
    Page : TfrxReportPage;
    MDB : TfrxMasterData;
    Memo : TfrxMemoView;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}


procedure TForm1.Button1Click(Sender: TObject);
var
  BW : Double;
begin
  IsEof := False;
  Page.PaperWidth :=  CW * TCol + 20; // EndlessWidth seems not work with band column
  MDB.SetBounds(0,0, CW * PF * TCol, CH * PF);
  MDB.Columns := TCol;
  MDB.ColumnWidth := CW * PF;
  frxReport1.ShowReport;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i, j : Integer;
begin
  CW := 12; // Cell Width in mm
  CH := 5;  // Cell Height in mm
  PF := 3.7794; // Pixie Factor i.e. the conversion of mm to FR component measurement
  TCol := 2000; // Total Column
  TRow := 16; // Total Row

  for i := 1 to TRow do
    for j := 1 to TCol do
      StringGrid1.Cells[i - 1, j - 1] := IntToStr(i * j);

  frxUserDataSet1.Fields.Text := 'Data';
  frxReport1.Clear;
  frxReport1.DataSets.Add(frxUserDataSet1);
  Page := TfrxReportPage.Create(frxReport1);
  Page.CreateUniqueName;
  Page.TopMargin := 10;
  Page.BottomMargin := 10;
  Page.LeftMargin := 10;
  Page.RightMargin := 10;
  Page.EndlessHeight := True;
  Page.EndlessWidth := True;
  MDB := TfrxMasterData.Create(Page);
  MDB.DataSet := frxUserDataSet1;
  Memo := TfrxMemoView.Create(MDB);
  Memo.SetBounds(0,0,CW * PF,CH * PF);
  Memo.Memo.Text := '[frxUserDataSet1."Data"]';
  Memo.Frame.Typ := [ftLeft, ftRight, ftTop, ftBottom];
end;

procedure TForm1.frxUserDataSet1CheckEOF(Sender: TObject; var Eof: Boolean);
begin
  Eof := IsEof;
end;

procedure TForm1.frxUserDataSet1First(Sender: TObject);
begin
  X := 0;
  Y := 0;
end;

procedure TForm1.frxUserDataSet1GetValue(const VarName: string; var Value: Variant);
begin
  Value := StringGrid1.Cells[X,Y];
end;

procedure TForm1.frxUserDataSet1Next(Sender: TObject);
begin
  If Y = TCol - 1 then
  begin
    if X = TRow - 1 then
      IsEof := True;
    Inc(X);
    Y := 0;
  end
  else
    Inc(Y);
end;

end.

Print-friendly version which is a bit more complex and separate data in different pages for printing :

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, frxClass, Grids, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    StringGrid1: TStringGrid;
    frxReport1: TfrxReport;
    frxUserDataSet1: TfrxUserDataSet;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure frxUserDataSet1Next(Sender: TObject);
    procedure frxUserDataSet1GetValue(const VarName: string; var Value: Variant);
    procedure frxUserDataSet1CheckEOF(Sender: TObject; var Eof: Boolean);
    procedure frxUserDataSet1First(Sender: TObject);
  private
    X, Y, TCol, TRow, RPP, ColBreak : Integer;
    IsEof : Boolean;
    CW, CH, PF : Double;
    Page : TfrxReportPage;
    MDB : TfrxMasterData;
    Memo : TfrxMemoView;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses Math;

{$R *.dfm}


procedure TForm1.Button1Click(Sender: TObject);
var
  BW : Double;
begin
  IsEof := False;
  RPP := Ceil((Page.PaperHeight - Page.TopMargin - Page.BottomMargin) / CH) - 1; // Row per page
  ColBreak := RPP; // break to next column

  frxReport1.ShowReport;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i, j : Integer;
begin
  CW := 12; // Cell Width in mm
  CH := 5;  // Cell Height in mm
  PF := 3.7794; // Pixil Factor i.e. the conversion of mm to FR component measurement
  TCol := 2000; // Total Column
  TRow := 16; // Total Row

  for i := 1 to TRow do
    for j := 1 to TCol do
      StringGrid1.Cells[i - 1, j - 1] := IntToStr(i * j);

  frxUserDataSet1.Fields.Text := 'Data';
  frxReport1.Clear;
  frxReport1.DataSets.Add(frxUserDataSet1);
  Page := TfrxReportPage.Create(frxReport1);
  Page.CreateUniqueName;
  Page.TopMargin := 10;
  Page.BottomMargin := 10;
  Page.LeftMargin := 10;
  Page.RightMargin := 10;
  Page.Columns := Ceil(Page.PaperWidth / CW);
  MDB := TfrxMasterData.Create(Page);
  MDB.DataSet := frxUserDataSet1;
  MDB.SetBounds(0,0, CW * PF, CH * PF);
  Memo := TfrxMemoView.Create(MDB);
  Memo.Align := baClient;
  Memo.Memo.Text := '[frxUserDataSet1."Data"]';
  Memo.Frame.Typ := [ftLeft, ftRight, ftTop, ftBottom];
end;

procedure TForm1.frxUserDataSet1CheckEOF(Sender: TObject; var Eof: Boolean);
begin
  Eof := IsEof;
end;

procedure TForm1.frxUserDataSet1First(Sender: TObject);
begin
  X := 0;
  Y := 0;
end;

procedure TForm1.frxUserDataSet1GetValue(const VarName: string; var Value: Variant);
begin
  Value := StringGrid1.Cells[X,Y];
end;

procedure TForm1.frxUserDataSet1Next(Sender: TObject);
begin
  If X = TRow - 1 then
  begin
    if Y = TCol - 1 then
      IsEof := True
    else
    begin
      frxReport1.Engine.NewColumn;
      Inc(Y);
      X := ColBreak - RPP;
    end;
  end
  else if (X = ColBreak - 1) then
  begin
    if Y = TCol - 1 then
    begin
      frxReport1.Engine.NewPage;
      ColBreak := ColBreak + RPP;
      Y := 0;
    end
    else
      Inc(Y);
    frxReport1.Engine.NewColumn;
    X := ColBreak - RPP;
  end
  else
    Inc(X);
end;

end.
Justmade
  • 1,496
  • 11
  • 16
  • In my notebook computer (i5 2.4GHz, 4GRam), the original version show first page at 2.5 seconds and ready (all page loaded) at 9 seconds. My Display version (1 large pages) ready at 2.5 seconds. My Print version show first page instantly and ready (112 pages) at 1.5 seconds. 2000 Col X 200 Row show first page instantly and ready (448 pages) at 10.5 seconds. 2000 Col X 2000 Row show first page instantly and ready (4144 pages) at 104 seconds. – Justmade Mar 14 '12 at 06:31
  • @philnext If you are using DBGrid / DataSet, Y can change to field index and the Inc(X) change to DataSet.Next and you may need a Bookmark for column break. So in the getvalue event you can provide DataSet.Fields[Y].Value. – Justmade Mar 14 '12 at 09:18
  • @JustMade, if possible please take a look at FastReport problem I am facing with at [link](http://stackoverflow.com/questions/9649875/performances-with-fastreport-tfrxcrossobject-and-large-grids-1000-rows) – Ninad Avasare Oct 31 '13 at 19:52
2

Your piece of code correspond to the PrintStringGrid demo of FastReport slightly modified (RowCount=2000 instead of 16).

Using TStringGrid as a container is not a good idea if you have to deal with large data: It should only be used for presention concern.

Use an in-memory dataset (e.g. ClientDataset as teran suggested in the question comment thread), you can still present your large data to a TStringGrid if using it is compulsory but a TDBGrid is more appropriate.

Iterating a large TDataset is fast than doing the same with a mere TStringGrid.

The PrintTable demo of FastReport could serve as a starting point, adapting it is left to you as an exercise knowing that it uses the same components as PrintStringGrid demo:

  • A TfrxReport and
  • A TfrxCrossObject where the iteration takes place.
menjaraz
  • 7,551
  • 4
  • 41
  • 81
  • 1
    may I ask if the Clientdataset (grid) has currently 100 row * 100 col with data and the user want to add 1 more column, how to add the column to the Clientdataset? Also, how to add that column to the report? If using DBCrossTab, I am sure there will be no performance improvement because of the calculation and sorting overhead of CrossTab. – Justmade Mar 14 '12 at 06:11
  • Yes IRL, I use TDBGrid but TStringGrid was easier here to explain my problem and for answerers to test it. I have to same perf. problems with TDBGrid. – philnext Mar 14 '12 at 08:56