Artykuł techniczny

Renderowanie tabeli danych do PDF w Delphi za pomocą HotPDF

Zestaw danych to wiersze i kolumny; strona PDF to pusta siatka współrzędnych bez pojęcia ani o jednym, ani o drugim. Wypełnienie tej luki to całe zadanie, jakie tu mamy. W HotPDF nie ma wywołania DrawTable, które pobiera zestaw danych i przekazuje sformatowaną siatkę. Zamiast tego otrzymujesz prymitywy, z których składa się siatka: TextOut do umieszczenia ciągu znaków w punkcie, SetFont do wyboru jego kroju, Rectangle i Fill do zacieniowania pasma, oraz MoveTo / LineTo / Stroke do rysowania linii. Działający eksporter tabeli to dyscyplina przekształcania myślenia o wierszach i kolumnach w jawne współrzędne x i y, a następnie pilnowania, by były one poprawne, gdy dane wykraczają poza dół strony

Poniższy przykład raportuje rekordy klientów, ale kod rysujący nic nie wie ani nie dba o to, skąd pochodzą wiersze. Oryginał używał przestarzałego TTable; zapytanie FireDAC, zestaw danych w pamięci lub zwykła tablica rekordów zasila te same procedury bez zmian. Ważne jest to, że możesz przechodzić przez dane wiersz po wierszu i odczytywać cztery pola znakowe z każdego z nich. Trzymaj renderowanie oddzielnie od źródła danych, a będziesz mógł zmienić każdą ze stron, nie zakłócając drugiej

Geometria kolumn na pierwszym miejscu

Zanim zostanie narysowany pojedynczy znak, zdecyduj, gdzie znajduje się każda kolumna. Tabela ma tutaj cztery kolumny, więc potrzebuje czterech lewych krawędzi i znanej prawej krawędzi. Kodowanie magicznej liczby na sztywno przy każdym wywołaniu TextOut, w sposób, do którego mają tendencję szybkie przykłady, jest dokładnie tym, co sprawia, że ​​późniejsze poszerzenie tabeli jest bolesne. Nazwij krawędzie raz, w punktach od lewego dolnego początku, a każde wywołanie rysowania odnosi się do nich przez nazwę:

const
  ColNo   = 70;    // left edge of the "No." column
  ColName = 110;   // company name
  ColAddr = 300;   // street address
  ColCity = 480;   // city
  RowLeft = 50;    // table frame: left rule
  RowRight = 570;  // table frame: right rule
  RowStep = 20;    // vertical distance between baselines

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // A shaded band behind the row. Rectangle takes X, Y, Width, Height.
    Page.SetRGBFillColor($00FFF3DD);
    Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
    Page.Fill;
    Page.SetRGBFillColor(clBlack);
  end;
  Page.TextOut(ColNo,   Y, 0, ANo);
  Page.TextOut(ColName, Y, 0, AName);
  Page.TextOut(ColAddr, Y, 0, AAddr);
  Page.TextOut(ColCity, Y, 0, ACity);
end;

Dwa szczegóły zarabiają tutaj na swoje utrzymanie. Zacieniony pasek jest rysowany najpierw, a następnie tekst na górze, ponieważ kolejność malowania to z-order w PDF: wypełnij prostokąt po tekście, a pogrzebiesz wiersz. Zmienne zacienienie nie jest też ozdobą samą w sobie. W gęstym raporcie jest to najtańszy sposób, aby zapobiec ześlizgnięciu się oka na niewłaściwy wiersz, dlatego późniejsza pętla przełącza wartość logiczną przy każdym wierszu i przekazuje ją bezpośrednio do Shaded

Pozycje kolumn powyżej są stałe, co jest uczciwe w przypadku raportu, którego schemat kontrolujesz. Gdy dane są zmienne, mierz, a nie zgaduj. HotPDF eksponuje pomiar szerokości tekstu w obiekcie strony, więc wersja produkcyjna PrintRow może przyjąć najdłuższą oczekiwaną wartość w każdej kolumnie, zmierzyć ją raz przy wybranym rozmiarze czcionki i wyprowadzić z tych szerokości lewe krawędzie powiększone o odstęp. Kształt procedury nie ulega zmianie; zmienia się tylko źródło stałych

