Ett dataset består av rader och kolumner; en PDF-sida är ett tomt koordinatraster som inte känner till någotdera. Att överbrygga denna klyfta är hela uppgiften här. Det finns inget DrawTable-anrop i HotPDF som tar ett dataset och ger dig ett formaterat rutnät. Vad du istället får är primitiverna som ett rutnät består av: TextOut för att placera en sträng vid en punkt, SetFont för att välja dess typsnitt, Rectangle och Fill för att skugga ett band, samt MoveTo / LineTo / Stroke för att rita linjer. En fungerande tabellexportör är disciplinen att förvandla rad-och-kolumn-tänkande till uttryckliga x- och y-koordinater, och sedan hålla dessa koordinater ärliga när data sträcker sig förbi sidans nederkant
Följande exempel rapporterar kundposter, men ingenting i ritkoden vet eller bryr sig om var raderna kommer ifrån. Originalet använde en föråldrad TTable; en FireDAC-fråga, ett dataset i minnet eller en vanlig array med poster matar samma rutiner oförändrat. Det viktiga är att du kan gå igenom datan en rad i taget och läsa ut fyra strängfält från varje. Håll renderingen separerad från datakällan så kan du ändra endera sidan utan att störa den andra
Kolumngeometri kommer först
Innan ett enda tecken ritas, bestäm var varje kolumn är belägen. En tabell har här fyra kolumner, så den behöver fyra vänsterkanter och en känd högermarginal. Att hårdkoda en magisk siffra vid varje TextOut-anrop, på det sätt som snabba exempel ofta gör, är precis det som gör en tabell smärtsam att bredda senare. Namnge kanterna en gång, i punkter från origo nere till vänster, så att varje ritanrop hänvisar till dem med namn:
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;
Två detaljer förtjänar sitt uppehälle här. Det skuggade bandet ritas först, sedan texten ovanpå, eftersom ritordningen är z-ordningen i PDF: fyller du rektangeln efter texten så begraver du raden. Och den alternerande skuggningen är inte dekoration för sin egen skull. På en tät rapport är det det billigaste sättet att hindra ögat från att glida till fel rad, vilket är anledningen till att loopen senare vänder ett booleskt värde för varje rad och skickar det direkt till Shaded
Kolumnpositionerna ovan är fasta, vilket är ärligt för en rapport vars schema du kontrollerar. När datan är variabel, mät istället för att gissa. HotPDF exponerar mätning av textbredd på sidobjektet, så produktionsversionen av PrintRow kan ta det längsta förväntade värdet i varje kolumn, mäta det en gång vid den valda teckenstorleken och härleda vänsterkanterna från dessa bredder plus en rännsten. Rutinens form ändras inte; bara källan till konstanterna gör det
Rubriken, linjerna och en plats som äger dem
En tabell som rullar av en sida och återupptas på nästa utan kolumnetiketter är oläslig. Lösningen är att behandla rubriken som något du ritar om, inte något du ritar en gång. Lägg kolumnrubrikerna och de horisontella linjerna som ramar in dem i en enda rutin, och anropa den rutinen både i början och igen varje gång du öppnar en ny sida. Eftersom rubriken och kroppen delar samma kolumnkonstanter hamnar de i linje per 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;
Lägg märke till att DrawHeader tar Y som referens och flyttar det framåt. Anroparen behöver aldrig komma ihåg hur hög rubriken är; rutinen som ritar den är rutinen som vet. Denna enda äganderegel är det som hindrar layouten från att glida när du senare lägger till en logotyp eller en filtersammanfattning i rubrikbandet. Kroppsloopen förblir omedveten. Den fortsätter bara att rita rader från var Y för närvarande pekar
Själva linjerna utgör skillnaden mellan en lista och en tabell. Vertikala kolumnavskiljare är samma idé tillämpad på x-axeln: en MoveTo / LineTo / Stroke vid varje kolumnkant, dragen från den översta linjen till botten av den sista raden på sidan. Exemplet håller sig till horisontella linjer för att förbli läsbart, men produktionssteget är mekaniskt när kolumnkonstanterna väl existerar
Markörloopen äger sidbrytningen
Att rita är den enkla hälften. Den hälft som skiljer en leksak från en rapport är sidindelningen: att veta, innan du ritar en rad, om den fortfarande får plats, och att påbörja en ny sida med en ny rubrik när den inte gör det. Det beslutet hör hemma på exakt en plats, loopen som går igenom datan, och ingen annanstans
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;
Två koordinatfakta driver hela loopen. PDF mäter y uppåt från det nedre vänstra hörnet, så raderna marscherar nedåt på sidan genom att subtrahera RowStep från Y varje gång, och testet för full sida utlöses när Y sjunker under den nedre marginalen snarare än över någon topp. Får du riktningen baklänges skrivs din första rad ut utanför den nedre kanten medan loopen tror att den har en hel sida med utrymme
Det andra faktumet fångar nästan alla en gång. AddPage skapar en ny sida och pekar om CurrentPage till den, men den överför ingenting: inte typsnittet, inte fyllnadsfärgen, inte positionen. Det är därför Page läses in på nytt från CurrentPage efter varje AddPage, och varför SetFont utfärdas igen före kroppens rader. Hoppar du över omläsningen fortsätter du att rita på sidan du precis lämnade bakom dig; hoppar du över typsnittet renderas den nya sidan i den standard som visningsprogrammet faller tillbaka på
Fallen som knäcker en tabellexportör
De flesta tabellbuggar dyker inte upp på den lyckliga vägen av några dussin prydliga rader. De lever vid kanterna, och kanterna är billiga att testa när du väl vet var de är
- Tomma dataset. En loop över noll rader producerar en sida med en rubrik och ingenting under den, vilket åtminstone ser avsiktligt ut. En blank sida utan rubrik ser ut som ett misslyckande. Bestäm vilket du vill ha innan du levererar
- Raden som landar exakt på gränsen. Generera en rapport vars sista rad sitter ett steg ovanför marginalen, därefter en vars nästa rad är ett steg under den. Ett-fel-sidindelning gömmer sig tills datan är exakt av fel längd
- För långa värden. Ett företagsnamn bredare än sin kolumn kommer att springa in i nästa. Mät fältet och besluta om en policy: bryt till en andra rad, klipp av, eller trunkera med uteslutningstecken. Tystnad är inte en policy
- Null-fält. Att läsa ett null direkt in i
TextOutkan dyka upp som bokstavstextenNulleller som tomt, beroende på hur du konverterar det. Välj renderingen medvetet istället för att låta variantkonverteringen välja åt dig
Kör resultatet genom mer än ett visningsprogram innan du kallar det klart. Typsnittsersättning och beskärning beter sig olika över renderare, och en tabell som ser fyrkantig ut i en PDF-läsare kan visa en feljusterad kolumn eller en avklippt stad i en annan. Bekräfta att den upprepade rubriken, radskuggningen och marginalerna överlever flytten, och att sidnumren förblir kontinuerliga efter att datan korsat en gräns
Att rita rutnätet själv istället för att luta sig mot en visuell rapportdesigner innebär mer kod, och kompromissen är värd att nämna tydligt: du äger varje koordinat, vilket är exakt vad du vill ha för serverbaserade batchjobb, fakturor och revisionsexporter som måste renderas identiskt på varje maskin, och exakt den omkostnad du hellre vill undvika för en engångs intern lista. För det förstnämnda betalar kontrollen för sig själv första gången en rapport måste se likadan ut i produktion som den gjorde på ditt skrivbord
Linjerna och de skuggade banden ovan stöder sig på samma vektor- och färgprimitiver som täcks i genomgången av canvas-ritning, ifall du vill att Rectangle, MoveTo och LineTo-anropen behandlas separat först. Ritprimitiverna som används här är en del av HotPDF-komponenten för Delphi och C++Builder