Artikel Teknis

Navigasi Bidang Formulir PDF di Delphi (Komponen PDFium)

Tekan Tab pada sebuah form PDF yang dibangun oleh kode Anda, dan kursor mendarat dua field lebih jauh dari seharusnya, atau melompati kolom kedua sepenuhnya, atau melompat kembali ke atas setelah field ketiga alih-alih field keempat. Orang yang mengisi sebuah invoice di viewer Anda mengharapkan keyboard menelusuri form itu dengan cara yang sama seperti ia menelusuri setiap form web yang pernah mereka gunakan. Ketika itu tidak terjadi, mereka meraih mouse, memburu kotak berikutnya, dan diam-diam menyimpulkan bahwa tool Anda belum selesai. Penelusuran field yang dapat diprediksi adalah pembeda antara sebuah viewer entri data yang ditoleransi orang dan yang dipercaya orang, dan itu hampir sepenuhnya soal menggunakan focus API yang tepat, bukan memalsukan input keyboard dengan klik simulasi

Contoh-contoh di bawah ini menggunakan PDFium Component, sebuah komponen VCL/LCL berbasis PDFium untuk Delphi, C++Builder, dan Lazarus. Navigasi adalah satu dari tiga hal yang harus dilakukan dengan benar oleh sebuah form viewer; dua lainnya, membuka form dengan benar dan menyimpan nilai yang diisi agar benar-benar muncul, adalah tempat sebagian besar kejutan bersembunyi, sehingga ketiganya dibahas di bawah ini

Membuka sebuah form: FormFill, FormType, dan pertanyaan seputar XFA

Akses field membutuhkan subsistem form-fill, yang dikendalikan oleh properti FormFill, untuk diaktifkan sebelum dokumen dibuka. Setelah aktif, FormType memberi tahu Anda jenis form apa yang sedang Anda hadapi, dan jawabannya mengubah kumpulan fitur yang dapat Anda janjikan:

Diagram cabang penyiapan FormFill dan deteksi FormType dalam penampil PDFium Component Delphi, memisahkan penanganan ftNone, ftAcroForm, dan ftXfaFull
FormType bercabang begitu FormFill diaktifkan, dan setiap cabang menjanjikan set fitur berbeda
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // aktifkan sebelum Active; diperlukan untuk akses field apa pun
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // navigasi dan pengeditan field lengkap tersedia
  ftXfaFull:
    ShowXfaNotice;      // XFA dirender dari templat XML miliknya sendiri;
                        // perlakukan pengeditan field sebagai terbatas
end;

Dua catatan praktis mengikuti dari switch tersebut. AcroForm adalah model form standar ISO 32000, dan itulah yang menjadi target setiap API di sini. Dokumen XFA menyematkan arsitektur form XML miliknya sendiri, sehingga menjanjikan pelanggan pengeditan XFA penuh setelah sebuah demo AcroForm singkat adalah komitmen yang akan Anda sesali. Catatan kedua adalah soal efek samping: menetapkan FormFill ke True juga menginisialisasi JavaScript dokumen. Pada sebuah viewer entri data, itu benar-benar tepat, karena script kalkulasi adalah yang menjaga total berjalan tetap terkini seiring seseorang mengetik. Pada sebuah jendela pratinjau untuk file yang asal-usulnya tidak diketahui, itu benar-benar keliru. Artikel pratinjau PDF yang aman membahas sisi FormFill := False dari trade-off tersebut

Penelusuran tombol Tab yang mendarat di tempat yang diharapkan pengguna

Kembali ke masalah keyboard dari awal tadi. Godaannya adalah memalsukan Tab dengan mensintesis klik mouse pada rectangle widget berikutnya, yang rusak seketika saat sebuah field ter-scroll keluar layar atau dua widget saling tumpang tindih. Focus API justru menggerakkan focus milik form itu sendiri secara langsung, tanpa tebak-tebakan geometri. Lima panggilan mencakup ini: FocusFormField berdasarkan indeks, FocusNextFormField dan FocusPreviousFormField untuk melangkah, FocusedFormFieldIndex untuk membaca posisi Anda saat ini, dan ClearFormFieldFocus untuk melepaskan focus sepenuhnya

Diagram penelusuran fokus tombol Tab dalam penampil PDFium Component Delphi di mana FocusNextFormField melingkari dalam urutan tab satu halaman dan lima API fokus mencakup navigasi papan tik
Traversal berloop di dalam tab order satu halaman, sehingga penyeberangan ke halaman berikutnya tetap menjadi tugas viewer
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // misalnya "Field 4 dari 17: InvoiceDate"
end;

