Technical Article

Renderiranje tablice podataka u PDF u Delphiju uz HotPDF

Skup podataka (dataset) sastoji se od redaka i stupaca; PDF stranica je prazna koordinatna mreža koja nema pojam ni o jednom ni o drugom. Premještanje tog jaza cijeli je posao ovdje. U HotPDF-u ne postoji poziv DrawTable koji prima skup podataka i daje vam oblikovanu tablicu. Umjesto toga, dobivate osnovne elemente od kojih je tablica izgrađena: TextOut za postavljanje teksta u točku, SetFont za odabir fonta, Rectangle i Fill za sjenčanje trake, te MoveTo / LineTo / Stroke za crtanje linija. Izrada funkcionalnog izvoza u tablicu zahtijeva disciplinu pretvaranja razmišljanja o redcima i stupcima u eksplicitne X i Y koordinate, te održavanje tih koordinata ispravnima kada podaci prijeđu na novu stranicu.

Sljedeći primjer prikazuje zapise o kupcima, ali ništa u kodu za crtanje ne zna niti mari za to odakle dolaze ti retci. Izvornik je koristio zastarjeli TTable; FireDAC upit, in-memory dataset ili običan niz zapisa (records) napajaju iste rutine bez ikakvih promjena. Važno je samo da možete proći kroz podatke jedan po jedan redak i pročitati četiri tekstualna polja iz svakoga. Držite renderiranje odvojenim od izvora podataka pa ćete moći mijenjati bilo koju stranu bez utjecaja na drugu.

Geometrija stupaca je na prvom mjestu

Prije nego što nacrtate ijedan znak, odlučite gdje se nalazi svaki stupac. Tablica ovdje ima četiri stupca, pa joj trebaju četiri lijeva ruba i poznata desna margina. Hardkodiranje fiksnih brojeva pri svakom pozivu TextOut, kako se to obično radi u brzim primjerima, upravo je ono što kasnije otežava proširivanje tablice. Definirajte rubove jednom, u točkama od donjeg lijevog ishodišta, i svaki poziv crtanja upućivat će na njih po imenu:

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 detalja ovdje opravdavaju svoju primjenu. Sjenčana traka se crta prva, a zatim tekst preko nje jer je redoslijed crtanja zapravo z-redoslijed u PDF-u: ako ispunite pravokutnik nakon pisanja teksta, prekrit ćete cijeli redak. Također, izmjenično sjenčanje nije samo ukras. U zgusnutom izvješću to je najjednostavniji način da se spriječi bježanje očiju u pogrešan redak, zbog čega petlja kasnije mijenja vrijednost booleana za svaki redak i prosljeđuje ga izravno u parametar Shaded.

Gore navedeni položaji stupaca su fiksni, što je ispravno za izvješće čiju shemu kontrolirate. Kada su podaci varijabilni, mjerite ih umjesto nagađanja. HotPDF omogućuje mjerenje širine teksta na objektu stranice, pa produkcijska verzija PrintRow može uzeti najdulju očekivanu vrijednost u svakom stupcu, izmjeriti je jednom pri odabranoj veličini fonta te izvesti lijeve rubove iz tih širina uvećanih za razmak. Oblik same rutine se ne mijenja; mijenja se samo izvor konstanti.

Zaglavlje, linije i jedno mjesto koje njima upravlja

Tablica koja prelazi na novu stranicu i nastavlja se bez oznaka stupaca potpuno je nečitljiva. Rješenje je tretirati zaglavlje kao nešto što ponovno crtate, a ne kao nešto što crtate samo jednom. Postavite nazive stupaca i vodoravne linije koje ih uokviruju u jednu rutinu, te pozovite tu rutinu na početku i ponovno svaki put kada otvorite novu stranicu. Budući da zaglavlje i tijelo dijele iste konstante stupaca, oni se automatski savršeno poravnavaju.

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;

Primijetite da DrawHeader prima Y po referenci (var) i pomiče ga naprijed. Pozivatelj nikada ne mora pamtiti koliko je zaglavlje visoko; rutina koja ga crta ujedno je i rutina koja zna njegovu visinu. To pravilo jedinstvenog vlasništva sprječava pomicanje rasporeda ako kasnije u zaglavlje dodate logotip ili sažetak filtra. Glavna petlja tijela tablice o tome ne mora brinuti. Ona samo nastavlja crtati retke od tamo gdje Y trenutačno pokazuje.

