Dataset adalah baris dan kolom; halaman PDF adalah kisi koordinat kosong yang tidak mengenal keduanya. Menjembatani kesenjangan itulah seluruh tugas di sini. Tidak ada pemanggilan DrawTable di HotPDF yang menerima dataset dan menghasilkan kisi berformat secara otomatis. Yang tersedia justru primitif-primitif penyusun kisi: TextOut untuk menempatkan string pada satu titik, SetFont untuk memilih jenisnya, Rectangle dan Fill untuk memberi warna latar pada pita, serta MoveTo / LineTo / Stroke untuk menggambar garis. Ekporter tabel yang berfungsi adalah disiplin mengubah pola pikir baris-dan-kolom menjadi koordinat x dan y yang eksplisit, lalu menjaga koordinat tersebut tetap konsisten saat data melewati bagian bawah halaman
Contoh berikut melaporkan data pelanggan, tetapi tidak ada satu pun kode gambar yang peduli dari mana baris-baris tersebut berasal. Contoh asli menggunakan TTable warisan lama; kueri FireDAC, dataset dalam memori, atau larik record biasa pun bisa dimasukkan ke rutinitas yang sama tanpa perubahan. Yang penting adalah Anda bisa menelusuri data satu baris demi satu baris dan membaca empat field string dari masing-masing baris. Pisahkan rendering dari sumber data, dan Anda bisa mengubah salah satu sisi tanpa mengganggu yang lain
Geometri kolom harus ditetapkan lebih dulu
Sebelum satu karakter pun digambar, tentukan letak setiap kolom. Tabel di sini memiliki empat kolom, sehingga membutuhkan empat tepi kiri dan satu margin kanan yang sudah diketahui. Meng-hardcode angka ajaib di setiap pemanggilan TextOut, seperti kebiasaan contoh cepat, adalah penyebab utama sulitnya melebarkan tabel di kemudian hari. Namai tepi-tepinya sekali saja, dalam poin dari sudut kiri bawah sebagai titik asal, dan setiap pemanggilan gambar merujuk pada nama tersebut:
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;
Ada dua detail penting di sini. Pita berwarna digambar lebih dulu, baru teks di atasnya, karena urutan penggambaran adalah z-order dalam PDF: mengisi persegi panjang setelah teks akan mengubur baris tersebut. Dan pola warna selang-seling bukan sekadar hiasan. Pada laporan yang padat, ini adalah cara paling murah untuk mencegah mata tergelincir ke baris yang salah, itulah mengapa loop di bawah ini membalik boolean di setiap baris dan langsung meneruskannya ke Shaded
Posisi kolom di atas bersifat tetap, yang wajar untuk laporan dengan skema yang Anda kendalikan. Ketika data bervariasi, ukurlah alih-alih menebak. HotPDF menyediakan pengukuran lebar teks pada objek halaman, sehingga versi produksi PrintRow bisa mengambil nilai terpanjang yang diharapkan di setiap kolom, mengukurnya sekali pada ukuran font yang dipilih, dan menurunkan tepi kiri dari lebar tersebut ditambah gutter. Bentuk rutinitas tidak berubah; hanya sumber konstantanya yang berbeda
Header, garis, dan satu tempat yang memilikinya
Tabel yang bergulir melewati satu halaman dan dilanjutkan di halaman berikutnya tanpa label kolom tidak bisa dibaca. Solusinya adalah memperlakukan header sebagai sesuatu yang digambar ulang, bukan digambar sekali. Taruh judul kolom dan garis horizontal yang membingkainya dalam satu rutinitas, lalu panggil rutinitas itu baik di awal maupun setiap kali halaman baru dibuka. Karena header dan isi menggunakan konstanta kolom yang sama, keduanya sejajar secara otomatis
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;
Perhatikan bahwa DrawHeader menerima Y by reference dan memajukannya. Pemanggil tidak perlu mengingat tinggi header; rutinitas yang menggambarnya adalah yang tahu. Aturan kepemilikan tunggal inilah yang mencegah tata letak bergeser saat Anda nanti menambahkan logo atau rangkuman filter ke pita header. Loop isi tetap tidak peduli. Ia cukup terus menggambar baris dari posisi Y saat ini
Garis-garis itu sendiri adalah perbedaan antara daftar dan tabel. Pemisah kolom vertikal adalah gagasan yang sama diterapkan pada sumbu x: sebuah MoveTo / LineTo / Stroke di setiap tepi kolom, dijalankan dari garis atas ke bagian bawah baris terakhir di halaman. Contoh ini menggunakan garis horizontal agar lebih mudah dibaca, tetapi langkah produksinya mudah begitu konstanta kolom sudah ada
Loop kursor yang mengelola pemisah halaman
Menggambar adalah bagian yang mudah. Bagian yang membedakan mainan dari laporan sungguhan adalah paginasi: mengetahui, sebelum menggambar baris, apakah baris itu masih muat, dan memulai halaman baru dengan header baru jika tidak. Keputusan itu berada tepat di satu tempat saja, yaitu loop yang menelusuri data, dan tidak di tempat lain
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;
Dua fakta koordinat menggerakkan seluruh loop. PDF mengukur y ke atas dari sudut kiri bawah, sehingga baris bergerak turun halaman dengan mengurangi RowStep dari Y setiap kali, dan pengujian halaman penuh diaktifkan saat Y turun di bawah margin bawah, bukan di atas suatu bagian atas tertentu. Membalik arahnya akan membuat baris pertama tercetak di luar tepi bawah sementara loop mengira masih ada satu halaman penuh ruang
Fakta lain yang hampir selalu mengejutkan semua orang. AddPage membuat halaman baru dan mengarahkan ulang CurrentPage ke sana, tetapi tidak membawa apa pun: tidak font, tidak warna isian, tidak posisi. Itulah mengapa Page dibaca ulang dari CurrentPage setelah setiap AddPage, dan mengapa SetFont diterbitkan ulang sebelum baris isi. Lewatkan pembacaan ulang dan Anda terus menggambar ke halaman yang baru saja ditinggalkan; lewatkan font dan halaman baru akan dirender dengan font default apa pun yang digunakan penampil
Kasus yang merusak ekporter tabel
Sebagian besar bug tabel tidak muncul pada skenario happy path dengan beberapa lusin baris yang rapi. Mereka bersembunyi di pinggiran, dan pinggiran tersebut murah untuk diuji begitu Anda tahu di mana lokasinya
- Dataset kosong. Loop atas nol baris menghasilkan halaman dengan header dan tidak ada apa pun di bawahnya, yang setidaknya terlihat disengaja. Halaman kosong tanpa header terlihat seperti kegagalan. Tentukan mana yang Anda inginkan sebelum merilis
- Baris yang tepat berada di batas. Buat laporan yang baris terakhirnya berada satu langkah di atas margin, lalu satu laporan yang baris berikutnya satu langkah di bawahnya. Bug paginasi off-by-one bersembunyi hingga data memiliki panjang yang persis salah
- Nilai yang terlalu panjang. Nama perusahaan yang lebih lebar dari kolomnya akan melenceng ke kolom berikutnya. Ukur field-nya dan tentukan kebijakan: bungkus ke baris kedua, potong, atau pangkas dengan elipsis. Diam bukan suatu kebijakan
- Field null. Membaca null langsung ke
TextOutbisa muncul sebagai teks literalNullatau sebagai kosong, tergantung cara konversinya. Pilih rendering secara eksplisit daripada membiarkan konversi variant yang memilih untuk Anda
Jalankan hasilnya melalui lebih dari satu penampil sebelum menyebutnya selesai. Substitusi font dan pemotongan berperilaku berbeda di berbagai renderer, dan tabel yang terlihat rapi di satu pembaca PDF bisa menampilkan kolom yang tidak sejajar atau kota yang terpotong di pembaca lain. Konfirmasikan bahwa header yang diulang, pewarnaan baris, dan margin bertahan setelah data melewati batas, dan bahwa nomor halaman tetap berurutan
Menggambar kisi sendiri daripada mengandalkan desainer laporan visual membutuhkan lebih banyak kode, dan tradeoff-nya layak disebutkan secara jelas: Anda memiliki setiap koordinat, yang adalah tepat apa yang Anda inginkan untuk pekerjaan batch sisi server, faktur, dan ekspor audit yang harus dirender identik di setiap mesin, dan tepat overhead yang lebih baik Anda hindari untuk listingan internal satu kali. Untuk yang pertama, kontrol ini terbayar dengan sendirinya saat pertama kali laporan harus terlihat sama di produksi seperti di meja Anda
Garis dan pita berwarna di atas mengandalkan primitif vektor dan warna yang sama yang dibahas dalam panduan menggambar kanvas, jika Anda ingin pemanggilan Rectangle, MoveTo, dan LineTo dibahas tersendiri terlebih dahulu. Primitif gambar yang digunakan di sini adalah bagian dari HotPDF Component untuk Delphi dan C++Builder