Rave Reports - raportowanie dynamiczne inaczej

Juhas

Cześć. Używając RaveReports dostrzegłem, że tworzenie raportów za pomocą ich edytora jest bardzo mozolne i denerwujące. Czasami nawet nie działa. Zwłaszcza jest to denerwujące, jak trzeba stworzyć wiele raportów do aplikacji. Pomyślałem o tym, żeby tworzyć je dynamicznie. Okazało się, że można i faktycznie tworzenie dynamiczne jest dużo prostsze i szybsze. Opiera się właściwie na napisaniu zapytania i wywołaniu jednej funkcji.
Jednakże mój kod wymaga kilku rzeczy:

Wymagania:

#Forma bazowa o nazwie np: TBaseRaportForm. Na formie następujące komponenty:
*BaseRVSystem: TRvSystem
*BaseRVProject: TRVProject
*BaseRVDataset: TRVDatasetConnection
*BaseQuery: TIBQuery(lub inny tego typu komponent, np. TADOQuery)
*BaseTrans: TIBTransaction(jeśli używamy TIBQuery)
wszystkie te komponenty powinniśmy znaleźć w standardzie Delphi z zainstalowanym RaveReports
#Na formie 3 przyciski: "Podgląd raportu", "Drukuj raport" i "Zamknij"
#Pusty plik raportu, tzn. muszą na nim być:
*Region1: Region Component
*Band1: Band Component

Ustawienia komponentów:
Właściwie wg własnych potrzeb ;) Ważne, żeby właściwość BaseRVDataset.Dataset wskazywała na nasze Query(lub analogiczny komponent)

Plik raportu
W tym pliku na Band1 możemy sobie położyć to, co chcemy. Ja mam text component z tytułem i kilka DataText componentów, które będą robić za parametry
(jeśli ktoś nie wie, jak się robi parametry w RaveReports i do czego służą to odsyłam do jakiegoś helpa, czy czegoś takiego.)

Możemy do formy oczywiście powsadzać sobie jeszcze jakieś filtry np. do dat i.t.d.

Rzeczą kolejną jest kod :)

Tworzymy sobie typ(ja go mam w oddzielnym unicie) TReportColumn:

type
  TReportColumn = record
    cCaption: string;
    cWidth: Double;
    cLeft: Double;
    FieldName: string;
    cAlignment: TPrintJustify;
end;

Mały opis:
cCaption - tytuł nagłówka
cWidth - szerokość kolumny(W Rave podawana w calach)
cLeft - położenie X
FieldName - nazwa pola, do którego się odnosi kolumna
cAlignment - położenie tekstu względem kolumny

OK, teraz w sekcji private tworzymy sobie kilka zmiennych:

    RepCol: array of TReportColumn;
    FColWidthSum: Double;

Zmienna FColWidthSum będzie przechowywała łączną szerokość zdefiniowanych kolumn. Potrzebne do ustawienia szerokości poziomych linii.

W tej samej sekcji dodajemy jeszcze kilka metod:

    function CheckData: boolean;
    procedure LoadReportFile(ReportFileName: string);
    procedure BuildReport;

Funkcja CheckData sprawdza, czy są jakieś dane:

function TBaseRaportForm.CheckData: boolean;
begin
  if not BaseQuery.Active then BaseQuery.Open;
  if BaseQuery.RecordCount = 0 then
  begin
    application.MessageBox('Brak danych dla wybranych filtrów', '', mb_OK+mb_IconInformation);
    result:=false;
  end else result:=true;

  if BaseQuery.Active then BaseQuery.Close;
end;

Procedura LoadReportFile oczywiście wczytuje przygotowany wcześniej pusty plik raportu:

procedure TBaseRaportForm.LoadReportFile(ReportFileName: string);
begin
  BaseRVProject.ProjectFile:=ReportFileName;
end;

Tutaj można dać oczywiście jeszcze jakieś sprawdzenie i inne tego typu rzeczy.
Następnie procedura BuildReport - jest to metoda właściwie najważniejsza, ponieważ to ona odpowiada za budowanie dynamiczne raportu. Znalazłem ją na stronie Rave'a i trochę przerobiłem(nie będę jej dokładnie omawiał):