Nagłówek, linie i jedno miejsce, które jest ich właścicielem

Tabela, która przewija się ze strony i jest wznawiana na następnej bez etykiet kolumn, jest nieczytelna. Rozwiązaniem jest potraktowanie nagłówka jako czegoś, co przerysowujesz, a nie rysujesz raz. Umieść tytuły kolumn i poziome linie, które je otaczają, w jednej procedurze, i wywołuj tę procedurę zarówno na początku, jak i za każdym razem, gdy otwierasz nową stronę. Ponieważ nagłówek i treść współdzielą te same stałe kolumn, wyrównują się konstrukcyjnie

procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Left: source label and page number. Right: generation time.
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Two horizontal rules that box the column titles.
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // The column titles, in a heavier face so they read as headings.
  Page.SetFont('Times New Roman', [fsBold], 12);
  Page.SetRGBFillColor(clNavy);
  PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
  Page.SetRGBFillColor(clBlack);

  Y := Y + RowStep + 45;  // advance past the boxed header before the first body row
end;

Zauważ, że DrawHeader przyjmuje Y przez referencję i posuwa je do przodu. Osoba wywołująca nigdy nie musi pamiętać, jak wysoki jest nagłówek; rutyna, która go rysuje, to ta, która to wie. Ta zasada pojedynczej własności jest tym, co zapobiega dryfowaniu układu, gdy później dodasz logo lub podsumowanie filtra do pasma nagłówka. Pętla treści pozostaje nieświadoma. Po prostu kontynuuje rysowanie rzędów z miejsca, w którym aktualnie wskazuje Y

Same zasady to różnica między listą a tabelą. Pionowe separatory kolumn to ten sam pomysł zastosowany do osi x: MoveTo / LineTo / Stroke na krawędzi każdej kolumny, biegnący od górnej krawędzi do dołu ostatniego wiersza na stronie. Przykładowa próbka utrzymuje zasady poziome, aby zachować czytelność, ale krok produkcji jest mechaniczny, gdy istnieją stałe kolumn

Pętla kursora kontroluje podział strony

Rysowanie to łatwiejsza połowa. Połowa, która oddziela zabawkę od raportu, to stronicowanie: wiedza, przed narysowaniem wiersza, czy nadal pasuje, i rozpoczęcie nowej strony od nowego nagłówka, jeśli nie pasuje. Ta decyzja należy dokładnie do jednego miejsca, do pętli, która przechodzi przez dane, i nigdzie indziej

var
  Pdf: THotPDF;
  Page: THPDFPage;
  Y: Single;
  PageNo: Integer;
  Shaded: boolean;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'CustomerReport.pdf';
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;

    // Report title, once, at the top of the first page.
    Page.SetFont('Arial', [fsBold], 24);
    Page.TextOut(200, 800, 0, 'Customer Report');

    PageNo := 1;
    Y := 760;
    DrawHeader(Page, Y, PageNo);
    Shaded := False;

    CustomerTable.First;
    while not CustomerTable.Eof do
    begin
      // Out of room? Open a new page and repeat the header there.
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage moves CurrentPage forward
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont must be reissued on every new page
      PrintRow(Page, Y,
        VarToStr(CustomerTable['CustNo']),
        VarToStr(CustomerTable['Company']),
        VarToStr(CustomerTable['Addr1']),
        VarToStr(CustomerTable['City']),
        Shaded);

      Y := Y - RowStep;
      CustomerTable.Next;
    end;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Pętlą kierują dwa fakty dotyczące współrzędnych. PDF mierzy oś y w górę od lewego dolnego rogu, więc wiersze przesuwają się w dół strony, odejmując za każdym razem RowStep od Y, a test zapełnienia strony uruchamia się, gdy Y spadnie poniżej dolnego marginesu, a nie powyżej pewnego wierzchołka. Odwróć kierunek, a twój pierwszy rząd zostanie wydrukowany poza dolną krawędzią, podczas gdy pętla myśli, że ma pełną stronę wolnego miejsca

