Artykuł techniczny

Eksportowanie obiektów TDataSet i raportów do formatu PDF w środowisku Delphi

Aby wyeksportować obiekt TDataSet lub raport do formatu PDF w środowisku Delphi, losLab PDF Library oferuje dwie ścieżki. PDFlibTableExport dostosowuje dowolny TDataSet — zapytanie FireDAC, ClientDataSet, tabelę w pamięci — do postaci tabeli PDF podzielonej na strony, a trzy mostki w katalogu Addons przekazują przygotowany raport FastReport, QuickReport lub ReportBuilder do tego samego modułu zapisu PDF. Obie ścieżki generują prawdziwy plik PDF bez sterownika drukarki i bez widocznego okna

Te dwa problemy wyglądają podobnie, lecz w rzeczywistości są zupełnie inne. DBGrid lub surowy wynik zapytania nie posiadają własnego układu, więc wyeksportowanie ich oznacza konieczność zaprojektowania takiego układu od zera: kolumn, nagłówków, podziałów stron. Dokument FastReport lub ReportBuilder zawiera już zaprojektowany układ, więc wyeksportowanie go polega na wiernym odtworzeniu poleceń rysowania w przestrzeni PDF. Biblioteka losLab PDF Library utrzymuje te funkcjonalności w osobnych modułach właśnie dlatego, że ich tryby błędów się różnią, a w dalszej części tego artykułu omówimy każdy z nich i wskażemy, w jakich sytuacjach przestają być niezawodne

Jak wyeksportować obiekt TDataSet do formatu PDF w środowisku Delphi?

Komponent PDFlibTableExport zamienia zestaw danych w tabelę za pomocą jednego wywołania. Eksporter jednokrotnie przechodzi przez listę pól, automatycznie pomijając binarne pola typu blob — ftBlob, ftGraphic, ftBytes i im pokrewne nie zawierają przydatnego tekstu komórek — wyrównuje do prawej kolumny numeryczne oraz nanosi opcjonalne pasy zebry na ostylowany nagłówek. Pod maską tworzy siatkę za pomocą CreateTable, wypełnia komórki za pomocą SetTableCellContent i renderuje za pomocą DrawTableRows, czyli korzysta z tego samego publicznego interfejsu Table API, którym można sterować ręcznie. Wygodne opakowanie PDFlibExportDataSet automatycznie konfiguruje milimetry oraz punkt początkowy w lewym górnym rogu, dzięki czemu wywołujący musi jedynie dostarczyć stronę

uses
  Data.DB, FireDAC.Comp.Client, FireDAC.Stan.StorageBin,
  PDFlibrary, PDFlibTableExport;

var
  MemTable: TFDMemTable;
  PDF: TPDFlib;
  Options: TPDFlibTableExportOptions;
begin
  MemTable := TFDMemTable.Create(nil);
  PDF := TPDFlib.Create;
  try
    MemTable.LoadFromFile('customer.FDS');   // any TDataSet works here

    PDF.SetOrigin(1);
    PDF.SetMeasurementUnits(1);              // millimetres
    PDF.SetPageSize('A4');
    PDF.AddStandardFont(4);

    Options := DefaultTableExportOptions;
    Options.Title := 'Customers';
    Options.ColumnWidth := 32;               // 11 fields fit on A4 at 32 mm
    Options.RepeatHeader := True;            // redraw the header on each page

    PDFlibExportDataSet(PDF, MemTable, 'customers.pdf', Options);
  finally
    PDF.Free;
    MemTable.Free;
  end;
end;

