<Duomenų lentelės braižymas PDF formatu „Delphi“ programoje su „HotPDF“

Technical Article

Duomenų lentelės braižymas PDF formatu „Delphi“ programoje su „HotPDF“

Duomenų rinkinį (dataset) sudaro eilutės ir stulpeliai, o PDF puslapis yra tiesiog tuščias koordinačių tinklelis, neturintis jokio supratimo apie lenteles. Mūsų užduotis yra užpildyti šią spragą. „HotPDF“ neturi paruošto DrawTable metodo, kuris priimtų duomenų rinkinį ir patektų suformatuotą lentelę. Vietoj to gausite pagrindinius elementus (primitivus), iš kurių kuriamas tinklelis: TextOut teksto išdėstymui taške, SetFont šrifto parinkimui, Rectangle ir Fill fono šešėliavimui bei MoveTo / LineTo / Stroke linijų nubrėžimui. Veikiantis lentelės eksportavimo įrankis reikalauja mąstymą eilutėmis ir stulpeliais paversti tiksliomis X ir Y koordinatėmis bei teisingai jas suvaldyti, kai duomenys nebetelpa viename puslapyje.

Toliau pateiktame pavyzdyje atvaizduojami klientų įrašai, tačiau braižymo kodui visiškai nesvarbu, iš kur gaunamos tos eilutės. Pavyzdyje buvo naudojamas pasenęs TTable komponentas, tačiau „FireDAC“ užklausa, atmintyje esantis duomenų rinkinys ar tiesiog įrašų masyvas sėkmingai maitina tą patį kodą. Svarbiausia yra tai, kad galite pereiti duomenis po vieną eilutę ir nuskaityti keturis tekstinius laukus. Atskirkite vizualinį atvaizdavimą nuo duomenų šaltinio ir galėsite keisti bet kurią pusę nekeisdami kitos.

Stulpelių geometrija

Prieš nupiešiant pirmąjį simbolį, nustatykite kiekvieno stulpelio vietą. Ši lentelė turi keturis stulpelius, zodžiu, jai reikalingos keturios kairiosios kraštinės ir žinoma dešinioji paraštė. Skaičių įrašymas tiesiogiai prie kiekvieno TextOut iškvietimo (kaip dažnai daroma greituose pavyzdžiuose) yra būtent tai, kas vėliau apsunkina stulpelių platinimą. Apibrėžkite stulpelių kraštus vieną kartą taškais nuo apatinio kairiojo kampo, ir kiekvienas metodas galės kreiptis į juos pagal pavadinimą:

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;

Čia labai svarbios dvi detalės. Pirmiausia nupiešiamas pilkas fono ruožas, o tik tada ant jo rašomas tekstas, nes PDF dokumente elementų braižymo tvarka lemia jų išsidėstymą Z ašyje (z-order): užpildę stačiakampį po to, kai parašėte tekstą, tiesiog uždengsite eilutę. Alternatyvus eilučių spalvinimas nėra tik dėl grožio. Tankioje ataskaitoje tai yra paprasčiausias būdas neleisti akims nuklysti į kitą eilutę, todėl cikle kiekvienai eilutei keičiama loginė reikšmė ir perduodama tiesiai į Shaded parametrą.

Aukščiau pateiktos stulpelių pozicijos yra fiksuotos, o tai tinka ataskaitoms, kurių struktūrą kontroliuojate patys. Kai duomenys yra kintamo ilgio, geriau matuoti, o ne spėlioti. „HotPDF“ leidžia matuoti teksto plotį puslapio objekte, todėl reali programos versija gali paimti ilgiausią laukiamą reikšmę kiekviename stulpelyje, vieną kartą ją išmatuoti pasirinktu šrifto dydžiu ir pagal tai apskaičiuoti stulpelių kraštus, pridedant tarpus. Pačio metodo struktūra nesikeičia, keičiasi tik konstantų šaltinis.

Antraštė, linijos ir bendras valdymas

Lentelė, kuri tęsiasi kitame puslapyje be stulpelių pavadinimų, yra neįskaitoma. Sprendimas yra traktuoti antraštę kaip perbraižomą elementą, o ne nupiešti ją tik vieną kartą. Įkelkite stulpelių pavadinimus ir juos rėminančias horizontalias linijas į vieną metodą ir iškvieskite jį pradžioje bei kiekvieną kartą sukurdami naują puslapį. Kadangi antraštė ir lentelės kūnas naudoja tas pačias stulpelių konstantas, jie visada idealiai sutaps.

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;

Atkreipkite dėmesį, kad DrawHeader priima Y koordinatę pagal nuorodą (var) ir pastumia ją į priekį. Kviečiančiajam metodui nereikia žinoti, kokio aukščio yra antraštė, tai žino pats antraštės braižymo metodas. Ši taisyklė neleidžia puslapio struktūrai išsiderinti, jei vėliau į antraštę nuspręsite įdėti logotipą ar filtrų santrauką. Lentelės eilučių ciklas išlieka nepriklausomas ir tiesiog tęsia braižymą nuo taško, kurį rodo Y.

