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

Techninis straipsnis

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ą:

HotPDF lentelės stulpelių geometrija Delphi: įvardyti x kraštai ties 70, 110, 300 ir 480 taškais tarp rėmo linijų ties 50 ir 570 taškais
Keturi pavaduotieji kairieji kraštai ir žinomoji dešinioji paraštė užfiksuoja visą lentelės geometriją prieš pirmąjį TextOut kvietimą
const
  ColNo   = 70;    // „No.“ stulpelio kairysis kraštas
  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;    // vertikalus atstumas tarp bazinių linijų

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // Už eilutės esanti šešėliuota juosta. Rectangle priima 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

HotPDF perpiešia DrawHeader pavadinimus ir linijas pirmajame puslapyje ir vėl po kiekvieno AddPage, kad abu PDF puslapiai atsivertų su identiška antrašte
Antraštės procedūra vyksta iš naujo kiekviename naujame puslapyje, todėl pavadinimai ir linijos nusileidžia toje pačioje vietoje pagal konstrukciją
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Kairėje: šaltinio etiketė ir puslapio numeris. Dešinėje: sukūrimo laikas
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Dvi horizontalios linijos, apgaubiančios stulpelių pavadinimus
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // Stulpelių pavadinimai sunkesniu šriftu, kad atrodytų kaip antraštės
  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;  // prieš pirmąją pagrindinio teksto eilutę pereikite už įrėmintos antraštės
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

Delphi žymeklio ciklo blokinė schema, kur Y žemiau 60 sužadina AddPage, CurrentPage pakartotinį skaitymą, SetFont pakartotinį išdavimą ir pasikartojančią antraštę prieš kitą lentelės eilutę
Kursoriaus ciklas yra vienintelė vieta, kuri atveria šviežią puslapį ir inicijuoja jį iš naujo, kai Y nukrenta žemiau apatinės paraštės
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;

    // Ataskaitos pavadinimas vieną kartą pirmojo puslapio viršuje
    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
      // Nebėra vietos? Atidarykite naują puslapį ir pakartokite jame antraštę
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage pastumia CurrentPage pirmyn
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont reikia iškviesti iš naujo kiekviename naujame puslapyje
      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 Delphi Component dalis