Technický článek

Vykreslení datové tabulky do PDF v Delphi s HotPDF

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:

Geometrie sloupců tabulky HotPDF v Delphi: pojmenované hrany x na 70, 110, 300 a 480 bodech mezi linkami rámu na 50 a 570 bodech
Čtyři pojmenované levé hrany a známý pravý okraj fixují celou geometrii tabulky dřív, než proběhne první volání TextOut
const
  ColNo   = 70;    // levý okraj sloupce "No."
  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;    // svislá vzdálenost mezi účařími

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // Stínovaný pás za řádkem; Rectangle přijímá 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í

HotPDF překreslí titulky a linky DrawHeader na straně jedna a znovu po každém AddPage, takže obě strany PDF otvírají se shodným záhlavím
Hlavičková rutina se spustí znovu na každé nové stránce, takže titulky a linky dopadnou na totéž místo už konstrukcí
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Vlevo: popisek zdroje a číslo stránky; vpravo: čas generování
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Dvě vodorovné linky ohraničující názvy sloupců
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // Názvy sloupců v tučnějším řezu, aby působily jako nadpisy
  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;  // posuňte se za ohraničené záhlaví před prvním řádkem těla
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

Vývojový diagram smyčky kurzoru Delphi, kde Y pod 60 spustí AddPage, znovu přečtení CurrentPage, znovu vystavení SetFont a opakované záhlaví před dalším řádkem tabulky
Smyčka kurzoru je jediné místo, které otevře čerstvou stránku a znovu ji inicializuje, když Y klesne pod dolní okraj
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;

    // Název sestavy jednou nahoře na první stránce
    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
      // Došlo místo? Otevřete novou stránku a zopakujte tam záhlaví
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage posune CurrentPage vpřed
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont je nutné znovu zavolat na každé nové stránce
      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 TextOut může vyplout napovrch jako doslovný text Null nebo 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