Et datasett er rader og kolonner; en PDF-side er et tomt koordinatrutenett uten begrep om noen av delene. Å bygge bro over det gapet er hele jobben her. Det er ikke noe DrawTable-kall i HotPDF som tar et datasett og gir deg et formatert rutenett. Det du får i stedet, er primitivene (primitives) et rutenett er laget av: TextOut for å plassere en streng ved et punkt, SetFont for å velge dens skrifttype, Rectangle og Fill for å skyggelegge et bånd, og MoveTo / LineTo / Stroke for å tegne linjer (rules). En fungerende tabelleksportør er disiplinen med å gjøre rad-og-kolonne-tenkning om til eksplisitte x- og y-koordinater, og deretter holde disse koordinatene ærlige når dataene løper forbi bunnen av siden
Eksempelet som følger rapporterer kundeposter, men ingenting i tegnekoden vet eller bryr seg om hvor radene kommer fra. Originalen brukte en eldre TTable; en FireDAC-spørring, et datasett i minnet, eller et rent array av poster (records) mater de samme rutinene uendret. Det som betyr noe er at du kan gå gjennom dataene én rad av gangen og lese fire strengfelt ut av hver. Hold gjengivelsen atskilt fra datakilden, så kan du endre hver side uten å forstyrre den andre
Kolonnegeometri kommer først
Før et eneste tegn tegnes, bestem hvor hver kolonne bor. En tabell har fire kolonner her, så den trenger fire venstrekanter og en kjent høyremarg. Å hardkode et magisk tall ved hvert TextOut-kall, slik raske eksempler har en tendens til å gjøre, er akkurat det som gjør en tabell smertefull å utvide senere. Navngi kantene én gang, i punkter fra opprinnelsen nede til venstre, og hvert tegnekall refererer til dem ved navn:
const
ColNo = 70; // venstre kant av "Nr."-kolonnen
ColName = 110; // firmanavn
ColAddr = 300; // gateadresse
ColCity = 480; // by
RowLeft = 50; // tabellramme: venstre linje
RowRight = 570; // tabellramme: høyre linje
RowStep = 20; // vertikal avstand mellom grunnlinjer
procedure PrintRow(Page: THPDFPage; Y: Single;
const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
if Shaded then
begin
// Et skyggelagt bånd bak raden. Rectangle tar X, Y, bredde, høyde.
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;
To detaljer gjør nytte for seg her. Det skyggelagte båndet tegnes først, deretter teksten på toppen, fordi male-rekkefølge er z-rekkefølge i PDF: fyll rektangelet etter teksten, og du begraver raden. Og den vekslende nyansen er ikke dekorasjon for sin egen del. På en tett rapport er det den billigste måten å stoppe øyet fra å gli over på feil linje, og det er grunnen til at løkken senere snur (flips) en boolsk verdi (boolean) på hver rad og sender den rett inn i Shaded
Kolonneposisjonene ovenfor er faste, noe som er ærlig for en rapport hvis skjema du kontrollerer. Når dataene er variable (variable), mål i stedet for å gjette. HotPDF eksponerer måling av tekstbredde på sideobjektet, så produksjonsversjonen av PrintRow kan ta den lengste forventede verdien i hver kolonne, måle den én gang med valgt fontstørrelse, og utlede venstrekanter fra disse breddene pluss en takrenne (gutter). Formen på rutinen endres ikke; det er bare kilden til konstantene som gjør det
Overskriften, linjene, og ett sted som eier dem
En tabell som scroller av en side og fortsetter på den neste uten kolonnetitler, er uleselig. Løsningen er å behandle overskriften (header) som noe du tegner på nytt, ikke noe du tegner bare én gang. Plasser kolonnetitlene og de horisontale linjene som rammer dem inn, i én enkelt rutine, og kall den rutinen både i starten og igjen hver gang du åpner en ny side. Fordi overskriften og selve tabellkroppen (body) deler de samme kolonne-konstantene, vil de stilles opp av seg selv (by construction)
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
// Venstre: kilde-etikett og sidetall. Høyre: genereringstidspunkt.
Page.SetFont('Arial', [fsItalic], 10);
Page.TextOut(RowLeft, Y, 0, 'customer.db Side ' + IntToStr(PageNo));
Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));
// To horisontale linjer som boksene kolonnetitlene.
Page.MoveTo(RowLeft, Y + 15);
Page.LineTo(RowRight, Y + 15);
Page.MoveTo(RowLeft, Y + 45);
Page.LineTo(RowRight, Y + 45);
Page.Stroke;
// Kolonnetitlene, i en tyngre skrifttype slik at de leses som overskrifter.
Page.SetFont('Times New Roman', [fsBold], 12);
Page.SetRGBFillColor(clNavy);
PrintRow(Page, Y + 25, 'Nr.', 'Firma', 'Adresse', 'By', False);
Page.SetRGBFillColor(clBlack);
Y := Y + RowStep + 45; // gå videre forbi den boksede overskriften før den første tabellraden
end;
Legg merke til at DrawHeader tar Y som referanse (by reference) og flytter den fremover. Den som kaller rutinen (the caller) trenger aldri å huske hvor høy overskriften er; rutinen som tegner den er rutinen som vet det. Denne regelen om enkelt-eierskap er det som forhindrer at oppsettet (layout) driver (drifting) når du senere legger til en logo eller en filtersammendrag i overskriftsbåndet. Tabellkropp-løkken (body loop) forblir uvitende. Den fortsetter bare å tegne rader fra der Y for øyeblikket peker
Selve linjene (rules) er forskjellen mellom en liste og en tabell. Vertikale kolonne-separatorer er samme idé brukt på x-aksen: en MoveTo / LineTo / Stroke ved hver kolonnekant, kjørt fra den øvre linjen til bunnen av den siste raden på siden. Eksempelet holder seg til horisontale linjer for å forbli lesbart, men produksjonstrinnet er mekanisk så snart kolonnekonstantene eksisterer
Markørløkken eier sideskiftet
Tegning er den enkle halvdelen. Den halvdelen som skiller et leketøy fra en rapport er paginering: å vite, før du tegner en rad, om den fremdeles får plass, og å starte en ny side med en ny overskrift når den ikke gjør det. Den avgjørelsen hører hjemme på nøyaktig ett sted, løkken som går gjennom dataene, og ingen andre steder
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;
// Rapporttittel, én gang, øverst på første side.
Page.SetFont('Arial', [fsBold], 24);
Page.TextOut(200, 800, 0, 'Kunderapport');
PageNo := 1;
Y := 760;
DrawHeader(Page, Y, PageNo);
Shaded := False;
CustomerTable.First;
while not CustomerTable.Eof do
begin
// Ikke mer plass? Åpne en ny side og gjenta overskriften der.
if Y < 60 then
begin
Pdf.AddPage;
Page := Pdf.CurrentPage; // AddPage flytter CurrentPage fremover
Inc(PageNo);
Y := 760;
DrawHeader(Page, Y, PageNo);
end;
Shaded := not Shaded;
Page.SetFont('Arial', [], 10); // SetFont må utstedes på nytt på hver nye side
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;
To koordinat-fakta driver hele løkken. PDF måler y oppover fra det nederste venstre hjørnet, slik at radene marsjerer nedover siden ved å trekke fra RowStep fra Y hver gang, og testen for full side utløses (fires) når Y faller under bunnmargen fremfor over en eller annen topp. Får du retningen bakvendt, skrives den første raden din ut fra den nederste kanten mens løkken tror den har plass til en hel side
Det andre faktumet fanger nesten alle en gang. AddPage oppretter en ny side og om-peker (repoints) CurrentPage til den, men den bærer ingenting over: ikke skrifttypen, ikke fyllfargen, ikke posisjonen. Det er grunnen til at Page leses på nytt fra CurrentPage etter hver AddPage, og hvorfor SetFont utstedes på nytt før tabellradene (body rows). Hopper du over den nye lesingen (re-read), fortsetter du å tegne på siden du nettopp forlot; hopper du over fonten, gjengis (renders) den nye siden i den standardinnstillingen visningsprogrammet faller tilbake til
Tilfellene som knekker en tabelleksportør
De fleste tabellfeil dukker ikke opp på den lykkelige stien (happy path) med noen få dusin ryddige rader. De bor i kantene (edges), og kantene er billige å teste når du først vet hvor de er
- Tomme datasett (Empty datasets). En løkke over null rader produserer en side med en overskrift og ingenting under den, noe som i det minste ser tilsiktet ut. En blank side uten overskrift ser ut som en feil. Bestem hva du vil ha før du leverer (shipping)
- Raden som lander nøyaktig på grensen (boundary). Generer en rapport der den siste raden sitter ett trinn over margen, og deretter en hvis neste rad er ett trinn under den. Off-by-one paginering gjemmer seg til dataene har nøyaktig feil lengde
- For lange verdier. Et firmanavn som er bredere enn kolonnen sin vil løpe inn i den neste. Mål feltet og bestem deg for en policy (retningslinje): bryt til en andre linje, klipp (clip), eller trunker med en ellipse. Stillhet er ingen policy
- Null-felt. Å lese en null rett inn i
TextOutkan dukke opp som bokstavelig tekstNulleller som en tom plass (blank), avhengig av hvordan du konverterer den. Velg gjengivelsen (rendering) bevisst i stedet for å la variant-konverteringen velge for deg
Kjør resultatet gjennom mer enn ett visningsprogram før du kaller det ferdig. Fonterstatning og klipping (clipping) oppfører seg forskjellig på tvers av gjengivere, og en tabell som ser firkantet ut i én PDF-leser, kan vise en feiljustert kolonne eller en klippet by i en annen. Bekreft at den gjentatte overskriften, radskyggen, og margene overlever flyttingen, og at sidetallene forblir sammenhengende etter at dataene krysser en grense
Å tegne rutenettet selv i stedet for å lene seg på en visuell rapportdesigner er mer kode, og avveiningen (tradeoff) er verdt å navngi i klartekst: du eier hver eneste koordinat, som er nøyaktig hva du ønsker for server-side batch-jobber, fakturaer, og revisjonseksporter (audit exports) som må gjengis identisk på hver maskin, og nøyaktig den tilleggsbelastningen (overhead) du heller vil unngå for en engangs intern liste. For det førstnevnte, betaler kontrollen for seg selv den første gangen en rapport må se ut på samme måte i produksjon som den gjorde på pulten din
Linjene og de skyggelagte båndene ovenfor lener seg på de samme vektor- og fargeprimitivene (primitives) som dekkes i canvas tegning-gjennomgangen (canvas drawing walkthrough), hvis du vil at Rectangle, MoveTo, og LineTo-kallene skal behandles for seg selv først. Tegneprimitivene som brukes her er en del av HotPDF-komponenten for Delphi og C++Builder