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
TextOutmoże ukazać się jako literał tekstuNulllub 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