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:

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;

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

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;

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

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;

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 Component for Delphi og C++Builder