Automatyczne pomijanie pól blob to zachowanie domyślne, a no-go dla elastyczności. Gdy potrzebujesz kontroli nad poszczególnymi polami — własnej etykiety kolumny, mniejszej szerokości lub odrzucenia wewnętrznej kolumny klucza, która nie jest obiektem blob — utwórz bezpośrednio obiekt TPDFlibTableExporter i podepnij się pod jego zdarzenie OnFieldFilter, które jest wyzwalane raz dla każdego pola i przekazuje modyfikowalną specyfikację. Ustaw właściwość Include na wartość False, aby odrzucić pole, lub ustaw ColumnWidth i DisplayLabel, aby nadpisać wartości domyślne. To zdarzenie jest również miejscem, w którym wyklucza się szerokie pola tekstowe (memo), aby nie rozciągały niepotrzebnie strony

Stronicowanie: przekazywanie stanu rysowania między stronami

Funkcja DrawTableRows przechowuje stan stronicowania, a zrozumienie jej działania to klucz do sukcesu. Wywołuje się ją, podając pierwszy i ostatni wiersz; przekazanie ostatniego wiersza o wartości mniejszej niż 1 oznacza rysowanie do końca tabeli, ograniczone podaną wysokością. Funkcja zwraca wysokość, jaką faktycznie narysowała, a GetTableLastDrawnRow zwraca ostatni wiersz, który się zmieścił. Ta para wartości stanowi stan, który przekazuje się z jednej strony na drugą: gdy ostatnio narysowany wiersz nie jest wierszem końcowym, otwiera się nową stronę i wznawia rysowanie od kolejnego wiersza. Nic nie jest mierzone od nowa — kontynuacja zaczyna się dokładnie w miejscu, w którym zakończyło się poprzednie wywołanie

// How the exporter continues a long table across pages
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // Last row = 0 means "draw to the end", bounded by DrawHeight
  PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
  LastDrawn := PDF.GetTableLastDrawnRow(TableID);
  if LastDrawn >= TotalRows then
    Break;                       // the whole table fit on this page
  if LastDrawn < Row then
    Break;                       // safety: no forward progress, bail out
  PDF.NewPage;
  Row := LastDrawn + 1;          // continue from the first undrawn row
  Y := PageHeight - Options.Top;
end;

W tej pętli obsługiwany jest także powtarzany nagłówek. Na każdej kolejnej stronie eksporter może opcjonalnie narysować wiersz 1 — czyli nagłówek — przed wierszami danych i wykorzystuje wysokość zwróconą przez to pierwsze wywołanie DrawTableRows, aby odpowiednio przesunąć treść poniżej. Właśnie dlatego DrawTableRows zwraca wysokość, a nie współrzędną następnego wiersza: ta wartość zwrotna pozwala ułożyć powtórzony nagłówek i dalszą część tabeli bez konieczności sztywnego określania rozmiaru któregokolwiek z tych elementów

Jak wyeksportować raport FastReport lub ReportBuilder do formatu PDF?

Mostki raportów przyjmują odwrotne podejście: nigdy nie tworzą własnego układu, lecz odtwarzają istniejący. Każdy mostek podłącza się do silnika raportów w jego natywnym punkcie eksportu. Klasa PDFlibFRExport dziedziczy po TfrxCustomExportFilter i odbiera obiekty FastReport takie jak memo, obrazy, kształty i linie, tłumacząc każdy z nich na elementy pierwotne biblioteki PDFlibPas. PDFlibRBDevice dziedziczy po TppFileDevice i przechodzi przez listę DrawCommand silnika ReportBuilder. Klasa PDFlibQRExport korzysta z zupełnie innej, trzeciej ścieżki, którą opisujemy w następnej sekcji. Wszystkie trzy działają w trybie bezgłowym (headless), co jest głównym powodem ich użycia na serwerze

Działanie w trybie bezgłowym niesie ze sobą pewne wyzwania, a FastReport jest tu klasycznym przykładem ostrzegawczym. Metoda TfrxReport.Export przechodzi przez strony podglądu, które sprawdzają właściwość ShowDialog filtra eksportu, a ta właściwość domyślnie przyjmuje wartość True. Na maszynie bez interaktywnego pulpitu okno modalne zwraca wynik anulowania i eksport po cichu kończy się niepowodzeniem. Ustaw właściwość ShowDialog na False przed wywołaniem Export, a raport wygeneruje się bez wyświetlania komunikatów. ReportBuilder ma analogiczne przełączniki — AllowPrintToFile oraz ShowPrintDialog — natomiast QuickReport, który przetwarza wszystko za pomocą metody Prepare, w ogóle nie wymaga wyciszania okien dialogowych

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // build the pages first
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // headless: skip the modal, no cancel
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

