Technical Article

Vykreslenie dátovej tabuľky do PDF v Delphi pomocou HotPDF

Dataset (dátová sada) pozostáva z riadkov a stĺpcov; stránka PDF je však prázdna súradnicová mriežka, ktorá nemá o riadkoch ani stĺpcoch žiadnu predstavu. Preklenutie tejto medzery je hlavnou úlohou tohto návodu. V HotPDF neexistuje žiadne volanie DrawTable, ktoré by prevzalo dataset a vygenerovalo naformátovanú mriežku. Namiesto toho máte k dispozícii základné prvky, z ktorých sa mriežka skladá: TextOut na umiestnenie reťazca na určitú pozíciu, SetFont na výber písma, Rectangle a Fill na tieňovanie riadkov a dvojicu MoveTo / LineTo / Stroke na kreslenie čiar. Funkčný exportér tabuliek vyžaduje previesť uvažovanie v riadkoch a stĺpcoch na explicitné súradnice X a Y a zabezpečiť ich správne správanie, keď dáta presiahnu spodný okraj stránky.

Nasledujúci príklad spracováva záznamy o zákazníkoch, no samotný kód pre kreslenie netuší a ani ho nezaujíma, odkiaľ tieto riadky pochádzajú. Pôvodný kód používal starší komponent TTable; dotaz FireDAC, in-memory dataset alebo obyčajné pole záznamov (records) však dokážu napájať rovnaké rutiny bez akýchkoľvek zmien. Dôležité je len to, aby ste dokázali prechádzať dáta po jednom riadku a z každého prečítať štyri textové polia. Udržiavajte vykresľovanie oddelené od zdroja dát a budete môcť meniť ktorúkoľvek stranu bez toho, aby ste ovplyvnili tú druhú.

Geometria stĺpcov na prvom mieste

Predtým, než vykreslíte prvý znak, definujte pozíciu každého stĺpca. Naša tabuľka má štyri stĺpce, takže potrebuje štyri ľavé okraje a známy pravý okraj. Zadávanie fixných hodnôt pri každom volaní TextOut (ako to býva v rýchlych ukážkach) je presne to, čo neskôr sťaží akékoľvek rozširovanie tabuľky. Zadefinujte okraje raz v bodoch od ľavého dolného rohu a všetky kresliace volania sa na ne budú odvolávať názvom:

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 detaily tu zohrávajú dôležitú rolu. Tieňovaný pás sa kreslí ako prvý a až potom text na ňom, pretože poradie kreslenia v PDF určuje aj z-order: ak vyplníte obdĺžnik až po texte, riadok ním prekryjete. Striedavé tieňovanie nie je len estetickou záležitosťou. V hustom reporte je to najjednoduchší spôsob, ako zabrániť tomu, aby oči čitateľa skĺzli na vedľajší riadok. Preto cyklus neskôr pri každom riadku prepína boolean hodnotu a odovzdáva ju parametru Shaded.

Vyššie uvedené pozície stĺpcov sú pevne stanovené, čo vyhovuje reportu, ktorého štruktúru máte pod kontrolou. Ak sú dáta premenlivé, namiesto odhadovania radšej merajte. HotPDF umožňuje meranie šírky textu priamo na objekte stránky, takže produkčná verzia metódy PrintRow môže vziať najdlhšiu očakávanú hodnotu pre každý stĺpec, raz ju zmerať pri vybranom písme a odvodiť ľavé okraje z týchto šírok plus prirážka (gutter). Štruktúra rutiny zostáva rovnaká, mení sa iba zdroj konštánt.

Hlavička, čiary a jedno spoločné miesto

Tabuľka, ktorá preteká na ďalšiu stránku a pokračuje na nej bez označenia stĺpcov, je nečitateľná. Riešením je pristupovať k hlavičke ako k niečomu, čo kreslíte opakovane, a nie iba raz. Umiestnite názvy stĺpcov a vodorovné čiary, ktoré ich ohraničujú, do jednej rutiny a túto rutinu zavolajte na začiatku a tiež zakaždým, keď otvoríte novú stránku. Vďaka tomu, že hlavička aj telo tabuľky zdieľajú rovnaké konštanty stĺpcov, budú automaticky zarovnané.

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šimnite si, že metóda DrawHeader preberá parameter Y referenciou a posúva ho vpred. Volajúci kód si nemusí pamätať výšku hlavičky; vie ju len tá rutina, ktorá ju kreslí. Toto pravidlo jedinej zodpovednosti zabraňuje tomu, aby sa rozvrhnutie rozpadlo, ak neskôr do záhlavia pridáte logo alebo sumár filtrov. Cyklus vykresľovania tela tabuľky o tom vôbec nevie. Pokračuje v kreslení riadkov od miesta, na ktoré aktuálne ukazuje Y.

Samotné čiary odlišujú zoznam od tabuľky. Zvislé oddeľovače stĺpcov sú založené na rovnakom princípe aplikovanom na os X: volania MoveTo / LineTo / Stroke na okraji každého stĺpca, vedené od hornej čiary až po spodok posledného riadku na stránke. Tento príklad kvôli čitateľnosti používa len vodorovné čiary, ale implementácia v produkcii je po definovaní konštánt stĺpcov čisto mechanickou záležitosťou.