Satu perilaku yang sering membuat orang tersandung adalah wrap-nya. Penelusuran bekerja melalui urutan tab pada halaman yang aktif dan berputar di dalamnya: melangkahi field terakhir dan Anda kembali ke yang pertama. Kedua fungsi pelangkah mengembalikan indeks field yang baru, atau -1 ketika halaman sama sekali tidak memiliki field. Perputaran itu berlaku per halaman, bukan per dokumen, yang berarti berpindah ke halaman berikutnya adalah tugas Anda, bukan tugas pustaka ini. Bandingkan indeks yang dikembalikan dengan indeks tempat Anda memulai, perhatikan saat ia telah berputar, dan majukan PageNumber sendiri jika form dimaksudkan untuk dibaca sebagai satu urutan yang berkesinambungan. Lewati pemeriksaan itu dan sebuah form dua halaman akan diam-diam memerangkap kursor di halaman satu, yang merupakan varian tersendiri dari keluhan Tab yang rusak

Penelusuran menjadi berguna begitu bagian UI lainnya bereaksi terhadapnya. Event OnFormFieldEnter terpicu saat focus tiba, dan pada viewer, OnFormFieldFocusChange melaporkan indeks field yang baru, sehingga sebuah panel samping dapat tetap selaras dengan apa pun yang baru saja dipilih oleh keyboard. Ketika Anda membutuhkan pemetaan sebaliknya, dari sebuah posisi layar ke sebuah field, properti terindeks FormFieldAt melakukan hit-testing untuk pratinjau tooltip dan panel click-to-edit. Ada keuntungan aksesibilitas yang senyap dalam semua ini: karena focus mengikuti urutan field milik dokumen itu sendiri, jalur yang Anda rangkai untuk tombol Tab adalah jalur yang sama yang diumumkan oleh sebuah screen reader, tanpa kerja tambahan

Menampilkan nama field, bukan sekadar nomor indeks mentah, membutuhkan satu properti lagi. FormFieldInfo[] mengembalikan sebuah record TPdfFormFieldInfo per indeks, membawa nama field, tipe, ukuran font, status checked, nilai ekspor, dan keanggotaan grup, yang merupakan apa yang seharusnya ditampilkan oleh sebuah daftar navigasi ("Field 4 dari 17: InvoiceDate", bukan sekadar "4"). Radio group adalah kasus yang layak mendapat file uji khusus. Beberapa widget dapat berbagi satu nama field yang sama, sehingga sebuah daftar yang disusun secara naif dari widget akan menampilkan grup yang sama beberapa kali dan membingungkan siapa pun yang membacanya

Mengapa nilai yang diisi muncul kosong, dan panggilan yang memperbaikinya

Keluhan lain yang memenuhi antrean dukungan lebih meresahkan daripada sebuah tombol Tab yang berkelakuan buruk: sebuah form diisi secara programatis, pelanggan membukanya di Acrobat, dan setiap field terlihat kosong. Klik ke dalam sebuah field dan nilainya langsung tampak. Datanya ada di dalam file sepanjang waktu. Yang hilang adalah gambaran dari data tersebut, dan alasannya layak dipahami sekali karena itu menjelaskan seluruh keluarga bug

Sebuah field teks AcroForm menyimpan nilainya dalam entri /V pada dictionary field (ISO 32000-1 §12.7.3.3). Apa yang sebenarnya digambar oleh sebuah viewer adalah hal yang terpisah: appearance stream milik widget di bawah /AP (§12.5.5), sebuah cuplikan konten kecil yang sudah dirender sebelumnya. Tulis /V dan biarkan /AP tidak tersentuh, dan keduanya akan bergeser saling menjauh. Nilainya ada di sana; versi hasil render darinya basi atau tidak ada. Acrobat kebetulan membangun ulang appearance sebuah field ketika field itu mendapat focus, yang merupakan seluruh penjelasan bagi nilai yang hanya muncul saat diklik. Flag NeedAppearances yang lama, yang meminta viewer untuk membangun ulang appearance untuk Anda, tidak pernah bekerja secara seragam dan sudah deprecated di PDF 2.0, dan print server serta pembuat thumbnail mengabaikannya sepenuhnya. Mereka menggambar /AP dan tidak ada yang lain, sehingga jika /AP kosong, mereka mencetak sebuah kotak kosong

Menetapkan sebuah nilai melalui FormField[i] hanya menulis /V. Itulah sebabnya mengisi sebuah form adalah urutan tiga langkah, dan langkah yang sering terlewat oleh tim adalah langkah tengahnya:

