Een dataset bestaat uit rijen en kolommen; een PDF-pagina is een blanco coördinatenraster zonder besef van beide. Het overbruggen van die kloof is hier het hele werk. Er is geen DrawTable-aanroep in HotPDF die een dataset aanneemt en u een geformatteerd raster overhandigt. Wat u in plaats daarvan krijgt, zijn de primitieven (primitives) waaruit een raster is opgebouwd: TextOut om een tekenreeks op een punt te plaatsen, SetFont om het lettertype te kiezen, Rectangle en Fill om een strook te arceren, en MoveTo / LineTo / Stroke om lijnen te tekenen. Een werkende tabelexporteur is de discipline van het omzetten van rij-en-kolom-denken in expliciete x- en y-coördinaten, en vervolgens het eerlijk houden van die coördinaten wanneer de gegevens voorbij de onderkant van de pagina lopen
Het onderstaande voorbeeld rapporteert klantrecords, maar niets in de tekencode weet of maakt het uit waar de rijen vandaan komen. Het origineel gebruikte een verouderde TTable; een FireDAC-query, een in-memory dataset of een eenvoudige array van records voedt dezelfde routines ongewijzigd. Wat belangrijk is, is dat u de gegevens rij voor rij kunt doorlopen en uit elke rij vier tekenreeksvelden kunt uitlezen. Houd de weergave (rendering) gescheiden van de gegevensbron en u kunt beide kanten wijzigen zonder de andere te verstoren
Kolomgeometrie komt op de eerste plaats
Voordat er een enkel teken is getekend, moet u beslissen waar elke kolom zich bevindt. Een tabel heeft hier vier kolommen, dus er zijn vier linkerranden en een bekende rechtermarge nodig. Het hard coderen van een magisch getal (magic number) bij elke TextOut-aanroep, zoals snelle voorbeelden vaak doen, is precies wat een tabel pijnlijk maakt om later breder te maken. Benoem de randen één keer, in punten vanaf de oorsprong linksonder, en elke tekenaanroep verwijst ernaar bij naam:
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;
Twee details bewijzen hier hun nut. De gearceerde strook wordt eerst getekend, daarna pas de tekst er bovenop, want de schildervolgorde is de z-orde (z-order) in PDF: vul de rechthoek na de tekst en u begraaft de rij. En de afwisselende arcering (alternating shade) is geen decoratie op zich. Op een dicht rapport is het de goedkoopste manier om te voorkomen dat het oog naar de verkeerde regel glijdt, wat de reden is waarom de lus later bij elke rij een booleaanse waarde (boolean) omdraait en deze rechtstreeks in Shaded doorgeeft
De bovenstaande kolomposities staan vast, wat eerlijk is voor een rapport waarvan u het schema beheert. Wanneer de gegevens variabel zijn, meet dan in plaats van te gokken. HotPDF stelt tekstbreedtemeting (text-width measurement) op het paginaobject beschikbaar, zodat de productieversie van PrintRow de langste verwachte waarde in elke kolom kan aannemen, deze eenmaal kan meten op de gekozen lettergrootte en de linkerranden kan afleiden uit die breedtes plus een tussenruimte (gutter). De vorm van de routine verandert niet; alleen de bron van de constanten doet dat
De koptekst, de lijnen en één plaats die ze bezit
Een tabel die van een pagina afrolt en verdergaat op de volgende zonder kolomlabels, is onleesbaar. De oplossing is om de koptekst (header) te behandelen als iets dat u opnieuw tekent, niet iets dat u eenmaal tekent. Plaats de kolomtitels en de horizontale lijnen (horizontal rules) die ze omkaderen in een enkele routine en roep die routine zowel bij het begin aan, alsook telkens wanneer u een nieuwe pagina opent. Omdat de koptekst en de romp (body) dezelfde kolomconstanten delen, komen ze op één lijn door constructie
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;
Merk op dat DrawHeader de Y meekrijgt bij verwijzing (by reference) en deze naar voren verplaatst. De aanroeper hoeft nooit te onthouden hoe hoog de koptekst is; de routine die deze tekent, is de routine die het weet. Die enkele eigendomsregel (single ownership rule) is wat voorkomt dat de lay-out verschuift wanneer u later een logo of een filtersamenvatting aan de kopstrook toevoegt. De romplus (body loop) blijft hier onwetend van. Het blijft gewoon rijen tekenen vanaf de plaats waar Y momenteel naar wijst
De lijnen zelf maken het verschil tussen een lijst en een tabel. Verticale kolomscheidingstekens zijn hetzelfde idee toegepast op de x-as: een MoveTo / LineTo / Stroke aan elke kolomrand, getrokken vanaf de bovenste lijn tot de onderkant van de laatste rij op de pagina. Het voorbeeld houdt zich aan horizontale lijnen om leesbaar te blijven, maar de productiestap is mechanisch zodra de kolomconstanten bestaan
De cursorlus bezit de pagina-einde
Het tekenen is de gemakkelijke helft. De helft die een speeltje scheidt van een rapport is paginering (pagination): weten, voordat u een rij tekent, of deze nog past, en een nieuwe pagina beginnen met een frisse koptekst als dat niet zo is. Die beslissing hoort op precies één plek thuis, de lus die de gegevens doorloopt, en nergens anders
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;
Twee coördinatenfeiten sturen de hele lus aan. PDF meet y omhoog vanaf de linkerbenedenhoek, dus de rijen marcheren naar beneden op de pagina door telkens RowStep van Y af te trekken, en de test of de pagina vol is wordt geactiveerd wanneer Y onder de ondermarge zakt in plaats van boven een bepaalde bovenkant. Als u de richting omkeert (backwards), wordt uw eerste rij buiten de onderrand afgedrukt terwijl de lus denkt dat het een volledige pagina aan ruimte heeft
Het andere feit pakt bijna iedereen wel een keer. AddPage creëert een nieuwe pagina en verwijst CurrentPage ernaar, maar het neemt niets mee over: niet het lettertype, niet de vulkleur, niet de positie. Daarom wordt Page na elke AddPage opnieuw gelezen vanuit CurrentPage, en daarom wordt SetFont opnieuw uitgegeven voor de romprijen (body rows). Sla het opnieuw lezen over en u blijft tekenen op de pagina die u zojuist hebt achtergelaten; sla het lettertype over en de nieuwe pagina wordt gerenderd in welke standaard de viewer ook terugvalt
De gevallen die een tabelexporteur breken
De meeste tabelbugs komen niet aan het licht op het gelukkige pad van enkele tientallen nette rijen. Ze leven aan de randen, en de randen zijn goedkoop te testen als u eenmaal weet waar ze zijn
- Lege datasets. Een lus over nul rijen produceert een pagina met een koptekst en niets daaronder, wat er in ieder geval intentioneel uitziet. Een blanco pagina zonder koptekst ziet eruit als een storing. Bepaal wat u wilt voordat u uitbrengt (shipping)
- De rij die precies op de grens belandt. Genereer een rapport waarvan de laatste rij precies één stap boven de marge zit, en vervolgens een waarvan de volgende rij één stap eronder zit. Off-by-one paginering verbergt zich totdat de gegevens exact de verkeerde lengte hebben
- Te lange waarden. Een bedrijfsnaam die breder is dan zijn kolom, zal overgaan in de volgende. Meet het veld en beslis over beleid: omwikkelen (wrap) naar een tweede regel, bijknippen (clip), of afkappen (truncate) met een weglatingsteken (ellipsis). Stilzwijgen is geen beleid
- Null velden. Het rechtstreeks inlezen van een null (null) in
TextOutkan naar boven komen als de letterlijke tekstNullof als een blanco ruimte, afhankelijk van hoe u het converteert. Kies de weergave bewust in plaats van de variantconversie voor u te laten kiezen
Haal het resultaat door meer dan één viewer voordat u het als voltooid beschouwt. Lettertypevervanging en bijknippen (clipping) gedragen zich anders over renderers heen, en een tabel die vierkant lijkt in de ene PDF-lezer kan een niet-uitgelijnde kolom of een afgekapte stad tonen in een andere. Bevestig dat de herhaalde koptekst, de rij-arcering en de marges de verhuizing overleven, en dat paginanummers doorlopend blijven nadat de gegevens een grens passeren
Zelf het raster tekenen in plaats van leunen op een visuele rapportontwerper betekent meer code, en de afweging is het waard om duidelijk te benoemen: u bent eigenaar van elke coördinaat, wat precies is wat u wilt voor server-side batchtaken, facturen en audit-exports die identiek moeten renderen op elke machine, en het is precies de overhead die u liever vermijdt voor een eenmalige interne lijst. Voor de eerste categorie betaalt de controle zichzelf terug op de eerste keer dat een rapport er in productie hetzelfde moet uitzien als op uw bureau
De lijnen en gearceerde stroken hierboven leunen op dezelfde vector- en kleurprimitieven die worden behandeld in de canvas drawing walkthrough, mocht u de aanroepen voor Rectangle, MoveTo en LineTo eerst op zichzelf willen behandelen. De tekenprimitieven die hier worden gebruikt, maken deel uit van de HotPDF-component voor Delphi en C++Builder