Egy adathalmaz sorokból és oszlopokból áll; egy PDF oldal egy üres koordináta-háló, amelynek egyikről sincs fogalma. Ezen űr áthidalása az egész feladat itt. A HotPDF-ben nincs olyan DrawTable hívás, amely egy adathalmazt fogad, és átad egy formázott rácsot. Amit ehelyett kap, azok a primitívek, amelyekből egy rács felépül: a TextOut egy karakterlánc pontra helyezéséhez, a SetFont a betűtípus kiválasztásához, a Rectangle és a Fill egy sáv árnyékolásához, valamint a MoveTo / LineTo / Stroke a vonalak megrajzolásához. Egy működő táblázatexportáló az a fegyelem, amellyel a sor-és-oszlop gondolkodást explicit x és y koordinátákká alakítjuk, majd ezeket a koordinátákat pontosan tartjuk, amikor az adatok túlfutnak az oldal alján
Az alábbi példa ügyfélrekordokat jelent, de a rajzoló kód semmit sem tud arról, és nem is érdekli, honnan származnak a sorok. Az eredeti egy régi TTable-t használt; egy FireDAC lekérdezés, egy memóriabeli adathalmaz vagy egy egyszerű rekordtömb változatlanul táplálja ugyanazokat a rutinokat. Ami számít, hogy az adatokat soronként be lehessen járni, és mindegyikből ki lehessen olvasni négy karakterlánc mezőt. Tartsa elkülönítve a renderelést az adatforrástól, és bármelyik oldalt módosíthatja anélkül, hogy a másikat megzavarná
Az oszlopgeometria az első
Mielőtt egyetlen karaktert is megrajzolna, döntse el, hol helyezkedik el minden egyes oszlop. Itt egy táblázatnak négy oszlopa van, így négy bal szélre és egy ismert jobb margóra van szüksége. Ha minden TextOut hívásnál fixen bekódol egy bűvös számot – ahogy a gyors minták teszik –, az pontosan az, ami miatt egy táblázatot később fájdalmas lesz kiszélesíteni. Nevezze el a széleket egyszer, pontokban a bal alsó origótól, és minden rajzoló hívás név szerint hivatkozik rájuk:
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;
Itt két részlet is megdolgozik a pénzéért. Az árnyékolt sávot rajzoljuk meg először, majd rá a szöveget, mert a festési sorrend a z-sorrend a PDF-ben: ha a szöveg után tölti ki a téglalapot, akkor eltemeti a sort. És a váltakozó árnyékolás nem öncélú díszítés. Egy sűrű jelentésnél ez a legolcsóbb módja annak, hogy megakadályozzuk a szem rossz sorra csúszását, ezért a ciklus később minden soron megfordít egy logikai változót, és egyenesen átadja a Shaded-nek
A fenti oszloppozíciók rögzítettek, ami őszinte egy olyan jelentés esetében, amelynek sémáját Ön irányítja. Ha az adatok változóak, mérjen ahelyett, hogy tippelne. A HotPDF hozzáférést biztosít a szövegszélesség méréséhez az oldal objektumon, így a PrintRow éles verziója veheti a leghosszabb várt értéket minden oszlopban, megmérheti egyszer a kiválasztott betűméretnél, és levezetheti a bal széleket ezekből a szélességekből plusz egy margóból (gutter). A rutin alakja nem változik; csak a konstansok forrása
A fejléc, a vonalak és egyetlen hely, amely birtokolja őket
Egy olyan táblázat, amely legördül egy oldalról, és a következőn oszlopcímkék nélkül folytatódik, olvashatatlan. A megoldás az, hogy a fejlécet valami olyannak tekintjük, amit újra megrajzolunk, nem pedig valaminek, amit csak egyszer rajzolunk meg. Tegye az oszlopcímeket és az azokat keretező vízszintes vonalakat egyetlen rutinba, és hívja meg ezt a rutint az elején, majd minden alkalommal újra, amikor új oldalt nyit. Mivel a fejléc és a törzs ugyanazokon az oszlopkonstansokon osztozik, már a felépítésükből adódóan egyvonalban lesznek
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;
Vegye figyelembe, hogy a DrawHeader referenciaként veszi fel az Y-t, és előre mozgatja. A hívónak soha nem kell emlékeznie arra, hogy milyen magas a fejléc; a rutin, amely megrajzolja, az a rutin, amely ezt tudja. Ez az egyetlen tulajdonosi (single ownership) szabály az, ami megakadályozza, hogy az elrendezés elcsússzon, amikor később egy logót vagy egy szűrő-összefoglalót ad hozzá a fejlécsávhoz. A törzsciklus (body loop) tudatlan marad. Csak folytatja a sorok megrajzolását onnan, ahová az Y éppen mutat
Maguk a vonalak (rules) jelentik a különbséget egy lista és egy táblázat között. A függőleges oszlopelválasztók ugyanazt az ötletet alkalmazzák az x tengelyre: egy MoveTo / LineTo / Stroke minden oszlop szélénél, a felső vonaltól az oldalon lévő utolsó sor aljáig futtatva. A minta a vízszintes vonalaknál marad az olvashatóság érdekében, de a gyártási lépés mechanikus, amint az oszlopkonstansok léteznek
A kurzorciklus birtokolja az oldaltörést
A rajzolás a könnyebbik fele. Az a fele, ami elválaszt egy játékot egy jelentéstől, a tördelés (pagination): annak tudata, mielőtt megrajzolna egy sort, hogy az még belefér-e, és ha nem, akkor egy friss oldal indítása friss fejléccel. Ez a döntés pontosan egyetlen helyre tartozik, a ciklusba, amely bejárja az adatokat, és sehova máshova
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;
Két koordinátatény vezérli az egész ciklust. A PDF az y-t felfelé méri a bal alsó sarokból, így a sorok lefelé haladnak az oldalon úgy, hogy minden alkalommal kivonjuk a RowStep-et az Y-ból, és az oldaltelítettségi (page-full) teszt akkor indul el, amikor az Y az alsó margó alá esik, nem pedig valamilyen felső határ fölé. Ha eltéveszti az irányt, az első sora lefut az alsó szélről, miközben a ciklus úgy gondolja, hogy még egy teljes oldalnyi helye van
A másik tény szinte mindenkit elkap egyszer. Az AddPage létrehoz egy új oldalt, és átirányítja rá a CurrentPage-t, de semmit nem visz át: sem a betűtípust, sem a kitöltési színt, sem a pozíciót. Ezért olvassa újra a Page-t a CurrentPage-ből minden AddPage után, és ezért adja ki újra a SetFont-ot a törzssorok előtt. Hagyja ki az újraolvasást, és továbbra is arra az oldalra fog rajzolni, amelyet éppen maga mögött hagyott; hagyja ki a betűtípust, és az új oldal abban az alapértelmezésben fog megjelenni, amelyre a nézegető visszaesik
Azok az esetek, amelyek megtörnek egy táblázatexportálót
A legtöbb táblázathiba nem mutatkozik meg néhány tucat rendezett sor boldog ösvényén. A peremeken élnek, és a peremeket olcsó tesztelni, amint tudja, hol vannak
- Üres adathalmazok. A nulla soron végzett ciklus egy olyan oldalt hoz létre, amelynek fejléce van, és alatta semmi sincs, ami legalább szándékosnak tűnik. Egy üres oldal fejléc nélkül hibának tűnik. Döntse el, melyiket szeretné szállítás előtt
- A sor, amely pontosan a határra esik. Hozzon létre egy jelentést, amelynek utolsó sora egy lépéssel a margó felett van, majd egy olyat, amelynek következő sora egy lépéssel alatta van. Az egyel elcsúszott (off-by-one) tördelés addig rejtve marad, amíg az adatok pontosan rossz hosszúságúak nem lesznek
- Túl hosszú értékek. Az oszlopánál szélesebb cégév belefut a következőbe. Mérje meg a mezőt, és döntsön egy irányelvről: tördelés egy második sorba, levágás, vagy csonkítás ellipszissel (három ponttal). A csend nem irányelv
- Null mezők. Egy null egyenesen a
TextOut-ba történő beolvasása a szó szerintiNullszövegként vagy üresként is megjelenhet, attól függően, hogyan konvertálja. Válassza ki a renderelést szándékosan, ahelyett, hogy hagyná, hogy a variáns konverzió válasszon Ön helyett
Futtassa át az eredményt egynél több nézegetőn, mielőtt késznek nyilvánítaná. A betűtípus-helyettesítés és a levágás eltérően viselkedik a renderelők között, és egy táblázat, amely egy PDF-olvasóban négyzetesnek (szabályosnak) tűnik, egy másikban egy rosszul igazított oszlopot vagy egy levágott várost mutathat. Erősítse meg, hogy az ismételt fejléc, a sorok árnyékolása és a margók túlélik a költözést, és hogy az oldalszámok folyamatosak maradnak az adatok határon való átlépése után
Ha maga rajzolja a rácsot, ahelyett, hogy vizuális jelentéstervezőre támaszkodna, az több kód, és a kompromisszumot érdemes nyíltan megnevezni: Ön birtokol minden koordinátát, ami pontosan az, amit a szerveroldali kötegelt feladatokhoz (batch jobs), számlákhoz és audit exportálásokhoz szeretne, amelyeknek minden gépen azonos módon kell renderelődniük, és pontosan az a többletmunka, amelyet inkább elkerülne egy egyszeri belső lista esetében. Az előbbi esetben a kontroll megtérül, amikor egy jelentésnek élesben is ugyanúgy kell kinéznie, mint az Ön asztalán
A fenti vonalak és árnyékolt sávok ugyanazokra a vektor- és színprimitívekre támaszkodnak, amelyeket a vászonrajzolási bemutató ismertet, ha a Rectangle, MoveTo és LineTo hívásokat először külön-külön szeretné kezelni. Az itt használt rajzolási primitívek a Delphihez és C++Builderhez készült HotPDF komponens részét képezik