Bir veri kümesi (dataset) satırlar ve sütunlardır; bir PDF sayfası ise her ikisi hakkında hiçbir fikri olmayan (no notion of either) boş bir koordinat ızgarasıdır. Bu boşluğu kapatmak buradaki tüm iştir. HotPDF'de bir veri kümesini alıp size biçimlendirilmiş (formatted) bir ızgara veren bir DrawTable çağrısı yoktur. Bunun yerine elde ettiğiniz şey, bir ızgaranın (grid) oluşturulduğu ilkellerdir (primitives): Bir noktaya bir dize (string) yerleştirmek için TextOut, yazı tipini (face) seçmek için SetFont, bir şeridi (band) gölgelendirmek için Rectangle ve Fill, ve çizgiler (rules) çizmek için MoveTo / LineTo / Stroke. Çalışan (working) bir tablo dışa aktarıcı (exporter), satır-ve-sütun düşüncesini (thinking) açık x ve y koordinatlarına dönüştürme ve daha sonra veriler sayfanın altına (bottom) ulaştığında bu koordinatları dürüst (honest) tutma disiplinidir
Aşağıdaki örnek müşteri kayıtlarını raporlar, ancak çizim kodundaki hiçbir şey (nothing) satırların nereden geldiğini bilmez veya umursamaz (cares). Orijinali eski bir (legacy) TTable kullanıyordu; bir FireDAC sorgusu (query), bellek içi (in-memory) bir veri kümesi veya düz (plain) bir kayıt (records) dizisi aynı rutinleri (routines) değiştirmeden (unchanged) besler (feeds). Önemli olan, verileri her seferinde bir satır dolaşabilmeniz (walk) ve her birinden dört dize alanını (string fields) okuyabilmenizdir (read). İşlemeyi (rendering) veri kaynağından ayrı (separate) tutun, böylece (and) diğer tarafı rahatsız etmeden herhangi bir tarafı değiştirebilirsiniz (change either side)
Önce sütun geometrisi gelir (comes first)
Tek bir (a single) karakter çizilmeden önce her bir sütunun (column) nerede yaşayacağına karar verin. Buradaki bir tablonun dört sütunu vardır, bu nedenle dört sol kenara (left edges) ve bilinen bir sağ kenar boşluğuna (right margin) ihtiyacı vardır. Her TextOut çağrısında sihirli bir sayıyı (magic number) sabit kodlamak (hard-coding), -ki hızlı örneklerin (quick samples) yapma eğiliminde olduğu gibi- bir tablonun (table) daha sonra genişletilmesini acı verici hale getiren şeyin (exactly what makes a table painful) ta kendisidir. Kenarları sol alt (bottom-left) başlangıç noktasından (origin) itibaren punto (points) cinsinden bir kez adlandırın (name) ve her çizim çağrısı bunlara ismen atıfta bulunsun (refers to them by name):
const
ColNo = 70; // "No." sütununun sol kenarı (left edge)
ColName = 110; // şirket (company) adı
ColAddr = 300; // sokak (street) adresi
ColCity = 480; // şehir (city)
RowLeft = 50; // tablo çerçevesi (table frame): sol çizgi (left rule)
RowRight = 570; // tablo çerçevesi: sağ çizgi (right rule)
RowStep = 20; // taban çizgileri (baselines) arasındaki dikey mesafe
procedure PrintRow(Page: THPDFPage; Y: Single;
const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
if Shaded then
begin
// Satırın (row) arkasında gölgeli (shaded) bir şerit (band). Rectangle X, Y, Width, Height alır.
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;
Burada iki detay kendini amorti eder (earn their keep). Gölgeli şerit (shaded band) önce (first) çizilir, ardından üstüne (on top) metin yazılır; çünkü PDF'de boyama sırası (painting order) z-sırasıdır (z-order): metinden (text) sonra (after) dikdörtgeni (rectangle) doldurursanız satırı (row) gömersiniz (bury). Ve değişen (alternating) gölge, kendi hatrına yapılan (for its own sake) bir dekorasyon değildir. Yoğun (dense) bir raporda gözün yanlış (wrong) satıra (line) kaymasını engellemenin (cheapest way) en ucuz yoludur; işte (which is why) döngünün daha sonra (later) her satırda bir boolean'ı tersine çevirip (flips) doğrudan Shaded'a geçirmesinin (passes) nedeni de budur
Yukarıdaki sütun (column) konumları (positions) sabittir (fixed), bu da (which is) şemasını kontrol ettiğiniz bir rapor için dürüsttür (honest). Veriler değişken (variable) olduğunda, tahmin etmek (guess) yerine ölçün (measure). HotPDF, sayfa nesnesi üzerinde metin genişliği ölçümünü (text-width measurement) açığa çıkarır (exposes), böylece PrintRow'un üretim sürümü (production version) her sütundaki (column) beklenen en uzun değeri (longest expected value) alabilir, seçilen (chosen) yazı tipi (font) boyutunda (size) bir kez (once) ölçebilir (measure) ve sol kenarları (left edges) bu genişliklerden (widths) artı bir oluktan (gutter) türetebilir (derive). Rutinin (routine) şekli (shape) değişmez; yalnızca sabitlerin (constants) kaynağı (source) değişir
Başlık (header), çizgiler (rules) ve onlara (them) sahip olan (owns) tek (one) bir yer (place)
Bir sayfadan kayıp (scrolls off) sonraki (next) sayfada hiçbir sütun (column) etiketi (labels) olmadan devam eden (resumes) bir tablo okunamaz (unreadable). Çözüm (fix), başlığa (header) bir kez (once) çizdiğiniz bir şey olarak değil, yeniden (redraw) çizdiğiniz bir şey olarak davranmaktır (treat). Sütun başlıklarını (column titles) ve onları çevreleyen (frame) yatay (horizontal) çizgileri (rules) tek (single) bir rutine (routine) koyun (put) ve o rutini (routine) hem başlangıçta (start) hem de (and again) yeni (new) bir sayfa (page) açtığınız her (every) seferde çağırın. Başlık (header) ve gövde (body) aynı sütun (column) sabitlerini paylaştığı için, yapı (construction) gereği (by) hizalanırlar (line up)
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
// Sol: kaynak (source) etiketi ve sayfa numarası. Sağ: oluşturma (generation) zamanı.
Page.SetFont('Arial', [fsItalic], 10);
Page.TextOut(RowLeft, Y, 0, 'customer.db Sayfa ' + IntToStr(PageNo));
Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));
// Sütun başlıklarını kutulayan iki yatay çizgi.
Page.MoveTo(RowLeft, Y + 15);
Page.LineTo(RowRight, Y + 15);
Page.MoveTo(RowLeft, Y + 45);
Page.LineTo(RowRight, Y + 45);
Page.Stroke;
// Sütun (Column) başlıkları (titles); başlık (headings) olarak okunabilmeleri için (so) daha ağır (heavier) bir yüz (face) ile.
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; // ilk gövde (body) satırından (row) önce kutulu (boxed) başlığı (header) geçin (advance)
end;
DrawHeader'ın referans (reference) olarak Y'yi aldığına ve onu (it) ileri (forward) taşıdığına (moves) dikkat edin (Notice). Çağıranın (caller), başlığın (header) ne kadar (how) uzun (tall) olduğunu (is) hatırlaması (remember) asla gerekmez (never has to); onu (it) çizen (draws) rutin, onu bilen (knows) rutindir. Daha sonra başlık (header) bandına (band) bir logo (logo) veya bir filtre özeti (filter summary) eklediğinizde (add) düzenin (layout) kaymasını (drifting) engelleyen (keeps) şey bu tek (single) sahiplik kuralıdır (ownership rule). Gövde (body) döngüsü (loop) bundan habersiz kalır (oblivious). Sadece (just) Y'nin şu anda (currently) işaret (points) ettiği (wherever) yerden satır (rows) çizmeye (drawing) devam eder (keeps)
Çizgilerin kendileri bir liste ile bir tablo arasındaki farktır (difference). Dikey (Vertical) sütun (column) ayırıcıları (separators), x eksenine uygulanan aynı fikirdir: her bir (each) sütun (column) kenarında (edge) üst çizgiden (top rule) sayfadaki son (last) satırın (row) altına (bottom) kadar çalışan (run) bir MoveTo / LineTo / Stroke. Örnek (sample), okunabilir (readable) kalmak (stay) için yatay (horizontal) çizgilere (rules) bağlı (keeps) kalır (to), ancak sütun (column) sabitleri (constants) var (exist) olduğunda üretim adımı mekaniktir (mechanical)
İmleç (cursor) döngüsü (loop) sayfa sonuna (page break) sahiptir
Çizim (Drawing) kolay yarıdır (half). Bir oyuncağı (toy) bir rapordan (report) ayıran yarı sayfalamadır (pagination): Bir satırı (row) çizmeden önce (before), hala (still) sığıp sığmadığını (fits) bilmek (knowing) ve sığmadığında (when it does not) yeni (fresh) bir başlıkla (header) yeni bir sayfaya (page) başlamaktır. Bu (That) karar, tam olarak tek (one) bir yere, veriyi (data) dolaşan (walks) döngüye (loop) ve başka hiçbir (nowhere) yere (else) ait değildir (belongs)
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;
// Rapor başlığı (title), bir kez (once), ilk (first) sayfanın en üstünde (top).
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
// Yer mi kalmadı (Out of room)? Yeni bir sayfa aç (Open) ve başlığı orada uyarla (repeat).
if Y < 60 then
begin
Pdf.AddPage;
Page := Pdf.CurrentPage; // AddPage, CurrentPage'i (geçerli sayfayı) ileri (forward) taşır
Inc(PageNo);
Y := 760;
DrawHeader(Page, Y, PageNo);
end;
Shaded := not Shaded;
Page.SetFont('Arial', [], 10); // SetFont her yeni (new) sayfada yeniden (reissued) verilmelidir
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;
İki (Two) koordinat gerçeği (coordinate facts) tüm döngüyü (whole loop) yönlendirir (drive). PDF y'yi sol alt (bottom-left) köşeden yukarı (upward) doğru ölçer (measures), bu (so) nedenle satırlar (rows) her (each) seferinde Y'den RowStep çıkarılarak (subtracting) sayfanın aşağısına (down) doğru ilerler (march) ve dolu sayfa testi (page-full test), Y bir (some) üst sınırın (top) üzerine çıkmak yerine alt (bottom) kenar boşluğunun (margin) altına düştüğünde (drops) tetiklenir (fires). Yönü (direction) ters (backwards) alın (Get) ve döngü, boş bir sayfaya sahip olduğunu (full page of room) düşünürken ilk (first) satırınız alt (bottom) kenardan (edge) dışarı (off) yazdırılır
Diğer gerçek neredeyse (almost) herkesi bir (once) kez (catches) yakalar. AddPage yeni bir sayfa (page) oluşturur ve CurrentPage'i ona (it) yönlendirir (repoints), ancak hiçbir şeyi (nothing) devretmez (carries over): yazı tipini (font) değil, dolgu rengini (fill color) değil, konumu (position) değil. Bu nedenle (That is why) Page, her (every) AddPage'den sonra (after) CurrentPage'den (geçerli sayfadan) yeniden (re-read) okunur (is) ve gövde satırlarından (body rows) önce SetFont yeniden verilir (reissued). Yeniden (re-read) okumayı atlayın (Skip) ve az (just) önce terk (left) ettiğiniz (behind) sayfaya (page) çizmeye devam (keep) edin (drawing); yazı tipini atlayın (skip) ve yeni (new) sayfa görüntüleyicinin (viewer) geri döndüğü (falls back to) herhangi (whatever) bir varsayılanda (default) oluşturulur (renders)
Tablo (table) dışa aktarıcıyı (exporter) bozan durumlar (cases)
Çoğu (Most) tablo (table) hatası (bugs), birkaç düzine (dozen) düzenli (tidy) satırın mutlu (happy) yolunda (path) ortaya (show) çıkmaz (do not). Uçlarda (edges) yaşarlar (live) ve uçların nerede (where) olduklarını (are) öğrendiğinizde (know) onları (they) test etmek ucuzdur (cheap)
- Boş (Empty) veri kümeleri (datasets). Sıfır satır (rows) üzerindeki bir döngü, bir (a) başlığa (header) sahip olan (with) ve altında (under) hiçbir (nothing) şey olmayan bir sayfa (page) üretir, bu (which) en (at) azından (least) kasıtlı (intentional) görünür (looks). Başlığı (header) olmayan boş (blank) bir sayfa (page) bir (a) hata (failure) gibi (like) görünür (looks). Göndermeden (shipping) önce (before) hangisini istediğinize karar verin
- Tam olarak (exactly) sınıra (boundary) inen (lands) satır. Son satırı (last row) kenar (margin) boşluğunun (above) bir adım (step) yukarısında yer (sits) alan bir rapor oluşturun, ardından (then) bir (one) sonraki satırı (next row) bunun bir adım aşağısında (below) olan bir rapor (report) oluşturun. Birden (one) sapmış (Off-by-one) sayfalama (pagination), veri (data) tam olarak (exactly) yanlış (wrong) uzunluğa (length) sahip (is) olana kadar gizlenir (hides)
- Aşırı uzun (Overlong) değerler. Sütunundan daha geniş olan bir (a) şirket adı bir (the) sonrakine taşacaktır (run into). Alanı (field) ölçün (measure) ve (and) bir ilkeye (policy) karar (decide) verin: ikinci bir (a) satıra sarın (wrap), kırpın (clip) veya (or) bir elips (ellipsis) ile (with) kesin (truncate). Sessizlik (Silence) bir ilke (policy) değildir (not)
- Boş (Null) alanlar. Bir null değerini doğrudan (straight)
TextOut'a okumak, onu (it) nasıl (how) dönüştürdüğünüze (convert) bağlı (depending) olarak (on)Nulldeğişmez metni (literal text) veya (or) bir (a) boşluk (blank) olarak yüzeye (surface) çıkabilir (can). Varyant (variant) dönüştürmenin (conversion) sizin yerinize seçmesine izin (letting) vermek yerine işlemeyi (rendering) kasıtlı (deliberately) olarak (rather than) seçin (choose)
Sonucu (result) bitti (done) demeden (call) önce birden (one) fazla görüntüleyiciden (viewer) geçirin. Yazı tipi ikamesi (font substitution) ve kırpma (clipping), oluşturucular (renderers) arasında farklı davranır (behave) ve bir PDF okuyucusunda düzgün (square) görünen bir (a) tablo, diğerinde hizalanmamış (misaligned) bir sütun veya kırpılmış bir (a) şehir gösterebilir. Tekrarlanan (repeated) başlığın, satır (row) gölgelendirmenin ve kenar boşluklarının taşımadan kurtulduğunu (survive the move) ve veriler bir sınırı aştıktan (crosses a boundary) sonra (after) sayfa numaralarının (numbers) sürekli (continuous) kaldığını doğrulayın (confirm)
Görsel (visual) bir rapor tasarımcısına (designer) yaslanmak yerine ızgarayı (grid) kendiniz çizmek daha (more) fazla (is) koddur ve değiş tokuşu (tradeoff) açıkça isimlendirmeye (naming plainly) değer (worth): her bir (every) koordinatın (coordinate) sahibi (own) sizsiniz, bu da sunucu tarafı (server-side) toplu (batch) işler, faturalar ve her (every) makinede (machine) aynı (identically) şekilde işlenmesi gereken denetim (audit) dışa (exports) aktarımları için tam (exactly) olarak (what) istediğiniz şeydir; ve bir (a) seferlik (one-off) dahili listeleme için (for) kaçınmayı tercih (rather avoid) edeceğiniz genel (overhead) giderdir. İlki (former) için (For), kontrol, bir raporun (report) masanızda olduğu gibi (as) üretimde de aynı görünmesi gereken (look the same) ilk (first) seferde kendini amorti (pays) eder (for itself)
Rectangle, MoveTo ve LineTo çağrılarının ilk (first) olarak kendi başlarına (on their own) ele alınmasını isterseniz (want), yukarıdaki kurallar (rules) ve (and) gölgeli (shaded) bantlar, tuval (canvas) çizimi yönergesinde (walkthrough) ele (covered) alınan aynı (same) vektör (vector) ve renk (color) ilkellerine (primitives) yaslanır (lean). Burada kullanılan çizim (drawing) ilkelleri (primitives), Delphi ve (and) C++Builder için (for) geliştirilmiş olan HotPDF Bileşeninin (HotPDF Component) bir (part) parçasıdır