Zalomenie stránky riadi cyklus dát

Kreslenie je tá jednoduchšia časť. To, čo odlišuje jednoduchú ukážku od skutočného reportu, je stránkovanie: schopnosť zistiť pred vykreslením riadku, či sa na stránku ešte zmestí, a vytvoriť novou stránku s novou hlavičkou, ak to tak nie je. Toto rozhodnutie patrí na jedno jediné miesto – do cyklu prechádzania dát – a nikam inam.

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;

Celý cyklus riadia dve vlastnosti súradníc. PDF meria Y smerom nahor od ľavého dolného rohu, takže riadky postupujú po stránke smerom nadol tým, že zakaždým odčítate RowStep od Y. Test zaplnenia stránky sa spustí vo chvíli, keď Y klesne pod dolný okraj, a nie nad horný. Ak si pomýlite smer, prvý riadok sa vykreslí mimo spodného okraja, zatiaľ čo cyklus si bude myslieť, že má k dispozícii ešte celú stránku.

Druhá skutočnosť zaskočí takmer každého vývojára. Metóda AddPage vytvorí novú stránku a nasmeruje na ňu CurrentPage, no neprenáša žiadny stav: ani písmo, ani farbu výplne, ani pozíciu. Preto sa premenná Page musí po každom AddPage znova načítať z CurrentPage a písmo SetFont sa musí nastaviť znova pred riadkami tela tabuľky. Ak vynecháte opätovné načítanie stránky, budete pokračovať v kreslení na predchádzajúcu stránku. Ak vynecháte nastavenie písma, nová stránka použije predvolené systémové písmo prehliadača.

Prípady, ktoré môžu exportér tabuliek znefunkčniť

Väčšina chýb pri kreslení tabuliek sa neprejaví pri bezproblémovom scenári s niekoľkými desiatkami úhľadných riadkov. Objavujú sa na hraničných prípadoch, ktoré sa dajú ľahko otestovať, ak voči tomu viete, kde ich hľadať.

  • Prázdne datasety. Cyklus nad nulovým počtom riadkov vygeneruje stránku s hlavičkou a ničím pod ňou, čo aspoň vyzerá zámerne. Úplne prázdna stránka bez hlavičky pôsobí ako chyba aplikácie. Pred nasadením sa rozhodnite, ktorý variant preferujete.
  • Riadok, ktorý padne presne na hranicu stránky. Vytvorte report, ktorého posledný riadok leží presne jeden krok nad spodným okrajom, a potom taký, kde ďalší riadok padne tesne pod neho. Chyby v zalamovaní o jeden riadok sa často neodhalia, kým dáta nemajú presne takúto kritickú dĺžku.
  • Príliš dlhé hodnoty. Názov spoločnosti širší ako stĺpec pretečie do susedného stĺpca. Odmerajte šírku poľa a rozhodnite o postupe: zalamovanie do druhého riadku, orezanie (clip) alebo skrátenie s trojbodkou. Nechať to bez riešenia nie je správny prístup.
  • Prázdne hodnoty (Null). Priame odovzdanie hodnoty Null do TextOut môže skončiť zobrazením textu „Null“ alebo prázdneho miesta v závislosti od spôsobu konverzie. Vyberte si spôsob zobrazenia vedome, namiesto toho, aby ste to nechali na automatickú konverziu typov.

Pred dokončením otestujte výsledok vo viacerých prehliadačoch PDF súborov. Nahrádzanie písiem a orezávanie sa v rôznych vykresľovačoch správajú odlišne. Tabuľka, ktorá vyzerá správne v jednom prehliadači, môže v inom zobraziť posunutý stĺpec alebo orezaný názov mesta. Uistite sa, že opakovaná hlavička, tieňovanie riadkov a okraje zostávajú neporušené a číslovanie strán plynule pokračuje aj po prechode na novú stránku.

Ručné vykresľovanie mriežky namiesto použitia vizuálneho návrhára reportov vyžaduje viac kódu. Tento kompromis má však jasné výhody: plne kontrolujete každú súradnicu. To je presne to, čo potrebujete pre dávkové spracovanie na serveri, faktúry alebo audítorské exporty, ktoré sa musia vykresliť identicky na každom počítači. Pre jednorazové interné výpisy je to však zbytočná réžia navyše. V prvom prípade sa však táto kontrola oplatí hneď pri prvom reporte, ktorý musí v produkcii vyzerať rovnako ako na vašom vývojárskom stole.

Čiary a tieňované pásy popísané vyššie využívajú rovnaké vektorové a farebné prvky, akým sa venuje sprievodca kreslením na plátno, pokiaľ sa chcete najprv zamerať na volania Rectangle, MoveTo a LineTo. Vykresľovacie prvky použité v tomto článku sú súčasťou komponentu HotPDF Component pre Delphi a C++Builder.