Teknisk artikel

Gengiv en datatabel til PDF i Delphi med HotPDF

Et datasæt er rækker og kolonner; en PDF-side er et tomt koordinatnet uden nogen forestilling om nogen af delene. At bygge bro over den kløft er hele opgaven her. Der er ikke noget DrawTable-kald i HotPDF, der tager et datasæt og giver dig et formateret gitter. Det, du får i stedet, er de primitiver, et gitter er lavet af: TextOut til at placere en streng på et punkt, SetFont til at vælge dens ansigt, Rectangle og Fill til at skygge et bånd, og MoveTo / LineTo / Stroke for at tegne linjer. En velfungerende tabeleksportør er disciplinen med at omsætte række-og-kolonne-tænkning til eksplicitte x- og y-koordinater og derefter holde disse koordinater ærlige, når dataene løber forbi bunden af siden

Eksemplet, der følger, rapporterer kundeposter, men intet i tegnekoden ved eller bekymrer sig om, hvor rækkerne kommer fra. Originalen brugte en ældre TTable; en FireDAC-forespørgsel, et in-memory-datasæt eller et simpelt array af poster fodrer de samme rutiner uændret. Det, der betyder noget, er, at du kan gennemgå dataene én række ad gangen og læse fire strengfelter ud af hver. Hold gengivelsen adskilt fra datakilden, og du kan ændre hver side uden at forstyrre den anden

Kolonnegeometri kommer først

Før et enkelt tegn tegnes, skal du beslutte, hvor hver kolonne lever. En tabel har fire kolonner her, så den har brug for fire venstre kanter og en kendt højre margen. At hårdkode et magisk tal ved hvert TextOut-kald, som hurtige prøver har en tendens til at gøre, er præcis det, der gør det smertefuldt at gøre en tabel bredere senere. Navngiv kanterne én gang, i punkter fra den nederste venstre oprindelse, og hvert tegnekald refererer til dem ved navn:

HotPDF tabelkolonnegeometri i Delphi: navngivne x-kanter ved 70, 110, 300 og 480 punkter mellem rammestreger ved 50 og 570 punkter
Fire navngivne venstre kanter og en kendt højre margen fastlægger hele tabelgeometrien før det første TextOut-kald
const
  ColNo   = 70;    // venstre kant af "No."-kolonnen
  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;    // lodret afstand mellem baselines

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // Et tonet bånd bag rækken. Rectangle tager 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;

To detaljer tjener deres indtægt her. Det skyggede bånd tegnes først og derefter teksten ovenpå, fordi malerækkefølge er z-rækkefølge i PDF: udfyld rektanglet efter teksten, og du begraver rækken. Og den vekslende skygge er ikke dekoration for dens egen skyld. På en tæt rapport er det den billigste måde at forhindre øjet i at glide over på den forkerte linje, og derfor vender løkken senere en boolesk værdi på hver række og videresender den direkte ind i Shaded

Kolonnepositionerne ovenfor er faste, hvilket er ærligt for en rapport, hvis skema du kontrollerer. Når dataene er variable, skal du måle i stedet for at gætte. HotPDF afslører tekstbreddemåling på sideobjektet, så produktionsversionen af PrintRow kan tage den længste forventede værdi i hver kolonne, måle den én gang ved den valgte skriftstørrelse og udlede de venstre kanter fra disse bredder plus en rendesten. Rutinens form ændres ikke; kun kilden til konstanterne gør det

Overskriften, reglerne og ét sted, der ejer dem

En tabel, der ruller ud af en side og genoptages på den næste uden kolonneetiketter, er ulæselig. Rettelsen er at behandle overskriften som noget, du tegner igen, ikke noget, du tegner én gang. Læg kolonneoverskrifterne og de vandrette linjer, der indrammer dem, i en enkelt rutine, og kalde den rutine både i starten og igen, hver gang du åbner en ny side. Fordi overskriften og brødteksten deler de samme kolonnekonstanter, flugter de i kraft af deres konstruktion

HotPDF gentegner DrawHeader-titler og -streger på side ét og igen efter hver AddPage, så begge PDF-sider åbner med en identisk header
Header-rutinen kører igen på hver ny side, så titler og linjer lander samme sted af konstruktion
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Venstre: kildelabel og sidetal. Højre: genereringstidspunkt
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // To vandrette streger der rammer kolonnetitlerne ind
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // Kolonnetitlerne, i en federe font så de læses som overskrifter
  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;  // ryk forbi den indrammede header før første brødtekstrække
end;

Bemærk, at DrawHeader tager Y som reference og flytter den fremad. Kalderen behøver aldrig at huske, hvor høj overskriften er; den rutine, der tegner den, er den rutine, der ved det. Den enkelte ejerskabsregel er det, der forhindrer layoutet i at glide, når du senere tilføjer et logo eller en filteroversigt til overskriftsbåndet. Brødtekst-løkken forbliver uvidende. Den fortsætter bare med at tegne rækker, uanset hvor Y i øjeblikket peger

