Tehnični članak

Izris podatkovne tabele v PDF v Delphi s HotPDF

Nabor podatkov vsebuje vrstice in stolpce, stran PDF pa je prazna koordinatna mreža, ki ne pozna nobene od teh dveh kategorij. Premoščanje te vrzeli je celotna naloga. V HotPDF ni klica DrawTable, ki bi sprejel nabor podatkov in vam predal oblikovano mrežo. Namesto tega dobite primitive, iz katerih je mreža sestavljena: TextOut za postavitev niza na točko, SetFont za izbiro pisave, Rectangle in Fill za senčenje pasu ter MoveTo / LineTo / Stroke za risanje črt. Delujoč izvoznik tabel je disciplina pretvarjanja razmišljanja o vrsticah in stolpcih v eksplicitne koordinate x in y ter ohranjanja teh koordinat pravilnih, ko podatki prekoračijo spodnji rob strani

Primer, ki sledi, prikazuje evidence strank, toda nič v kodi za izrisovanje ne ve in mu ni mar, od kod vrstice prihajajo. Prvotni primer je uporabljal starejšo komponento TTable; poizvedba FireDAC, nabor podatkov v pomnilniku ali navadna matrika zapisov pa napajata enake rutine nespremenjene. Pomembno je le, da se po podatkih premikate eno vrstico naenkrat in iz vsake preberete štiri nize polj. Ločite izrisovanje od vira podatkov in katera koli stran se lahko spremenila ne da bi motila drugo

Geometrija stolpcev pride na vrsto najprej

Preden se izriše en sam znak, določite, kje se nahaja vsak stolpec. Tabela ima tukaj štiri stolpce, zato potrebuje štiri leve robove in znan desni rob. Trdo kodiranje magičnih številk pri vsakem klicu TextOut, kar je značilno za hitre vzorce, je točno to, kar naredi poznejše razširjanje tabele boleče. Enkrat poimenujte robove, v točkah od levega spodnjega izhodišča, in vsak klic za izrisovanje se nanje sklicuje 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;

Dve podrobnosti si zaslužita pozornost. Sencovan pas se izriše najprej, nato besedilo na vrhu, ker je vrstni red slikanja v PDF vrstni red z (z-order): če napolnite pravokotnik za besedilom, zakopljete vrstico. Izmenično senčenje pa ni dekoracija sama sebi namenom. Na gosti poročilni strani je to najcenejši način, da oko preprečite drseti na napačno vrstico - zato zanka pozneje obrne logično vrednost pri vsaki vrstici in jo neposredno posreduje v Shaded

Položaji stolpcev zgoraj so fiksni, kar je pošteno za poročilo, katerega shemo nadzorujete. Ko so podatki spremenljivi, merite namesto ugibanja. HotPDF izpostavlja merjenje širine besedila na objektu strani, zato lahko produkcijska različica PrintRow vzame najdaljšo pričakovano vrednost v vsakem stolpcu, jo enkrat izmeri pri izbrani velikosti pisave in iz teh širin plus vmesnega prostora izpelje leve robove. Oblika rutine se ne spremeni; spremenjen bo le vir konstant

Glava, črte in eno mesto, ki ju hrani

Tabela, ki se nadaljuje na naslednji strani brez oznak stolpcev, je neberljiva. Rešitev je, da glavo obravnavate kot nekaj, kar znova narišete, ne kot nekaj, kar narišete enkrat. Postavite naslove stolpcev in vodoravne črte, ki jih uokvirjajo, v eno rutino in to rutino pokličite tako ob začetku kot vsakič, ko odprete novo stran. Ker glava in telo delita enake konstante stolpcev, se samodejno poravnata

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;

Opazite, da DrawHeader jemlje Y po referenci in ga premakne naprej. Klicatelj si nikoli ne sme zapomniti, kako visoka je glava; rutina, ki jo riše, je tista, ki to ve. To pravilo enotnega lastništva je tisto, kar ohranja postavitev pred odstopanjem, ko pozneje dodate logotip ali povzetek filtra v pas glave. Telesna zanka ostane nevedna. Preprosto nadaljuje z risanjem vrstic od tam, kamor trenutno kaže Y