Linijos yra tai, kas paverčia paprastą sąrašą lentelė. Vertikalūs stulpelių skirtukai braižomi pagal tą pačią logiką X ašyje: naudojami MoveTo / LineTo / Stroke metodai ties kiekvieno stulpelio kraštu nuo viršutinės linijos iki paskutinės eilutės apačios. Pavyzdyje naudojamos tik horizontalios linijos, kad kodas išliktų paprastas, tačiau realiame procese pridėti vertikalias linijas yra nesudėtinga, kai jau turite stulpelių konstantas.

Puslapių skaidymo valdymas cikle

Braižymas yra lengvesnioji dalis. Puslapiavimas yra tai, kas paverčia paprastą programą realiu ataskaitų įrankiu: prieš piešiant eilutę reikia žinoti, ar ji dar telpa puslapyje, o jei ne, sukurti naują puslapį su nauja antrašte. Šis sprendimas turi būti priimamas tik vienoje vietoje, duomenų apdorojimo cikle.

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;

Visą ciklą valdo du faktai apie koordinates. PDF koordinatę Y matuoja į viršų nuo apatinio kairiojo kampo, todėl eilutės braižomos žemyn, kiekvieną kartą iš Y atimant RowStep reikšmę. Puslapio užsipildymo patikra suveikia tada, kai Y nukrenta žemiau apatinės paraštės ribos. Supainiojus kryptį, pirmoji eilutė bus nupiešta už puslapio ribų, nors ciklas manys, kad vietos dar yra į valias.

Kitas faktas dažnai klaidina pradedančiuosius. Metodas AddPage sukuria naują puslapį ir nukreipia į jį savybę CurrentPage, tačiau neperkelia jokios būsenos: nei šrifto, nei užpildymo spalvos, nei pozicijos. Štai kodėl po kiekvieno AddPage iškvietimo būtina iš naujo nuskaityti Page reikšmę iš CurrentPage bei iš naujo iškviesti SetFont prieš pradedant rašyti naujas eilutes. Jei to nepadarysite, tęsite piešimą senajame puslapyje arba naujas puslapis bus atvaizduojamas su numatytuoju sistemos šriftu.

Atvejai, galintys sugadinti lentelės eksportavimą

Dauguma su lentelėmis susijusių klaidų nepasitaiko apdorojant vos kelias tvarkingas eilutes. Jos išryškėja nestandartinėse situacijose, kurias nesudėtinga patikrinti žinant, kur jų ieškoti.

  • Tušti duomenų rinkiniai. Ciklas per nulį eilučių sukurs puslapį su antrašte ir tuščia sritimi po ja, o tai atrodo logiškai. Tuščias puslapis be jokios antraštės atrodys kaip programos klaida. Prieš pateikdami programą nuspręskite, kuris variantas jums tinkamesnis.
  • Eilutė, esanti tiksliai ant ribos. Sugeneruokite ataskaitą, kurios paskutinė eilutė yra tiesiai virš paraštės ribos, ir kitą, kurios kita eilutė jau yra po ja. Puslapiavimo klaidos per vieną elementą dažniausiai išryškėja tik esant tam tikram specifiniam duomenų kiekiui.
  • Per ilgos reikšmės. Įmonės pavadinimas, kuris yra platesnis nei jam skirtas stulpelis, užlips ant kito stulpelio. Išmatuokite lauką ir pasirinkite sprendimą: kelkite tekstą į kitą eilutę, apkirpkite jį arba sutrumpinkite su daugtaškiu. Problemos ignoravimas nėra sprendimas.
  • Null (tušti) laukai. Null reikšmės siuntimas tiesiai į TextOut gali būti atvaizduotas kaip tekstas „Null“ arba tiesiog tuščia vieta, priklausomai nuo konvertavimo būdo. Pasirinkite atvaizdavimą sąmoningai, užuot leidę sistemai nuspręsti už jus.

Prieš baigdami darbą, peržiūrėkite rezultatą keliose skirtingose PDF peržiūros programose. Šriftų pakeitimas ir teksto apkirpimas skirtingose programose gali skirtis. Lentelė, kuri vienoje programoje atrodo tvarkingai, kitoje gali rodyti pasislinkusius stulpelius ar nukirptus miestų pavadinimus. Įsitikinkite, kad kartojama antraštė, eilučių šešėliavimas ir paraštės išlieka taisyklingi, o puslapių numeracija tęsiasi be pertrūkių.

Rankinis lentelės tinklelio braižymas reikalauja daugiau kodo, todėl verta įvertinti šį sprendimą: jūs pilnai kontroliuojate kiekvieną koordinatę, o tai ypač svarbu serveryje vykdomiems masiniams darbams, sąskaitoms faktūroms ir audito dokumentams, kurie turi būti atvaizduojami vienodai bet kuriame įrenginyje. Tačiau tai gali būti per didelis darbas paprastiems vienkartiniams vidiniams sąrašams. Pirmuoju atveju tokia kontrolė atsiperka jau pirmą kartą, kai ataskaita gamybinėje aplinkoje turi atrodyti lygiai taip pat, kaip atrodė jūsų kompiuteryje.

Aukščiau aprašytos linijos ir šešėliavimas remiasi tomis pačiomis vektorinio braižymo ir spalvų funkcijomis, kurios aprašytos drobės braižymo vadove. Čia naudojami braižymo metodai yra „Delphi“ ir „C++Builder“ skirto HotPDF Component dalis.