Selve reglerne er forskellen mellem en liste og en tabel. Lodrette kolonneseparatorer er den samme idé anvendt på x-aksen: en MoveTo / LineTo / Stroke ved hver kolonnekant, der køres fra den øverste linje til bunden af den sidste række på siden. Eksemplet holder sig til vandrette linjer for at forblive læselig, men produktionstrinnet er mekanisk, når først kolonnekonstanterne eksisterer

Cursor-løkken ejer sideskiftet

Tegning er den nemme halvdel. Den halvdel, der adskiller et legetøj fra en rapport, er sideinddeling: at vide, før du tegner en række, om den stadig passer, og at starte en ny side med en ny overskrift, når den ikke gør. Den beslutning hører til præcis ét sted, i løkken, der gennemgår dataene, og ingen andre steder

Delphi cursor-loop-flowchart, hvor en Y under 60 udløser AddPage, en CurrentPage-genlæsning, en SetFont-genudstedelse og en gentaget header før næste tabelrække
Markørløkken er det eneste sted, der åbner en frisk side og geninitialiserer den, når Y falder under den nederste margen
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;

    // Rapporttitel, én gang, øverst på første side
    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
      // Løbet tør for plads? Åbn en ny side og gentag headeren der
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage rykker CurrentPage frem
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont skal sættes igen på hver ny side
      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;

To koordinatfakta driver hele løkken. PDF måler y opad fra det nederste venstre hjørne, så rækkerne marcherer nedad på siden ved at trække RowStep fra Y hver gang, og testen for side-fuld udløses, når Y falder under den nederste margen i stedet for at være over en eller anden top. Få retningen baglæns, og din første række udskrives uden for den nederste kant, mens løkken tror, den har en hel side med plads

Den anden kendsgerning fanger næsten alle én gang. AddPage opretter en ny side og peger CurrentPage hen til den, men det overfører ingenting: ikke skrifttypen, ikke fyldfarven, ikke positionen. Det er grunden til, at Page genlæses fra CurrentPage efter hver AddPage, og grunden til, at SetFont genudstedes før brødtekst-rækkerne. Spring genlæsningen over, og du fortsætter med at tegne på den side, du lige har efterladt; spring skrifttypen over, og den nye side gengives med den standard, fremviseren falder tilbage til

De tilfælde, der ødelægger en tabeleksportør

De fleste tabelfejl viser sig ikke på den lykkelige vej af et par dusin pæne rækker. De lever i kanterne, og kanterne er billige at teste, når du ved, hvor de er

  • Tomme datasæt. En løkke over nul rækker producerer en side med en overskrift og intet under den, hvilket i det mindste ser ud til at være bevidst. En blank side uden nogen overskrift ligner en fejl. Beslut, hvilken du vil have, før du sender ud
  • Rækken, der lander nøjagtigt på grænsen. Generer en rapport, hvis sidste række sidder et skridt over margenen, derefter en, hvis næste række er et skridt under den. Sideinddeling, der er en ved siden af, gemmer sig, indtil dataene har nøjagtig den forkerte længde
  • For lange værdier. Et firmanavn, der er bredere end dets kolonne, løber ind i det næste. Mål feltet og beslut dig for en politik: ombryd til en anden linje, klip af eller afkort med en ellipse. Tavshed er ikke en politik
  • Null-felter. At læse et nul direkte ind i TextOut kan dukke op som den bogstavelige tekst Null eller som en tom linje, afhængigt af hvordan du konverterer den. Vælg bevidst gengivelsen i stedet for at lade variantkonverteringen vælge for dig

Kør resultatet gennem mere end én fremviser, før du kalder det færdigt. Skrifttypesubstitution og udklip opfører sig forskelligt på tværs af renderere, og en tabel, der ser kvadratisk ud i én PDF-læser, kan vise en forskudt kolonne eller en klippet by i en anden. Bekræft, at den gentagne overskrift, rækkeskyggen og margenerne overlever flytningen, og at sidetal forbliver kontinuerlige, efter at data krydser en grænse

At tegne selve gitteret i stedet for at læne sig op ad en visuel rapportdesigner er mere kode, og afvejningen er værd at navngive klart: du ejer hver koordinat, hvilket er præcis, hvad du vil have til batchjob på serversiden, fakturaer og revisions-eksporter, der skal gengives identisk på hver maskine, og præcis den overhead, du hellere vil undgå ved en intern enkeltstående oversigt. For førstnævnte betaler kontrollen for sig selv, første gang en rapport skal se ud på samme måde i produktion, som den gjorde på dit skrivebord

Reglerne og skyggefulde bånd ovenfor bygger på de samme vektor- og farveprimitiver, der er dækket i gennemgangen af lærredstegning, hvis du ønsker Rectangle, MoveTo og LineTo-kald behandlet for sig selv først. Tegneprimitiverne, der bruges her, er en del af HotPDF Delphi Component for Delphi og C++Builder