Skup podataka (dataset) su retci i stupci; PDF stranica je prazna koordinatna mreža (coordinate grid) bez ijednog od ta dva pojma. Premošćivanje tog jaza je zapravo sav posao ovdje. Ne postoji poziv DrawTable u HotPDF-u koji uzima skup podataka i predaje vam formatiranu mrežu (grid). Umjesto toga, dobivate primitive od kojih je mreža sačinjena: TextOut za postavljanje niza znakova u točku, SetFont za odabir izgleda slova (font face), Rectangle i Fill za sjenčanje trake (band), te MoveTo / LineTo / Stroke za crtanje linija. Radni izvoznik tablice (table exporter) disciplina je pretvaranja razmišljanja o recima i stupcima u eksplicitne x i y koordinate, a zatim održavanja tih koordinata točnima kada podaci prijeđu preko dna stranice
Primjer koji slijedi izvještava o zapisima kupaca (customer records), ali ništa u kodu za crtanje ne zna i nije ga briga odakle ti retci dolaze. Original je koristio zastarjeli (legacy) TTable; FireDAC upit (query), in-memory skup podataka ili običan niz zapisa hrani iste rutine bez promjena. Bitno je to da možete prolaziti podacima (walk the data) redak po redak i pročitati četiri polja nizova znakova iz svakog od njih. Držite iscrtavanje odvojenim od izvora podataka i moći ćete mijenjati bilo koju stranu bez narušavanja one druge
Geometrija stupaca dolazi prva
Prije nego što se nacrta ijedan jedini znak, odlučite gdje se nalazi svaki stupac. Ovdje tablica ima četiri stupca, pa joj trebaju četiri lijeva ruba (left edges) i poznata desna margina. Tvrdo kodiranje (hard-coding) magičnog broja pri svakom pozivu TextOut, na način kako to brzi primjeri obično rade, upravo je ono što kasnije čini tablicu bolnom za proširivanje. Imenujte rubove jednom, u točkama od ishodišta u donjem lijevom kutu, i svaki poziv za crtanje pozivat će se na njih po imenu:
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;
Dva detalja ovdje opravdavaju svoje postojanje. Prvo se crta osjenčana traka, a zatim tekst povrh nje, jer je redoslijed crtanja zapravo Z-redoslijed u PDF-u: ispunite (fill) pravokutnik nakon teksta i pokopat ćete redak. Nadalje, naizmjenično sjenčanje nije tek puka dekoracija same radi. Na gustom izvješću to je najjeftiniji način da se spriječi klizanje oka na pogrešan redak, i upravo to je razlog zašto petlja (loop) kasnije prebacuje (flips) boolean varijablu na svakom retku te ju izravno prosljeđuje u Shaded
Pozicije stupaca iznad su fiksne, što je pošteno za izvješće čiju shemu kontrolirate. Kada su podaci varijabilni, mjerite umjesto da nagađate. HotPDF na objektu stranice izlaže mjerenje širine teksta, tako da produkcijska verzija PrintRow može uzeti najdužu očekivanu vrijednost u svakom stupcu, izmjeriti je jednom za odabranu veličinu fonta i izvesti lijeve rubove iz tih širina plus razmak (gutter). Oblik rutine se ne mijenja; mijenja se samo izvor tih konstanti
Zaglavlje, linije i jedno mjesto koje njima vlada
Tablica koja isklizne sa stranice (scrolls off) te se nastavi na sljedećoj bez oznaka (labels) stupaca nečitljiva je. Popravak je da zaglavlje (header) tretirate kao nešto što ponovno crtate, a ne kao nešto što crtate samo jednom. Stavite naslove stupaca i vodoravne linije koje ih uokviruju u jednu rutinu, a tu rutinu pozovite i na početku, kao i ponovno svaki put kada otvorite novu stranicu. Budući da zaglavlje i tijelo dijele iste konstante za stupce, oni se poravnavaju (line up) po samoj svojoj konstrukciji
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;
Primijetite da DrawHeader uzima Y prema referenci i pomiče ga naprijed. Pozivatelj nikada ne mora pamtiti koliko je visoko zaglavlje; rutina koja ga crta je rutina koja to zna. To pravilo o isključivom vlasništvu je ono što sprječava otklon izgleda (drifting layout) kada kasnije dodate logotip ili sažetak filtra (filter summary) u traku zaglavlja. Petlja tijela ostaje nesvjesna toga. Ona samo nastavlja crtati retke od mjesta na koje Y trenutno pokazuje
Sama pravila (odnosno crte / linije) predstavljaju razliku između liste i tablice. Okomiti separatori stupaca jesu ista ideja primijenjena na x os: MoveTo / LineTo / Stroke na svakom rubu stupca, koji se proteže (run) od gornje linije (top rule) do dna zadnjeg retka na stranici. Uzorak se drži horizontalnih linija kako bi ostao čitljiv, no produkcijski korak je mehanički nakon što konstante za stupce jednom postoje
Petlja pokazivača drži prekid stranice (page break)
Crtanje je laka polovica zadatka. Ono što razlikuje igračku od izvješća jest paginacija: znati, prije nego što nacrtate redak, stane li on još uvijek tamo te započeti novu stranicu s novim zaglavljem onda kada ne stane. Ta odluka pripada na točno jedno mjesto - u petlju koja se kreće podacima (walks the data), i nigdje drugdje
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;
Dvije koordinatne činjenice pokreću cijelu petlju. PDF mjeri Y prema gore od donjeg lijevog kuta, tako da retci marširaju dolje po stranici oduzimanjem RowStep od Y svaki put, a test da je stranica puna okida se kada Y padne ispod donje margine, umjesto iznad nekog vrha. Zbunite li se s usmjeravanjem (Get the direction backwards), i vaš prvi redak ispisat će se izvan donjeg ruba dok će petlja misliti kako još ima cijelu stranicu prostora
Druga činjenica pak barem jednom uhvati u zamku gotovo svakoga. AddPage stvara novu stranicu i preusmjerava CurrentPage na nju, ali pritom ne prenosi sa sobom baš ništa: ni font, ni boju ispune (fill color), kao ni samu poziciju. Upravo to je razlog zašto se Page iznova učitava (re-read) iz CurrentPage nakon svakog AddPage, i zašto se iznova poziva SetFont prije redaka tijela. Preskočite li to iznova očitavanje i nastavit ćete crtati po stranici koju ste tek ostavili iza sebe; preskočite font i nova stranica iscrtat će se (renders) u kojem god defaultu na koji preglednik prijeđe u nedostatku drugog izbora (falls back to)
Slučajevi koji razbijaju izvoznika tablice
Većina bugova u tablicama ne prikazuje se na onom "sretnom putu" (happy path) od nekoliko desetaka urednih redaka. Oni žive na rubovima (edges), a rubovi su jeftini za testiranje kad jednom znate gdje se nalaze
- Prazni skupovi podataka. Petlja (loop) iznad nula redaka daje stranicu sa zaglavljem ispod kojeg nema ničega, a što barem izgleda kao da je bilo namjerno napravljeno. Prazna stranica bez ikakvog zaglavlja pak izgleda kao greška (failure). Odlučite se što želite prije nego što to isporučite
- Redak koji slijeće točno na granicu. Generirajte izvješće čiji zadnji redak sjedi samo jedan korak iznad margine, a onda jedno čiji se sljedeći redak nalazi za jedan korak ispod nje. Paginacija pomaknuta za jedan (Off-by-one) skriva se sve dok podaci nisu u točno onoj "pogrešnoj" dužini (wrong length)
- Preduge vrijednosti. Ime tvrtke šire od svog stupca pretrčat će na onaj sljedeći. Izmjerite to polje i odlučite se za politiku rješavanja istog: prelamanje (wrap) u novi redak, odsijecanje (clip) ili skraćivanje s trotočjem (truncate with an ellipsis). Tišina pak nikako nije opcija (politika)
- Null polja. Čitanje nule (null) izravno u
TextOutmože isplivati na površinu kao doslovni tekstNullili pak kao praznina (blank), u ovisnosti o tome na koji ste ga način konvertirali. Pažljivo i s namjerom birajte iscrtavanje (rendering) umjesto da dozvolite pretvorbi varijante (variant conversion) da bira umjesto vas
Pokrenite (run) rezultat kroz više preglednika prije nego li ga proglasite završenim (done). Zamjena fonta i skraćivanje (clipping) ponašaju se drukčije kod raznih uređaja za renderiranje (renderers), te tablica koja u jednom čitaču PDF-a izgleda naoko "kvadratno" (square) u nekom drugom može prikazivati pogrešno postavljen (misaligned) stupac ili odrezan (clipped) naziv grada. Potvrdite da ponovljeno zaglavlje, sjenčanje redaka i margine preživljavaju selidbu i da brojevi stranica ostaju uzastopni nakon što podaci prijeđu neku granicu
Samostalno crtanje mreže (grid) umjesto oslanjanja na vizualnog dizajnera izvješća iziskuje više koda, a taj kompromis itekako se isplati jasno i glasno izreći: vi posjedujete svaki pojedini koordinatni detalj, a što je točno ono što želite za serverske (server-side) grupne poslove (batch jobs), izdavanje računa (invoices) i revizijske izvoze koji se moraju izrenderirati potpuno identično na svakom pojedinom stroju, dok je istovremeno to točno i onaj overhead kojeg biste najradije izbjegli kod jednokratnih (one-off) internih ispisa. Kada je pak riječ o onom prvom - kontrola se isplati odmah čim izvještaj u produkciji mora poprimiti potpuno jednak oblik (look the same) kao što ga je imao za vašim radnim stolom
Pravila i zasjenčane (shaded) trake spomenute ranije oslanjaju se na iste one primitivne vektore i boje (vector and color primitives) koji su obuhvaćeni u vodiču za crtanje na platnu, u slučaju kada najprije na svoj vlastiti (on their own) način želite upotrijebiti naredbe Rectangle, MoveTo te LineTo. Ovdje korišteni primitivi za crtanje dio su HotPDF komponente za Delphi i C++Builder