procedure TBaseRaportForm.BuildReport;
Var
  MyBand: TRaveBand;
  MyDataBand: TRaveDataBand;
  MyDataCnx: TRaveDataConnection;
  MyDataText: TRaveDataText;
  MyDataView: TRaveDataView;
  MyLine: TRaveHLine;
  MyVLine: TRaveVLine;
  MyPage: TRavePage;
  MyRegion: TRaveRegion;
  MyText: TRaveText;
  nMarginBottom, nMarginLeft, nMarginRight, nMarginTop: Double;
  i: integer;
  lName: integer;
Begin
  if length(RepCol) = 0 then exit;
  BaseRvProject.Open;

  // 1 - use a blank page "Page 1 in Report 1" ======================
  MyPage := BaseRvProject.ProjMan.FindRaveComponent('Report1.Page1', Nil) As TRavePage;
  if myPage = nil then raise Exception.Create('Nie utworzono odpowiedniej strony!');
  MyPage.WasteFit := True;

  // Set all margins to “waste” or 0.5 which ever is greater
  nMarginBottom := MaxValue([RpDev.Waste.Bottom / RpDev.YDPI, 0.5]);
  nMarginLeft := MaxValue([RpDev.Waste.Left / RpDev.XDPI, 0.5]);
  nMarginRight := MaxValue([RpDev.Waste.Right / RpDev.XDPI, 0.5]);
  nMarginTop := MaxValue([RpDev.Waste.Top / RpDev.YDPI, 0.5]);

  // 2 - Create DataView using Connection ===========================
  MyDataCnx := CreateDataCon(BaseRvDataset);
  MyDataView := BaseRvProject.ProjMan.NewDataObject(TRaveDataView) As TRaveDataView;
  MyDataView.ConnectionName := MyDataCnx.Name;
  MyDataView.DataCon := MyDataCnx;
  CreateFields(MyDataView, Nil, Nil, true);

  // 2a - place “Page Number” directly ON the page NOT a band
  MyDataText := MyPage.CreateChild(TRaveDataText, 'dtPage') As TRaveDataText;
  MyDataText.DataField := '"Strona " + Report.CurrentPage + " z " + Report.TotalPages';
  MyDataText.FontJustify := pjRight;
  MyDataText.Width := 2; // Width MUST be before LEFT assignment
  MyDataText.Left := MyPage.PageWidth - nMarginRight - MyDataText.Width;
  MyDataText.Top := nMarginTop;

  // 3 - create a region and set it's properties ====================
  MyRegion:=(BaseRVProject.ProjMan.FindRaveComponent('Region1', MyPage) as TRaveRegion);
  if MyRegion = nil then raise Exception.Create('Brak odpowiedniego regionu!');
  MyRegion.Top := nMarginTop + 0.4; // Top MUST be before HEIGHT assignment
  MyRegion.Width := MyPage.PageWidth - (nMarginLeft + nMarginRight);
  MyRegion.Height := MyPage.PageHeight - (MyRegion.Top + nMarginBottom);

  // 4 - Create Databand named "DetailBand" First (controller) ======
  MyDataBand := MyRegion.CreateChild(TRaveDataBAnd, 'DetailBand') As TRaveDataBand;
  MyDataBand.DataView := MyDataView; // Always set the band dataview property

  // 4a - Place DataText components in "DetailBand" just created
  MyDataText := MyDataBand.CreateChild(TRaveDataText, 'dtName') As TRaveDataText;

  lName:=1;
  MyVLine := MyDataBand.CreateChild(TRaveVLine, 'VLine'+intToStr(lName)) As TRaveVLine;
  MyVLine.Left:=0;
  MyVLine.Height:=myDataBand.Height;
  lName:=2;

