Articol tehnic

Tabel de date randat în PDF cu HotPDF în Delphi

Un set de date înseamnă rânduri și coloane; o pagină PDF este o grilă goală de coordonate, care nu are noțiunea niciuneia dintre ele. Acoperirea acestei distanțe este toată treaba de aici. Nu există în HotPDF niciun apel DrawTable care să primească un set de date și să vă înapoieze o grilă formatată. În schimb, primiți primitivele din care este făcută o grilă: TextOut pentru a plasa un șir într-un punct, SetFont pentru a-i alege fontul, Rectangle și Fill pentru a colora o bandă și MoveTo / LineTo / Stroke pentru a trasa linii. Un exportator de tabele care funcționează este disciplina de a transforma gândirea în rânduri și coloane în coordonate x și y explicite, și apoi de a păstra acele coordonate oneste atunci când datele trec dincolo de partea de jos a paginii

Exemplul care urmează raportează înregistrări de clienți, dar nimic din codul de desenare nu știe și nu îl interesează de unde vin rândurile. Originalul folosea un vechi TTable; o interogare FireDAC, un set de date în memorie sau un simplu tablou de înregistrări alimentează aceleași rutine nemodificate. Ce contează este să puteți parcurge datele rând cu rând și să citiți patru câmpuri de tip șir din fiecare. Țineți randarea separată de sursa de date și puteți schimba oricare parte fără să o deranjați pe cealaltă

Geometria coloanelor vine prima

Înainte să fie desenat un singur caracter, hotărâți unde trăiește fiecare coloană. Tabelul are aici patru coloane, deci are nevoie de patru margini din stânga și de o margine din dreapta cunoscută. Scrierea unui număr magic la fiecare apel TextOut, așa cum tind să facă exemplele rapide, este exact ce face dureroasă lărgirea ulterioară a unui tabel. Denumiți marginile o singură dată, în puncte de la originea din stânga jos, iar fiecare apel de desenare le va referi pe nume:

Geometria coloanelor unui tabel HotPDF în Delphi: margini x denumite la 70, 110, 300 și 480 de puncte, între liniile de cadru de la 50 și 570 de puncte
Patru margini din stânga denumite și o margine din dreapta cunoscută fixează toată geometria tabelului înainte de primul apel TextOut
const
  ColNo   = 70;    // marginea din stânga a coloanei „No.”
  ColName = 110;   // numele companiei
  ColAddr = 300;   // adresa
  ColCity = 480;   // orașul
  RowLeft = 50;    // cadrul tabelului: linia din stânga
  RowRight = 570;  // cadrul tabelului: linia din dreapta
  RowStep = 20;    // distanța verticală dintre liniile de bază

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // O bandă colorată în spatele rândului. Rectangle primește 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;

Două detalii își merită locul aici. Banda colorată este desenată prima, apoi textul deasupra, pentru că ordinea de pictare este ordinea de suprapunere în PDF: umpleți dreptunghiul după text și îngropați rândul. Iar colorarea alternativă nu este o decorațiune de dragul ei. Într-un raport dens este cea mai ieftină cale de a împiedica ochiul să alunece pe rândul greșit, motiv pentru care bucla de mai jos inversează un boolean la fiecare rând și îl transmite direct în Shaded

Pozițiile de coloană de mai sus sunt fixe, ceea ce este cinstit pentru un raport a cărui schemă o controlați. Când datele sunt variabile, măsurați în loc să ghiciți. HotPDF expune măsurarea lățimii textului pe obiectul pagină, deci versiunea de producție a lui PrintRow poate lua cea mai lungă valoare așteptată din fiecare coloană, o poate măsura o dată la dimensiunea de font aleasă și poate deriva marginile din stânga din acele lățimi plus un spațiu de separare. Forma rutinei nu se schimbă; se schimbă doar sursa constantelor

Antetul, liniile și un singur loc care le deține

Un tabel care iese de pe o pagină și reîncepe pe următoarea fără etichete de coloană este imposibil de citit. Corectura este să tratați antetul ca pe ceva ce redesenați, nu ca pe ceva ce desenați o dată. Puneți titlurile de coloană și liniile orizontale care le încadrează într-o singură rutină și apelați acea rutină atât la început, cât și de fiecare dată când deschideți o pagină nouă. Fiindcă antetul și corpul împart aceleași constante de coloană, ele se aliniază prin construcție

HotPDF redesenează titlurile și liniile din DrawHeader pe prima pagină și din nou după fiecare AddPage, așa că ambele pagini PDF încep cu un antet identic
Rutina de antet rulează din nou pe fiecare pagină nouă, deci titlurile și liniile aterizează în același loc prin construcție
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Stânga: eticheta sursei și numărul paginii. Dreapta: momentul generării.
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Două linii orizontale care încadrează titlurile de coloană.
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // Titlurile de coloană, cu un font mai gros, ca să se citească drept titluri.
  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;  // trece dincolo de antetul încadrat, înaintea primului rând de corp
end;

Observați că DrawHeader primește Y prin referință și îl duce mai departe. Apelantul nu trebuie să își amintească niciodată cât de înalt este antetul; rutina care îl desenează este rutina care știe. Această regulă de proprietate unică este cea care împiedică aranjamentul să se deplaseze atunci când adăugați ulterior un logo sau un rezumat de filtre în banda de antet. Bucla de corp rămâne neștiutoare. Ea doar continuă să deseneze rânduri de acolo de unde arată Y în acel moment