Same linije čine razliku između popisa i tablice. Okomiti razdjelnici stupaca su ista ideja primijenjena na os X: MoveTo / LineTo / Stroke na svakom rubu stupca, povučeni od gornje linije do dna posljednjeg retka na stranici. Primjer se drži samo vodoravnih linija radi lakšeg čitanja, ali produkcijski korak je mehanički čim definirate konstante stupaca.

Petlja pokazivača upravlja prijelomom stranice

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;

Dvije činjenice o koordinatama pokreću cijelu petlju. PDF mjeri Y prema gore od donjeg lijevog kuta, pa se retci pomiču dolje niz stranicu tako što se svaki put oduzima RowStep od Y, a provjera ispunjenosti stranice aktivira se kada Y padne ispod donje margine, a ne iznad nekog vrha. Ako pogriješite smjer, vaš će se prvi redak iscrtati izvan donjeg ruba dok će petlja misliti da još ima cijelu stranicu prostora.

Druga činjenica s kojom se gotovo svi susretnu barem jednom: AddPage stvara novu stranicu i preusmjerava CurrentPage na nju, ali ne prenosi ništa sa sobom: ni font, ni boju punjenja, ni položaj. Zbog toga se Page ponovno učitava iz CurrentPage nakon svakog AddPage, i zato se SetFont mora ponovno izdati prije iscrtavanja redaka sadržaja. Preskočite ponovno učitavanje i nastavit ćete crtati po stranici koju ste upravo napustili; preskočite postavljanje fonta i nova stranica će se prikazati u zadanim postavkama koje preglednik odabere.

Slučajevi koji kvare izvoz tablice

Većina bugova s tablicama ne pojavljuje se u uobičajenim scenarijima s nekoliko desetaka urednih redaka. Oni žive na rubovima, a rubne slučajeve je lako testirati čim saznate gdje se nalaze.

  • Prazni skupovi podataka. Petlja kroz nula redaka proizvodi stranicu sa zaglavljem i ničim ispod njega, što barem izgleda namjerno. Potpuno prazna stranica bez zaglavlja izgleda kao pogreška. Odlučite što želite prije isporuke.
  • Redak koji slijeće točno na granicu. Generirajte izvješće čiji se zadnji redak nalazi točno jedan korak iznad margine, a zatim ono u kojem je sljedeći redak jedan korak ispod nje. Paginacija s pogreškom za jedan redak (off-by-one) ostaje skrivena dok podaci ne budu točno kritične duljine.
  • Predugačke vrijednosti. Naziv tvrtke koji je širi od svog stupca prelit će se u sljedeći. Izmjerite polje i odlučite o pravilu: prijelom u drugi redak, odsijecanje ili skraćivanje s tri točke. Ignoriranje problema nije rješenje.
  • Null polja. Slanje null vrijednosti izravno u TextOut može se prikazati kao doslovni tekst Null ili kao praznina, ovisno o tome kako je pretvarate. Odaberite način renderiranja namjerno umjesto da dopustite konverziji varijanata da to učini umjesto vas.

Provjerite rezultat u više preglednika prije nego što ga smatrate dovršenim. Zamjena fontova i odsijecanje ponašaju se različito u različitim rendererima, a tablica koja izgleda pravilno u jednom PDF čitaču može pokazati nepravilno poravnate stupce ili odsječena imena gradova u drugome. Potvrdite da ponovljeno zaglavlje, sjenčanje redaka i margine preživljavaju promjene te da brojevi stranica ostaju neprekinuti nakon što podaci prijeđu granicu stranice.

Samostalno crtanje tablice umjesto oslanjanja na vizualni dizajner izvješća zahtijeva više koda, a taj kompromis vrijedi jasno definirati: vi posjedujete svaku koordinatu, što je točno ono što želite za serverske batch zadatke, račune i revizijske izvoze koji se moraju prikazati identično na svakom stroju, ali je ujedno i dodatni posao koji biste radije izbjegli za jednokratni interni ispis. U prvom slučaju, ta se kontrola isplati već prvi put kada izvješće mora u produkciji izgledati točno onako kako je izgledalo na vašem stolu.

Linije i sjenčane trake u gornjim primjerima oslanjaju se na iste vektorske i bojne primitive opisane u pregledu crtanja po platnu, ako želite najprije zasebno proučiti pozive Rectangle, MoveTo i LineTo. Primitive za crtanje korištene ovdje dio su komponente HotPDF Component za Delphi i C++Builder.