Tehnički članak

Prikazivanje (renderovanje) tabele podataka u PDF u programskom jeziku Delphi pomoću HotPDF-a

Skup podataka su redovi i kolone; PDF stranica je prazna koordinatna mreža bez ikakvog pojma ni o jednom ni o drugom. Premošćavanje tog jaza je čitav posao ovde. U HotPDF-u ne postoji poziv DrawTable koji prima skup podataka i pruža vam formatiranu mrežu. Ono što dobijate umesto toga su primitivi od kojih je mreža sazdana: TextOut za postavljanje niske u tačku, SetFont za izbor njenog pisma, Rectangle i Fill za senčenje trake, i MoveTo / LineTo / Stroke za crtanje linija. Radan izvoznik tabela je disciplina pretvaranja razmišljanja u redovima i kolonama u eksplicitne x i y koordinate, a zatim održavanje tih koordinata poštenim kada podaci pređu dno stranice

Primer koji sledi izveštava o zapisima kupaca, ali ništa u kodu za crtanje ne zna niti mari odakle redovi dolaze. Original je koristio zastareli TTable; FireDAC upit, skup podataka u memoriji, ili obični niz zapisa napaja iste rutine nepromenjene. Ono što je bitno je da možete proći kroz podatke jedan red po jedan i pročitati četiri tekstualna polja iz svakog. Držite iscrtavanje odvojeno od izvora podataka i možete promeniti bilo koju stranu bez ometanja druge

Geometrija kolona dolazi prva

Pre nego što se nacrta i jedan znak, odlučite gde svaka kolona živi. Ovde tabela ima četiri kolone, pa joj trebaju četiri leve ivice i poznata desna margina. Tvrdo kodiranje magičnog broja pri svakom pozivu TextOut, kako brzi primeri obično rade, je upravo ono što kasnije čini proširivanje tabele bolnim. Imenujte ivice jednom, u tačkama od koordinatnog početka u donjem levom uglu, i svaki poziv za crtanje se poziva na njih po imenu:

HotPDF geometrija kolona tabele u Delphi-ju: imenovane x ivice na 70, 110, 300 i 480 tačaka između pravila okvira na 50 i 570 tačaka
Četiri imenovane leve ivice i poznata desna margina učvršćuju celu geometriju tablice pre prvog poziva TextOut
const
  ColNo   = 70;    // leva ivica kolone "No."
  ColName = 110;   // naziv kompanije
  ColAddr = 300;   // adresa ulice
  ColCity = 480;   // grad
  RowLeft = 50;    // okvir tabele: leva linija
  RowRight = 570;  // okvir tabele: desna linija
  RowStep = 20;    // vertikalno rastojanje između baznih linija

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // Osenčena traka iza reda. Rectangle prima 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 ovde opravdavaju svoje postojanje. Osenčena traka se crta prva, a zatim tekst preko nje, jer je redosled crtanja z-red u PDF-u: popunite pravougaonik posle teksta i zatrpaćete red. A naizmenično senčenje nije ukras radi ukrasa. Na gustom izveštaju to je najjeftiniji način da se spreči da oko sklizne na pogrešan red, i zato petlja kasnije okreće bulovu vrednost na svakom redu i prosleđuje je direktno u Shaded

Pozicije kolona iznad su fiksne, što je pošteno za izveštaj čiju šemu kontrolišete. Kada su podaci promenljivi, merite umesto da nagađate. HotPDF izlaže merenje širine teksta na objektu stranice, tako da produkciona verzija PrintRow može uzeti najdužu očekivanu vrednost u svakoj koloni, izmeriti je jednom pri izabranoj veličini fonta, i izvesti leve ivice iz tih širina plus razmak. Oblik rutine se ne menja; menja se samo izvor konstanti

Zaglavlje, linije, i jedno mesto koje njima upravlja

Tabela koja se prelije preko stranice i nastavi na sledećoj bez naslova kolona je nečitljiva. Rešenje je tretirati zaglavlje kao nešto što ponovo crtate, ne kao nešto što crtate jednom. Stavite naslove kolona i horizontalne linije koje ih uokviruju u jednu rutinu, i pozovite tu rutinu i na početku i ponovo svaki put kada otvorite novu stranicu. Pošto zaglavlje i telo dele iste konstante kolona, one se poravnavaju samom konstrukcijom

HotPDF ponovo crta DrawHeader naslove i pravila na prvoj strani i ponovo posle svakog AddPage tako obe PDF strane otvaraju identično zaglavlje
Zaglavlja rutina radi ponovo na svakoj novoj stranici, pa naslovi i linije sleću na isto mesto po konstrukciji
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Levo: oznaka izvora i broj stranice. Desno: vreme generisanja.
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Dve horizontalne linije koje uokviruju naslove kolona.
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // Naslovi kolona, u debljem pismu tako da se čitaju kao naslovi.
  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;  // pomeri se preko uokvirenog zaglavlja pre prvog reda tela
end;

Primetite da DrawHeader prima Y po referenci i pomera ga napred. Pozivalac nikada ne mora da pamti koliko je zaglavlje visoko; rutina koja ga crta je rutina koja to zna. To pravilo jednog vlasništva je ono što sprečava da se raspored razjedini kada kasnije dodate logotip ili sažetak filtera u traku zaglavlja. Petlja tela ostaje ravnodušna. Ona samo nastavlja da crta redove odakle god Y trenutno pokazuje