//tworzenie kolumn z zawartością (detail)
  for i:=low(RepCol) to high(RepCol) do
  begin
    MyDataText := MyDataBand.CreateChild(TRaveDataText, 'dt'+RepCol[i].FieldName) As TRaveDataText;
    MyDataText.DataField := RepCol[i].FieldName;
    MyDataText.DataView := MyDataView;
    MyDataText.FontJustify := RepCol[i].cAlignment;
    MyDataText.Left := RepCol[i].cLeft;
    MyDataText.Top := 0.01;
    MyDataText.Width := RepCol[i].cWidth;

    MyVLine := MyDataBand.CreateChild(TRaveVLine, 'VLine'+intToStr(lName)) As TRaveVLine;
    MyVLine.Left:=RepCol[i].cLeft+RepCol[i].cWidth;
    MyVLine.Height:=MyDataBand.Height;
    lName:=lName+1;

    MyLine := MyDataBand.CreateChild(TRaveHLine, 'HLine'+intToStr(lName)) As TRaveHLine;
    MyLine.Left:=0;
    MyLine.Width:=FColWidthSum;
    MyLine.Top:=MyDataBand.Bottom;
    lName:=lName+1;
  end;

  // 5 - Create Header Band controlled by "DetailBand" ==============
  MyBand := MyRegion.CreateChild(TRaveBand, 'HeaderBand') As TRaveBand;

  //      BandStyle Print Location codes
  // BandStyle PrintLoc b plBodyFooter
  // BandStyle PrintLoc g plGroupFooter
  // BandStyle PrintLoc r plRowFooter
  // BandStyle Printloc D plDetail
  // BandStyle Printloc R plRowHeader
  // BandStyle Printloc G plGroupHeader
  // BandStyle Printloc B plBodyHeader
  MyBand.BandStyle.PrintLoc := MyBand.BandStyle.PrintLoc + [plBodyHeader];

  //      BandStyle Print Occurrence codes
  // BandStyle PrintOcc C poNewColumn
  // BandStyle PrintOcc P poNewPage
  // BandStyle PrintOcc 1 poFirst
  MyBand.BandStyle.PrintOcc := MyBand.BandStyle.PrintOcc + [poFirst, poNewPage];

  MyBand.ControllerBand := MyDataBand;
  MyBand.Height := 0.2;
  MyBand.Left := MyRegion.Left;
  MyBand.MoveBehind; // Looks better when you "view it"
  MyBand.Top := MyRegion.Top;
  MyBand.Width := MyRegion.Width;

  MyLine:=MyBand.CreateChild(TRaveHLine, 'HLine'+intToStr(lName)) as TRaveHLine;
  MyLine.Left:=0;
  MyLine.Width:=FColWidthSum;
  MyLine.Top:=0;

  lName:=lName+1;
  MyVLine := MyBand.CreateChild(TRaveVLine, 'VLine'+intToStr(lName)) As TRaveVLine;
  MyVLine.Left:=0;
  MyVLine.Height:=MyBand.Height;
  lName:=lName+1;

//nagłówki dla ww kolumn
  for i:=low(RepCol) to high(RepCol) do
  begin
    MyText := MyBand.CreateChild(TRaveText, 'Text'+RepCol[i].FieldName) As TRaveText;
    MyText.Left := RepCol[i].cLeft;
    MyText.Top := 0.01; // Band.Top ???
    MyText.Font.Size := 10;
    MyText.Font.Style := MyText.Font.Style + [fsBold];
    MyText.FontJustify := pjCenter;
    MyText.Text :=RepCol[i].cCaption;
    MyText.Width :=RepCol[i].cWidth;

    MyVLine := MyBand.CreateChild(TRaveVLine, 'VLine'+intToStr(lName)) As TRavevLine;
    MyVLine.Left:=RepCol[i].cLeft+RepCol[i].cWidth;
    MyVLine.Height:=MyBand.Height;
    lName:=lName+1;
  end;

  // 5d - draw a horizontal line
  MyLine := MyBand.CreateChild(TRaveHLine, 'BottomLine') As TRaveHLine;
  MyLine.Left := 0; // MyBand.Left;
  MyLine.LineWidth := 2;
  //MyLine.LineWidthType := wtPoints;
  MyLine.Top := MyBand.Height - 0.01;
  MyLine.Width := FColWidthSum;
end;

Następnym krokiem jest utworzenie w sekcji protected (na dobrą sprawę public też może być) 3 kolejnych metod:

    procedure BuildQuery; virtual;
    procedure BuildColumns; virtual;
    procedure CreateColumn(cCaption, FieldName: string; cWidth: Double; cAlignment: TPrintJustify = pjCenter);

2 pierwsze będziemy wykorzystywali później. Jak się można domyśleć tutaj mają puste ciało.

Natomiast procedura CreateColumn wygląda tak:

  procedure TBaseRaportForm.CreateColumn(cCaption, FieldName: string; cWidth: double; cAlignment: TPrintJustify = pjCenter);
begin
  setLength(RepCol, length(RepCol)+1);
  RepCol[high(RepCol)].cCaption:=cCaption;
  RepCol[high(RepCol)].cWidth:=cWidth;
  RepCol[high(RepCol)].FieldName:=FieldName;
  RepCol[high(RepCol)].cLeft:=FColWidthSum;
  RepCol[high(RepCol)].cAlignment:=cAlignment;
  FColWidthSum:=FColWidthSum+cWidth;