Same črte so razlika med seznamom in tabelo. Navpični ločevalniki stolpcev so enaka ideja, aplicirana na os x: MoveTo / LineTo / Stroke pri vsakem robu stolpca, od zgornje črte do dna zadnje vrstice na strani. Vzorec se drži vodoravnih črt zaradi berljivosti, toda produkcijski korak je mehanski, ko konstante stolpcev obstajajo

Zanka kazalca upravlja prelom strani

Izrisovanje je lažja polovica. Polovica, ki loči igračo od poročila, je oštevilčevanje strani: vedeti, preden narišete vrstico, ali še ustreza, in začeti svežo stran s svežo glavo, ko ne. Ta odločitev spada natanko na eno mesto - v zanko, ki hodi po podatkih - in nikamor drugam

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;

Dve koordinatni dejstvi poganjata celotno zanko. PDF meri y navzgor od levega spodnjega kota, zato vrstice potujejo navzdol po strani z odštevanjem RowStep od Y vsakič, preizkus polne strani pa se sproži, ko Y pade pod spodnji rob in ne nad neki vrh. Zamenjajte smer in vaša prva vrstica se izriše pod spodnjim robom, medtem ko zanka misli, da ima celo stran prostora

Drugo dejstvo ujame skoraj vsakogar enkrat. AddPage ustvari novo stran in preusmeri CurrentPage nanjo, toda ne prenese ničesar: ne pisave, ne barve polnjenja, ne položaja. Zato se Page ponovno prebere iz CurrentPage po vsakem AddPage in zato se SetFont znova pokliče pred telesnimi vrsticami. Preskočite vnovično branje in nadaljujete z izrisovanjem na stran, ki ste jo pravkar pustili za seboj; preskočite pisavo in nova stran se izriše s katero koli privzeto vrednostjo, na katero se bralnik vrne

Primeri, ki pokvarijo izvoznik tabel

Večina napak tabel se ne pokaže na srečni poti z nekaj desetinami urejenih vrstic. Živijo na robovih, robovi pa so poceni za testiranje, ko enkrat veste, kje so

  • Prazni nabori podatkov. Zanka prek nič vrstic proizvede stran z glavo in ničemer pod njo, kar vsaj izgleda namerno. Prazna stran brez glave izgleda kot napaka. Odločite se, katero želite, preden pošljete
  • Vrstica, ki pade točno na mejo. Ustvarite poročilo, katerega zadnja vrstica sedi en korak nad robom, nato pa takega, katerega naslednja vrstica je en korak pod njim. Napaka oštevilčevanja strani za eno se skriva, dokler podatki niso točno napačne dolžine
  • Predolge vrednosti. Ime podjetja, širše od svojega stolpca, bo prekoračilo v naslednji. Izmerite polje in se odločite za politiko: prelom na drugo vrstico, odrezovanje ali okrajšava z elipso. Molk ni politika
  • Polja Null. Branje vrednosti null neposredno v TextOut se lahko pojavi kot dobesedno besedilo Null ali kot prazno, odvisno od tega, kako ga pretvorite. Namerno izberite izrisovanje namesto da to prepustite pretvorbi variante

Zaženite rezultat skozi več bralnikov, preden ga razglasite za dokončanega. Nadomeščanje pisav in odrezovanje se obnašata različno v različnih bralnikih, in tabela, ki izgleda pravilna v enem bralnikih PDF, lahko v drugem pokaže neporavnan stolpec ali odrezan naziv mesta. Potrdite, da ponavljajoča se glava, senčenje vrstic in robovi preživijo premik ter da številke strani ostanejo zvezne, ko podatki prečkajo mejo

Risanje mreže sami namesto zanašanja na vizualnega oblikovalca poročil zahteva več kode, kompromis pa si zasluži, da ga jasno poimenujemo: imate v lasti vsako koordinato, kar je točno tisto, kar želite za strežniška paketna opravila, račune in revizijske izvoze, ki morajo biti enako izrisani na vsaki napravi, in točno tisto obremenitev, ki bi se ji raje izognili za enkratni interni seznam. Za prej naštete primere se nadzor povrne pri prvem poročilu, ki mora izgledati enako v produkciji kot na vaši mizi

Pravila in sencovani pasovi zgoraj se opirajo na enake vektorske in barvne primitive, obravnavane v vodičem za risanje na platnu, če želite, da se klici Rectangle, MoveTo in LineTo najprej obravnavajo samostojno. Primitive za izrisovanje, ki se tukaj uporabljajo, so del komponente HotPDF za Delphi in C++Builder