Same linije su razlika između liste i tabele. Vertikalni razdvajači kolona su ista ideja primenjena na x osu: MoveTo / LineTo / Stroke na svakoj ivici kolone, koji se protežu od gornje linije do dna poslednjeg reda na stranici. Primer se drži horizontalnih linija radi čitljivosti, ali je produkcioni korak mehanički čim konstante kolona postoje

Petlja kursora upravlja prelomom stranice

Crtanje je laka polovina. Polovina koja razdvaja igračku od izveštaja je paginacija: znanje, pre nego što nacrtate red, da li još stane, i započinjanje nove stranice sa novim zaglavljem kada ne stane. Ta odluka pripada tačno jednom mestu, petlji koja prolazi kroz podatke, i nigde drugde

Dijagram toka Delphi petlje kursora gde Y ispod 60 okida AddPage, ponovno čitanje CurrentPage, ponovno izdavanje SetFont i ponovljeno zaglavlje pre sledećeg reda tabele
Petlja kursora je jedino mesto koje otvara svežu stranicu i ponovo je inicijalizuje kad Y padne ispod donje margine
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;

    // Naslov izveštaja, jednom, na vrhu prve stranice.
    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
      // Nema više mesta? Otvorite novu stranicu i ponovite zaglavlje tamo.
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage pomera CurrentPage napred
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont se mora ponovo izdati na svakoj novoj stranici
      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;

Dve koordinatne činjenice pokreću celu petlju. PDF meri y naviše od donjeg levog ugla, pa redovi koračaju naniže niz stranicu oduzimanjem RowStep od Y svaki put, a test za punu stranicu se aktivira kada Y padne ispod donje margine, a ne iznad neke gornje. Pogrešite smer i vaš prvi red će se odštampati van donje ivice dok petlja misli da ima celu stranicu prostora

Druga činjenica uhvati skoro svakog jednom. AddPage pravi novu stranicu i ponovo usmerava CurrentPage na nju, ali ne prenosi ništa: ni font, ni boju popune, ni poziciju. Zato se Page ponovo čita iz CurrentPage posle svakog AddPage, i zato se SetFont ponovo izdaje pre redova tela. Preskočite ponovno čitanje i nastavljate da crtate na stranicu koju ste upravo napustili; preskočite font i nova stranica se iscrtava u kojem god podrazumevanom obliku pregledač koristi kao rezervni

Slučajevi koji lome izvoznik tabela

Većina grešaka u tabelama se ne pojavljuje na srećnom putu od nekoliko desetina urednih redova. One žive na ivicama, a ivice je jeftino testirati kada jednom znate gde su

  • Prazni skupovi podataka. Petlja preko nula redova proizvodi stranicu sa zaglavljem i ničim ispod njega, što bar izgleda namerno. Prazna stranica bez zaglavlja izgleda kao greška. Odlučite šta želite pre isporuke
  • Red koji sleti tačno na granicu. Generišite izveštaj čiji poslednji red sedi jedan korak iznad margine, zatim onaj čiji naredni red je jedan korak ispod nje. Paginacija sa greškom za jedan se krije sve dok podaci nisu tačno pogrešne dužine
  • Predugačke vrednosti. Naziv kompanije širi od svoje kolone će se preliti u sledeću. Izmerite polje i odlučite se za politiku: prelomite u drugi red, isecite, ili skratite sa tri tačke. Ćutanje nije politika
  • Null polja. Čitanje null vrednosti direktno u TextOut može se pojaviti kao doslovan tekst Null ili kao prazno, u zavisnosti od toga kako je konvertujete. Izaberite iscrtavanje namerno umesto da konverzija varijante bira umesto vas

Provucite rezultat kroz više od jednog pregledača pre nego što proglasite da je gotovo. Zamena fonta i isecanje se različito ponašaju kroz različite rendere, a tabela koja izgleda uredno u jednom PDF čitaču može pokazati neporavnatu kolonu ili isečen grad u drugom. Potvrdite da ponovljeno zaglavlje, senčenje redova, i margine prežive prelaz, i da brojevi stranica ostanu neprekidni nakon što podaci pređu granicu

Ručno crtanje mreže umesto oslanjanja na vizuelni dizajner izveštaja je više koda, i tu razmenu vredi jasno imenovati: vi posedujete svaku koordinatu, što je tačno ono što želite za paketne poslove na serverskoj strani, fakture, i revizorske izvoze koji moraju identično da se iscrtaju na svakoj mašini, i tačno onaj režijski trošak koji biste radije izbegli za jednokratni interni spisak. Za ono prvo, kontrola se isplati prvi put kada izveštaj mora da izgleda isto u produkciji kao što je izgledao na vašem stolu

Linije i osenčene trake iznad se oslanjaju na iste vektorske i primitive boje pokrivene u vodiču kroz crtanje na platnu, ako želite da pozivi Rectangle, MoveTo, i LineTo budu obrađeni prvo za sebe. Primitivi za crtanje korišćeni ovde su deo HotPDF Delphi Component-a za Delphi i C++Builder