end;

Teraz musimy uzupełnić kod do przycisków.
Najpierw Podgląd wydruku
(Właściwie to podgląd i drukowanie można umieścić jakoś w jednej procedurze, nie zrobiłem tego, specjalnie)

Screen.Cursor:=crSQLWait;
  try
    //tutaj można dać jeszcze jakieś metody robiące coś np. z parametrami
    BuildQuery;
    if not CheckData then exit;
    RepCol:=nil;
    FColWidthSum:=0;
    BuildColumns;
    BuildReport;
    Screen.Cursor:=crDefault;
    BaseRVProject.Execute;
    BaseRVProject.Close;
  finally
    Screen.Cursor:=crDefault;
  end;    

Kod do drukowania wygląda prawie tak samo:

  Screen.Cursor:=crSQLWait;
  //tutaj można dać jakiś kod
  BuildQuery;
  if not CheckData then exit;
  RepCol:=nil;
  FColWidthSum:=0;
  BuildColumns;
  BuildReport;
  BaseRVSystem.defaultDest:=rdPrinter;
  Screen.Cursor:=crDefault;
  BaseRVProject.Execute;

To właściwie wszystko, co trzeba zrobić w tej formie, żeby to śmigało. A teraz jak tego użyć.

<font size="3">Użyj mnie!</span>
No skoro mamy już formę bazową, to na jej podstawie tworzymy nową formę(trzeba z niej dziedziczyć).

Następnie musimy zrobić 2 rzeczy, a mianowicie przeładować procedury wirtualne, czyli BuildQuery i BuildColumns.
BuildQuery może np. wyglądać tak:

procedure TRapAForm.BuildQuery;
var
  myQuery: string;
begin
  inherited;
  myQuery:='SELECT kol1, kol2 FROM tabela';
  BaseQuery.SQL.Text:=myQuery;
end;

W tej procedurze musimy po prostu zbudować zapytanie i wrzucić je do BaseQuery.
Możemy również zmienić parametry w raporcie, np:

  BaseRVProject.SetParam('Wydrukowal', 'Wydrukował: Zdichu Liter');

Następnie procedura BuildColumns, która tworzy nam kolumny:

procedure TRapAForm.BuildColumns;
begin
  inherited;
  CreateColumn('Kolumna 1', 'kol1', 1.6);
  CreateColumn('Inna kolumna', 'kol2', 1.5);
end;

W tej procedurze po prostu musimy podać jakby definicje kolumn.

To by było na tyle.
Dużo szybciej jest przeładować 2 proste metody niż projektować cały raport ręcznie. Mam nadzieję, że kod będzie Wam służył długo :)
Jeśli macie jakieś pytania to pytajcie.

Autor: Juhas

7 komentarzy

uses
RpCon, RpConDS, RpDevice,
RvClass , RvCsData, RvCsDraw, RvCsStd, RvCsRpt, RvData, RvDefine, RvDirectDataView, RvUtil;

Takie coś załatwiło sprawę - konkretnie RvCsData, ale profilaktycznie wrzuciłem wszystko do IMPLEMENTATION

I oczywiście prośba - jaki unit dodać do USES by to mogło pójść?
I drugie pytanie - czy istnieje możliwość z poziomu programu zmiany np . położenia pola tupu TDATATEXT?
Czyli - tworzę raport ręcznie w RAVIE i ZANIM wykonam execute w programie chce poprzestawiać pewne elementy - jak to zrobić?
Jest gdzies HELP w necie dotyczący tego problemu (tzn tutorial do rave'a)?

Wszystko pięknie, ale jeden problem - TRaveDataText nie jest znanym typem i nie mogę znaleźć, w którym jest unicie.....
Niestety powyższy kod jest niekompilowalny (delphi XE proffesional)

Master - detail, chodzi np:
Master1
Detail1
Detail2
Detail3
Master2
....

Jeśli tak, to nie robiłem. A kodu unitu nie zamieszczę, bo w moim oryginale jest jeszcze kupa innych rzeczy, i usuwać tego wszystkiego mi się nie chce ;)

Jeszcze jak byś mógł dać przykład raportu master-detail

Podaj kod całego unita to będzie łatwiej analizować twój artykuł