Drugi fakt chwyta każdego na raz. AddPage tworzy nową stronę i ponownie wskazuje na nią CurrentPage, ale nic nie przenosi: ani czcionki, ani koloru wypełnienia, ani pozycji. Dlatego Page jest ponownie odczytywane z CurrentPage po każdym AddPage i dlaczego SetFont jest wydawane ponownie przed wierszami treści. Pomiń ponowne czytanie, a będziesz kontynuował rysowanie na stronie, którą właśnie zostawiłeś; pomiń czcionkę, a nowa strona wyrenderuje się w dowolnym ustawieniu domyślnym, do którego cofnie się przeglądarka

Przypadki psujące eksporter tabeli

Większość błędów w tabelach nie pojawia się na optymistycznej ścieżce kilkudziesięciu uporządkowanych wierszy. Mieszkają na krawędziach, a przetestowanie tych krawędzi jest tanie, kiedy już wiesz, gdzie się znajdują

  • Puste zestawy danych. Pętla z zero rzędów produkuje stronę z nagłówkiem i niczym pod nim, co przynajmniej wygląda na celowe. Pusta strona bez nagłówka wygląda na błąd. Zdecyduj, czego chcesz przed wysyłką
  • Rząd, który ląduje dokładnie na granicy. Wygeneruj raport, którego ostatni rząd siedzi jeden krok nad marginesem, następnie jeden, w którym następny rząd znajduje się o jeden krok poniżej. Błąd stronicowania o jeden (off-by-one) ukrywa się aż dane mają dokładnie nieodpowiednią długość
  • Zbyt długie wartości. Nazwa firmy szersza niż kolumna wejdzie na następną. Zmierz pole i zdecyduj o polityce: przenieś na drugą linijkę, przypnij lub utnij z użyciem wielokropka. Cisza nie jest polityką
  • Pola Null. Odczytanie wartości null bezpośrednio do TextOut może ukazać się jako literał tekstu Null lub jako pustka, zależnie od sposobu jej przekonwertowania. Celowo wybierz renderowanie raczej niż pozwalanie na wybór konwersji wariantowej dla ciebie

Przeprowadź wynik przez więcej niż jedną przeglądarkę, zanim uznasz go za gotowy. Podstawianie czcionek i przypinanie zachowują się różnie pomiędzy rendererami, a tabela, która wygląda kwadratowo w jednym czytniku PDF, w innym może pokazać źle dopasowaną kolumnę lub obcięte miasto. Upewnij się, że powtarzający się nagłówek, zacienienie wierszy i marginesy przetrwają to posunięcie oraz czy numery stron pozostają ciągłe po przekroczeniu przez dane granicy

Rysowanie siatki samodzielnie zamiast polegania na wizualnym kreatorze raportów to więcej kodu, i ten kompromis należy nazwać wprost: jesteś właścicielem każdej współrzędnej, czego właśnie potrzebujesz od prac wsadowych po stronie serwera, faktur oraz eksportów podczas audytów, które muszą się renderować identycznie na każdej maszynie i jest to dokładnie to obciążenie operacyjne, jakiego byś wolał uniknąć w jednorazowym wykazie wewnątrzfirmowym. W przypadku pierwszego wariantu ten nadzór odpłaci się po raz pierwszy, w chwili gdy dany raport musi wyglądać tak samo na produkcji, tak jak wyglądał na twoim biurku

Zasady i zacienione paski u góry opierają się na tych samych wektorach oraz prymitywach kolorystycznych opisanych podczas omawiania rysowania na płótnie, jeżeli chcesz, by wywołania Rectangle, MoveTo, a także LineTo zostały w pierwszej kolejności zbadane własnoręcznie. Prymitywy używane tutaj to część HotPDF Component dla Delphi i C++Builder