Artikel Teknis

Merender Tabel Data ke PDF di Delphi dengan HotPDF

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:

Geometri kolom tabel HotPDF di Delphi: tepi x bernama di 70, 110, 300, dan 480 poin di antara garis bingkai di 50 dan 570 poin
Empat tepi kiri bernama dan margin kanan yang diketahui mengunci seluruh geometri tabel sebelum panggilan TextOut pertama
const
  ColNo   = 70;    // tepi kiri kolom "No."
  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;    // jarak vertikal antarbaseline

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // Pita berarsir di belakang baris; Rectangle menerima 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

HotPDF menggambar ulang judul dan garis DrawHeader pada halaman satu dan lagi setelah setiap AddPage sehingga kedua halaman PDF terbuka dengan header identik
Rutinitas header berjalan lagi di setiap halaman baru, sehingga judul dan garis mendarat di tempat yang sama secara konstruksi
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Kiri: label sumber dan nomor halaman; kanan: waktu pembuatan
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Dua garis horizontal yang membingkai judul kolom
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // Judul kolom dengan font lebih tebal agar terbaca sebagai heading
  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;  // lewati header berkotak sebelum baris isi pertama
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

Flowchart loop kursor Delphi di mana Y di bawah 60 memicu AddPage, pembacaan ulang CurrentPage, penerbitan ulang SetFont, dan header berulang sebelum baris tabel berikutnya
Loop kursor adalah satu-satunya tempat yang membuka halaman baru dan menginisialisasi ulangnya ketika Y jatuh di bawah margin bawah
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;

    // Judul laporan, sekali, di bagian atas halaman pertama
    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
      // Kehabisan ruang? Buka halaman baru dan ulangi header di sana
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage memajukan CurrentPage
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont harus dipanggil ulang pada setiap halaman baru
      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 TextOut bisa muncul sebagai teks literal Null atau 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 Delphi Component untuk Delphi dan C++Builder