Diagram nilai /V versus penyimpangan appearance /AP dalam bidang AcroForm dan urutan pengisian Delphi tiga langkah yang dibangun di sekitar GenerateFormAppearances
Menetapkan nilai hanya menulis /V, dan langkah tengah itulah yang melukis ulang apa yang benar-benar dirender print server
procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // hanya menulis /V

  // Bangun ulang appearance stream /AP; tanpa ini form
  // akan terlihat kosong di Acrobat sampai setiap field diklik
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances adalah keseluruhan perbaikannya. Ia membangun ulang appearance stream setiap widget dari nilai, font, dan quadding yang berlaku saat ini, sehingga sebuah viewer yang tidak pernah menjalankan event focus, sebuah print server, atau sebuah pembuat thumbnail, tetap menggambar keadaan yang sudah terisi. Panggil ini satu kali setelah sekumpulan penetapan nilai, bukan satu kali per field. Pembuatan appearance melakukan pekerjaan layout yang sesungguhnya, dan panggilan per-field akan melipatgandakan itu di seluruh form besar tanpa guna

Membangun ulang appearance juga menjadi momen ketika font dan perataan menegaskan dirinya, yang menjadi sumber sebuah kejutan orde kedua. Stream yang baru menata setiap nilai di dalam rectangle widget menggunakan font, ukuran, dan quadding milik field tersebut. Sebuah nilai yang muat dengan nyaman di form uji Anda bisa terpotong atau menyusut pada salinan milik pelanggan tempat field yang sama lebih sempit. Field berukuran otomatis (ukuran font nol) menyusutkan teks agar muat; field berukuran tetap hanya akan memotongnya. Keduanya sah, dan satu-satunya cara jujur untuk mengetahui mana yang dilakukan oleh sebuah form tertentu adalah dengan melihat output yang dibangun ulang, bukan string yang Anda tulis. Ketika seseorang melaporkan teks terpotong di tepi sebuah kotak, inilah alasannya hampir selalu

Perlakukan verifikasi sebagai bagian dari penyelesaian pekerjaan, bukan sebagai renungan belakangan. Buka file yang sudah disimpan di Acrobat dan pastikan nilai-nilainya terlihat sebelum Anda menyentuh field mana pun. Kemudian cetak file itu ke PDF atau ke sebuah gambar dari viewer yang berbeda, satu yang sepenuhnya mengabaikan logika form, dan pastikan nilai-nilainya bertahan melalui jalur itu juga. Bersama-sama, kedua pemeriksaan itu menangkap setiap varian dari pergeseran /V-versus-/AP

Konfigurasi field yang lolos demo tetapi gagal di lapangan

Form demo yang bersih menyembunyikan sekumpulan edge case yang tidak disembunyikan oleh file pelanggan. Empat di antaranya menjadi penyebab sebagian besar laporan "berhasil di komputer saya"

  • Nilai ekspor checkbox. Status "on" tidak selalu Yes. Sebuah form bebas mendefinisikan nilai ekspornya sendiri, dan menulis string yang salah membuat kotak terlihat tidak tercentang secara visual padahal kode Anda yakin telah mengaturnya. Baca nilai ekspor dari FormFieldInfo[], jangan mengasumsikannya begitu saja
  • Radio group dengan nama bersama. Satu field, beberapa widget. Nilai yang Anda tetapkan menentukan widget mana yang terbaca sebagai terpilih, sehingga kode UI yang mengasumsikan satu nama dipetakan ke satu rectangle akan berakhir menggambar cincin focus pada tombol yang salah
  • Field terkalkulasi. Total yang dipertahankan oleh JavaScript dokumen diperbarui sebagai respons terhadap event field. Sebuah pengisian programatis yang melewati event tersebut harus memicu perhitungan ulang atau menimpa field terkalkulasi secara langsung. Sebuah form tempat item baris dan totalnya tidak sesuai lebih buruk daripada kedua perbaikan itu sekalipun
  • Field wajib yang tersembunyi. Form kondisional menyembunyikan field yang masih ditandai required. Tentukan sejak awal apakah validasi Anda menghormati visibilitas atau flag required mentahnya, lalu catat keputusan itu di suatu tempat yang bisa ditemukan oleh tim support

Satu perbedaan layak diselesaikan sebelum ia menggigit Anda: membangkitkan appearance bukanlah flattening. GenerateFormAppearances membuat nilai terlihat di mana-mana sambil tetap membiarkan field dapat diedit. Flattening memanggang appearance ke dalam konten halaman statis dan mencabut interaktivitasnya untuk selamanya, yang tepat untuk sebuah salinan arsip dan keliru untuk sebuah form yang masih harus diisi oleh orang berikutnya. Jika FormType melaporkan ftXfaFull alih-alih ftAcroForm, tidak ada satu pun permukaan pengeditan di sini yang berlaku dengan mulus, karena dokumen itu dirender dari templat XML miliknya sendiri; deteksi kasus itu dan beri tahu pengguna, alih-alih membiarkan mereka menemukan batasnya sendiri

Subsistem form-fill, penelusuran focus, dan pembuatan appearance yang ditunjukkan di sini adalah bagian dari PDFium Component untuk Delphi, C++Builder, dan Lazarus/FPC. Jika viewer Anda juga menangani markup peninjau di samping data form, artikel peninjauan anotasi membahas model yang bersebelahan itu