Chiar liniile sunt diferența dintre o listă și un tabel. Separatoarele verticale de coloană sunt aceeași idee aplicată axei x: un MoveTo / LineTo / Stroke la fiecare margine de coloană, trasat de la linia de sus până la baza ultimului rând de pe pagină. Exemplul se ține de liniile orizontale ca să rămână lizibil, dar pasul de producție este mecanic odată ce constantele de coloană există

Bucla de cursor deține schimbarea de pagină

Desenarea este jumătatea ușoară. Jumătatea care desparte o jucărie de un raport este paginarea: să știți, înainte de a desena un rând, dacă mai încape, și să deschideți o pagină nouă cu un antet nou atunci când nu mai încape. Acea decizie aparține exact unui singur loc, buclei care parcurge datele, și nicăieri altundeva

Diagramă de flux a buclei de cursor din Delphi, în care un Y sub 60 declanșează AddPage, o recitire a lui CurrentPage, o reemitere a lui SetFont și un antet repetat înaintea următorului rând de tabel
Bucla de cursor este singurul loc care deschide o pagină nouă și o inițializează din nou atunci când Y coboară sub marginea de jos
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;

    // Titlul raportului, o singură dată, sus pe prima pagină.
    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
      // Nu mai este loc? Deschide o pagină nouă și repetă antetul acolo.
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage mută CurrentPage mai departe
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont trebuie reemis pe fiecare pagină nouă
      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;

Două fapte legate de coordonate conduc toată bucla. PDF măsoară y în sus, din colțul din stânga jos, deci rândurile mărșăluiesc în jos pe pagină scăzând RowStep din Y de fiecare dată, iar testul de pagină plină se declanșează atunci când Y scade sub marginea de jos, nu când urcă peste vreo margine de sus. Greșiți direcția și primul rând se tipărește dincolo de marginea de jos, în timp ce bucla crede că are o pagină întreagă la dispoziție

Celălalt fapt prinde aproape pe toată lumea măcar o dată. AddPage creează o pagină nouă și îl repoziționează pe CurrentPage spre ea, dar nu duce nimic mai departe: nici fontul, nici culoarea de umplere, nici poziția. De aceea Page este recitit din CurrentPage după fiecare AddPage și de aceea SetFont este reemis înaintea rândurilor de corp. Săriți peste recitire și continuați să desenați pe pagina pe care tocmai ați părăsit-o; săriți peste font și pagina nouă se randează cu orice valoare implicită la care recurge vizualizatorul

Cazurile care strică un exportator de tabele

Majoritatea defectelor de tabel nu apar pe drumul fericit al câtorva zeci de rânduri îngrijite. Ele trăiesc la margini, iar marginile sunt ieftin de testat odată ce știți unde sunt

  • Seturi de date goale. O buclă peste zero rânduri produce o pagină cu un antet și nimic sub el, ceea ce măcar pare intenționat. O pagină albă fără antet pare un eșec. Hotărâți ce vreți înainte de livrare
  • Rândul care aterizează exact pe limită. Generați un raport al cărui ultim rând stă cu un pas deasupra marginii, apoi unul al cărui rând următor este cu un pas sub ea. Paginarea greșită cu o unitate se ascunde până când datele au exact lungimea nepotrivită
  • Valori prea lungi. Un nume de companie mai lat decât coloana lui va intra peste următoarea. Măsurați câmpul și hotărâți o politică: împărțire pe un al doilea rând, decupare sau trunchiere cu puncte de suspensie. Tăcerea nu este o politică
  • Câmpuri nule. Citirea unui null direct în TextOut poate ieși ca textul literal Null sau ca un gol, în funcție de felul în care îl convertiți. Alegeți randarea intenționat, în loc să lăsați conversia de variantă să aleagă în locul dumneavoastră

Treceți rezultatul prin mai mult de un vizualizator înainte să îl declarați gata. Substituirea fonturilor și decuparea se comportă diferit de la un randor la altul, iar un tabel care pare drept într-un cititor PDF poate arăta o coloană nealiniată sau un oraș decupat în altul. Confirmați că antetul repetat, colorarea rândurilor și marginile supraviețuiesc mutării și că numerele de pagină rămân continue după ce datele trec de o limită

Desenarea grilei chiar de dumneavoastră, în loc să vă sprijiniți pe un proiectant vizual de rapoarte, înseamnă mai mult cod, iar compromisul merită spus pe față: dețineți fiecare coordonată, ceea ce este exact ce vreți pentru sarcini de lot pe server, facturi și exporturi de audit care trebuie să se randeze identic pe orice mașină, și exact efortul pe care ați prefera să îl evitați pentru o listare internă de o singură dată. Pentru primul caz, controlul se plătește singur prima dată când un raport trebuie să arate în producție la fel ca pe biroul dumneavoastră

Liniile și benzile colorate de mai sus se sprijină pe aceleași primitive vectoriale și de culoare tratate în parcurgerea desenului pe pânză, dacă vreți să vedeți întâi apelurile Rectangle, MoveTo și LineTo luate separat. Primitivele de desenare folosite aici fac parte din HotPDF Delphi Component pentru Delphi și C++Builder