Trzy silniki, trzy układy współrzędnych

Każdy mostek wykonuje własne obliczenia współrzędnych, ponieważ każdy silnik mierzy przestrzeń w inny sposób. Eksporter PDFlibFRExport traktuje pozycje obiektów FastReport jako piksele przy rozdzielczości 96 PPI i mnoży je przez współczynnik 96/25.4, aby uzyskać milimetry. Klasa PDFlibRBDevice odczytuje polecenia rysowania ReportBuilder w tysięcznych częściach milimetra i dzieli je przez 1000, a także rejestruje się poprzez ppRegisterDevice, więc ustawienie właściwości ppReport.DeviceType na wartość 'PDFlibPas' i wywołanie metody Print wystarczy, aby skierować wyjście do tego mostka. PDFlibQRExport w ogóle nie wykonuje obliczeń współrzędnych: przygotowana strona QuickReport jest już plikiem EMF, więc mostek zapisuje każdą stronę do strumienia i przekazuje do metody ImportEMFFromStream, pozwalając, aby biblioteczna ścieżka renderowania kontekstu urządzenia i plików EMF umieściła każdy glif i linię. Z kolei mostki FastReport i ReportBuilder generują rodzime wektorowe elementy rysunkowe oraz natywny tekst

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // Prepares the report and appends every page in one call; the bridge
    // turns each page's EMF into PDF through ImportEMFFromStream.
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

Gdzie kończy się wierność odwzorowania

Poznaj ograniczenia tych mostków przed wdrożeniem ich w produkcyjnych przepływach pracy. Eksporter zestawów danych po cichu pomija kolumny binarne, więc raport, który musi wyświetlać osadzony obraz, wymaga innego podejścia niż zwykła tabela. Ścieżka QuickReport, z uwagi na to, że korzysta z plików EMF, zamienia tekst w glify wektorowe — strona wygląda prawidłowo, ale nie zawiera tekstu, który można zaznaczyć lub przeszukać, ani struktury znaczników PDF (tagged PDF) na potrzeby ułatwień dostępu. Mostki FastReport i ReportBuilder zachowują prawdziwy tekst, ale ich dedykowane procedury obsługi obejmują tylko typowe elementy — memo, obrazy, kształty, linie — i w przypadku bardziej egzotycznych obiektów rysują jedynie ramkę ograniczającą lub je pomijają, dlatego raport mocno opierający się na sformatowanym tekście (rich text), kodach kreskowych lub wypełnieniach gradientowych utraci detale. Nie jest to wada komponentu; to po prostu naturalna granica możliwości warstwy translacji

Jedno zastrzeżenie operacyjne ma kluczowe znaczenie. Żaden z tych trzech silników nie jest dostarczany w pakiecie z losLab PDF Library, a mostki są celowo wyłączone z głównej kompilacji — skompilują się tylko wtedy, gdy projekt ma już w swojej ścieżce wyszukiwania biblioteki FastReport VCL 6.x, QuickReport 8 lub ReportBuilder 20. Interfejsy EMF i DrawCommand, na których bazują mostki, pozostały stabilne w wielu głównych wersjach silników, ale to po Twojej stronie leży zapewnienie zgodności wersji. Jeśli o to zadbasz, te dwie ścieżki pokryją całe spektrum potrzeb — od doraźnego zrzutu danych z komponentu DBGrid po zaawansowaną fakturę. Zarówno eksporter tabel, jak i mostki raportów są dostarczane z biblioteką losLab PDF Library dla Delphi oraz C++Builder