Datová sada (dataset) jsou řádky a sloupce; stránka PDF je prázdná mřížka souřadnic, která o nich nemá tušení. Překlenutí této propasti je tu tím hlavním úkolem. V HotPDF neexistuje volání DrawTable, které by vzalo datovou sadu a předalo vám formátovanou mřížku. Místo toho získáte primitiva, ze kterých se mřížka skládá: TextOut pro umístění řetězce na bod, SetFont pro výběr jeho řezu, Rectangle a Fill pro stínování pruhu a MoveTo / LineTo / Stroke pro kreslení linek. Fungující exportér tabulek je disciplína, jak převést myšlení v řádcích a sloupcích na explicitní souřadnice x a y a následně tyto souřadnice udržet v pořádku, když data přeběhnou přes spodní okraj stránky
Následující příklad vykazuje záznamy o zákaznících, ale nic ve vykreslovacím kódu neví, ani se nestará o to, odkud řádky pocházejí. Originál používal starší TTable; dotaz FireDAC, datová sada v paměti nebo prosté pole záznamů krmí stejné rutiny beze změny. Záleží na tom, že můžete data procházet řádek po řádku a z každého z nich přečíst čtyři textová pole. Oddělte vykreslování od zdroje dat a budete moci měnit jednu nebo druhou stranu, aniž byste tu druhou narušili
Geometrie sloupce je na prvním místě
Než se nakreslí jediný znak, rozhodněte se, kde bude žít každý sloupec. Tabulka zde má čtyři sloupce, takže potřebuje čtyři levé okraje a známý pravý okraj. Pevné zakódování magického čísla (hard-coding) při každém volání TextOut, jak to dělávají rychlé ukázky, je přesně to, kvůli čemu je pozdější rozšíření tabulky tak bolestivé. Pojmenujte okraje jednou, v bodech od levého dolního počátku, a každé volání vykreslování se na ně bude odkazovat jménem:
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;
Tady se vyplatí dva detaily. Stínovaný pás je nakreslen jako první a pak text přes něj, protože pořadí malování je v PDF z-order: vyplňte obdélník až po textu a pohřbíte řádek. A střídání stínů není ozdobou samo pro sebe. Na hustém reportu je to nejlevnější způsob, jak zabránit tomu, aby oko sklouzlo na špatný řádek, a to je důvod, proč smyčka později překlopí boolean na každém řádku a předá jej přímo do Shaded
Výše uvedené pozice sloupců jsou pevné, což je u reportu, jehož schéma ovládáte, upřímné. Když jsou data proměnná, měřte, místo abyste hádali. HotPDF odhaluje měření šířky textu na objektu stránky, takže produkční verze PrintRow může vzít nejdelší očekávanou hodnotu v každém sloupci, změřit ji jednou ve zvolené velikosti písma a odvodit levé okraje z těchto šířek plus mezera. Tvar rutiny se nemění; mění se pouze zdroj konstant
Hlavička, linky a jedno místo, které je vlastní
Tabulka, která scroluje mimo stránku a pokračuje na další, je bez popisků sloupců nečitelná. Řešením je považovat hlavičku za něco, co překreslujete, nikoli za něco, co nakreslíte jen jednou. Dejte názvy sloupců a vodorovné linky, které je rámují, do jediné rutiny, a volejte tuto rutinu jak na začátku, tak znovu pokaždé, když otevřete novou stránku. Vzhledem k tomu, že hlavička i tělo sdílejí stejné konstanty sloupců, přirozeně se zarovnají
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;
Všimněte si, že DrawHeader bere Y referencí a posouvá je vpřed. Volající si nikdy nemusí pamatovat, jak vysoká je hlavička; rutina, která ji kreslí, to ví. Toto pravidlo jediného vlastníka je to, co brání rozvržení v posouvání, když do pruhu hlavičky později přidáte logo nebo shrnutí filtru. Smyčka těla zůstává nevšímavá. Zkrátka dál kreslí řádky odtud, kam momentálně ukazuje Y
Samotné linky představují rozdíl mezi seznamem a tabulkou. Svislé oddělovače sloupců představují stejný princip aplikovaný na osu x: MoveTo / LineTo / Stroke na hraně každého sloupce, tažené od horní linky ke spodní straně posledního řádku na stránce. V ukázce se držíme vodorovných linek, abychom zachovali čitelnost, ale jakmile existují konstanty sloupců, je tento krok do produkce již mechanický
Smyčka kurzoru vlastní zalomení stránky
Vykreslování je ta snazší polovina. To, co odděluje hračku od reportu, je stránkování: vědět ještě před nakreslením řádku, jestli se tam ještě vejde, a začít čerstvou stránku s novou hlavičkou, když se už nevejde. Toto rozhodnutí patří přesně na jedno místo, do smyčky, která prochází data, a nikam jinam
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;
Celou smyčku pohánějí dvě skutečnosti ze souřadnic. PDF měří y od levého dolního rohu směrem nahoru, takže řádky pochodují na stránce dolů tak, že se pokaždé odečte RowStep od Y, a test zaplnění stránky se spustí v okamžiku, kdy Y klesne pod spodní okraj, než aby přesáhl nějaký horní. Spleťte si směr a váš první řádek se vytiskne mimo spodní okraj, zatímco si smyčka myslí, že má k dispozici celou stránku
Druhá skutečnost nachytá téměř každého. AddPage vytvoří novou stránku a nasměruje na ni CurrentPage, ale nic s sebou nepřesouvá: ani písmo, ani barvu výplně, ani polohu. Proto je po každém AddPage Page znovu načtena z CurrentPage a proto je před řádky těla opětovně vydán příkaz SetFont. Pokud přeskočíte opětovné čtení, dál kreslíte na stránku, kterou jste právě opustili; pokud přeskočíte písmo, nová stránka se vykreslí v jakémkoli výchozím písmu, ke kterému se prohlížeč vrátí
Případy, které rozbijí exportér tabulek
Většina chyb v tabulkách se neprojeví na bezproblémové cestě (happy path) několika desítek upravených řádků. Žijí na okrajích a okraje se dají snadno (levně) testovat, jakmile víte, kde jsou
- Prázdné datové sady. Smyčka přes nula řádků vytvoří stránku s hlavičkou a ničím pod ní, což alespoň vypadá jako záměr. Prázdná stránka bez hlavičky vypadá jako chyba. Než to vypustíte do světa, rozhodněte se, co chcete
- Řádek, který padne přesně na hranici. Vygenerujte report, jehož poslední řádek sedí jeden krok nad okrajem, pak takový, jehož další řádek je jeden krok pod ním. Chyba stránkování off-by-one se skrývá do chvíle, kdy mají data přesně nesprávnou délku
- Příliš dlouhé hodnoty. Název společnosti širší než její sloupec zaběhne do dalšího. Změřte pole a rozhodněte se pro strategii: zalomení na druhý řádek, oříznutí nebo zkrácení pomocí elipsy (tři tečky). Mlčení není strategie
- Nulová pole. Přímé načtení hodnoty null do
TextOutmůže vyplout napovrch jako doslovný textNullnebo jako prázdné místo, záleží na tom, jak jej převedete. Zvolte si vykreslování cíleně, nenechte variantní konverzi rozhodovat za vás
Předtím, než to prohlásíte za hotové, prožeňte výsledek více než jedním prohlížečem. Nahrazování písem a ořezávání se u různých vykreslovačů chová odlišně a tabulka, která v jednom PDF prohlížeči vypadá spořádaně, může ve druhém vykazovat rozhozený sloupec nebo oříznuté město. Ověřte si, že opakovaná hlavička, stínování řádků a okraje přežijí přesun a že čísla stránek zůstávají po překročení hranice dat kontinuální
To, že si nakreslíte mřížku sami namísto spoléhání se na vizuální návrhář reportů, znamená více kódu, a tento kompromis stojí za to si na rovinu pojmenovat: vlastníte každou souřadnici, což je přesně to, co chcete pro dávkové úlohy na straně serveru, faktury a audity exportů, které se musí na každém stroji vykreslit identicky, a přesně ta režie, které byste se raději vyhnuli u jednorázového interního výpisu. U prvně jmenovaného se ta kontrola zaplatí hned napoprvé, kdy report musí vypadat v produkci stejně jako u vás na stole
Pravidla a stínované pruhy výše se opírají o stejná vektorová a barevná primitiva, která jsou probírána v návodu ke kreslení na plátno, pokud chcete nejprve probrat samostatně volání Rectangle, MoveTo a LineTo. Vykreslovací primitiva zde použitá jsou součástí komponenty HotPDF pro Delphi a C++Builder