กดปุ่ม Tab ในฟอร์ม PDF ที่โค้ดของคุณสร้างขึ้น แล้วเคอร์เซอร์กลับไปตกที่ฟิลด์ที่ห่างจากตำแหน่งที่ควรจะเป็นสองฟิลด์ หรือข้ามคอลัมน์ที่สองไปเลย หรือกระโดดกลับไปด้านบนหลังฟิลด์ที่สามแทนที่จะเป็นฟิลด์ที่สี่ ผู้ใช้ที่กำลังกรอกใบแจ้งหนี้ในตัวแสดงผลของคุณคาดหวังว่าคีย์บอร์ดจะไล่ลำดับฟอร์มแบบเดียวกับที่เคยใช้กับฟอร์มเว็บทุกแบบที่เขาเคยเจอมา เมื่อมันไม่เป็นแบบนั้น เขาก็จะหันไปใช้เมาส์ ไล่หากล่องถัดไปเอง แล้วเงียบ ๆ ตัดสินใจว่าเครื่องมือของคุณยังทำไม่เสร็จ การไล่ลำดับฟิลด์ที่คาดเดาได้คือความต่างระหว่างตัวแสดงผลสำหรับกรอกข้อมูลที่คนแค่ทนใช้ กับตัวที่คนไว้ใจใช้จริง และเกือบทั้งหมดขึ้นอยู่กับการใช้ focus API ที่ถูกต้อง แทนที่จะปลอมการกดคีย์บอร์ดด้วยการจำลองคลิกเมาส์
ตัวอย่างด้านล่างใช้ PDFium Component ซึ่งเป็นคอมโพเนนต์ VCL/LCL ที่สร้างบน PDFium สำหรับ Delphi, C++Builder และ Lazarus การไล่ลำดับการนำทางเป็นหนึ่งในสามเรื่องที่ตัวแสดงผลฟอร์มต้องทำให้ถูกต้อง ส่วนอีกสองเรื่องคือการเปิดฟอร์มอย่างถูกวิธี และการบันทึกค่าที่กรอกแล้วให้ปรากฏขึ้นจริง ซึ่งเป็นจุดที่ความประหลาดใจส่วนใหญ่ซ่อนอยู่ บทความนี้จึงครอบคลุมทั้งสามเรื่อง
การเปิดฟอร์ม: FormFill, FormType และคำถามเรื่อง XFA
การเข้าถึงฟิลด์ต้องเปิดใช้งานระบบย่อย form-fill ซึ่งควบคุมด้วยพร็อพเพอร์ตี้ FormFill ก่อนที่จะเปิดเอกสาร เมื่อเปิดใช้งานแล้ว FormType จะบอกว่าคุณกำลังเจอฟอร์มประเภทใด และคำตอบนั้นเปลี่ยนชุดฟีเจอร์ที่คุณรับปากลูกค้าได้:
Pdf.FileName := FormPath;
Pdf.FormFill := True; // เปิดใช้งานก่อน Active; จำเป็นสำหรับการเข้าถึงฟิลด์ใด ๆ
Pdf.Active := True;
case Pdf.FormType of
ftNone:
DisableFormPanel('This document has no interactive form');
ftAcroForm:
BuildFieldList; // ไล่ลำดับฟิลด์และแก้ไขได้เต็มรูปแบบ
ftXfaFull:
ShowXfaNotice; // XFA เรนเดอร์จากเทมเพลต XML ของตัวเอง;
// ถือว่าการแก้ไขฟิลด์มีข้อจำกัด
end;
มีข้อสังเกตเชิงปฏิบัติสองข้อตามมาจากตัวสวิตช์นี้ AcroForm คือโมเดลฟอร์มมาตรฐานตาม ISO 32000 และเป็นสิ่งที่ API ทุกตัวในบทความนี้มุ่งเป้าไป ส่วนเอกสาร XFA ฝังสถาปัตยกรรมฟอร์ม XML ของตัวเองไว้ ดังนั้นการรับปากลูกค้าว่าจะแก้ไข XFA ได้เต็มรูปแบบหลังจากสาธิต AcroForm สั้น ๆ คือคำมั่นที่คุณจะต้องเสียใจภายหลัง ข้อสังเกตที่สองเกี่ยวกับผลข้างเคียง: การตั้งค่า FormFill เป็น True ยังเป็นการเริ่มต้นระบบ JavaScript ของเอกสารด้วย ในตัวแสดงผลสำหรับกรอกข้อมูลนี่คือสิ่งที่ถูกต้องพอดี เพราะสคริปต์คำนวณคือสิ่งที่ทำให้ยอดรวมทันสมัยขณะที่มีคนพิมพ์อยู่ แต่ในหน้าต่างพรีวิวสำหรับไฟล์ที่ไม่ทราบที่มา นี่คือสิ่งที่ผิดพอดีเช่นกัน บทความเรื่องการพรีวิว PDF อย่างปลอดภัย ครอบคลุมฝั่ง FormFill := False ของการแลกเปลี่ยนนี้
การไล่ลำดับด้วยปุ่ม Tab ที่พาไปยังจุดที่ผู้ใช้คาดหวัง
กลับไปที่ปัญหาคีย์บอร์ดตั้งแต่ต้น สิ่งที่ล่อใจให้ทำคือปลอมปุ่ม Tab ด้วยการจำลองคลิกเมาส์ลงบนสี่เหลี่ยมของวิดเจ็ตถัดไป ซึ่งจะพังทันทีที่ฟิลด์เลื่อนออกนอกจอหรือวิดเจ็ตสองตัวซ้อนทับกัน แทนที่จะทำแบบนั้น focus API จะย้าย focus ของฟอร์มเองโดยตรง โดยไม่ต้องเดาเรื่องตำแหน่งเรขาคณิตเลย มีฟังก์ชันห้าตัวที่ครอบคลุมเรื่องนี้: FocusFormField ตามดัชนี, FocusNextFormField และ FocusPreviousFormField สำหรับการก้าวไปทีละฟิลด์, FocusedFormFieldIndex สำหรับอ่านตำแหน่งปัจจุบัน และ ClearFormFieldFocus สำหรับยกเลิก focus ทั้งหมด
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
if ssShift in Shift then
PdfView.FocusPreviousFormField
else
PdfView.FocusNextFormField;
UpdateFieldStatus; // เช่น "Field 4 of 17: InvoiceDate"
end;
พฤติกรรมจุดเดียวที่ทำให้คนสับสนคือการวนกลับ การไล่ลำดับทำงานผ่านลำดับ tab ของหน้าปัจจุบันแล้ววนอยู่ในนั้น: ก้าวผ่านฟิลด์สุดท้ายไปแล้วคุณจะกลับมาที่ฟิลด์แรกอีกครั้ง ฟังก์ชันการก้าวทั้งสองตัวคืนค่าดัชนีฟิลด์ใหม่ หรือ -1 เมื่อหน้านั้นไม่มีฟิลด์เลย การวนนี้เกิดขึ้นต่อหน้า ไม่ใช่ต่อเอกสาร ซึ่งหมายความว่าการข้ามไปหน้าถัดไปเป็นหน้าที่ของคุณ ไม่ใช่ของไลบรารี ให้เทียบดัชนีที่ได้กับดัชนีที่คุณเริ่มต้นไว้ สังเกตว่ามันวนกลับมาแล้วหรือยัง แล้วเลื่อน PageNumber เองถ้าฟอร์มนั้นตั้งใจให้อ่านเป็นลำดับต่อเนื่องเดียว ถ้าข้ามการตรวจสอบนี้ไป ฟอร์มสองหน้าจะขังเคอร์เซอร์ไว้ที่หน้าแรกอย่างเงียบ ๆ ซึ่งเป็นอาการอีกแบบหนึ่งของปัญหา Tab เสีย
การไล่ลำดับจะมีประโยชน์จริงเมื่อ UI ส่วนที่เหลือตอบสนองต่อมันด้วย เหตุการณ์ OnFormFieldEnter จะยิงขึ้นเมื่อ focus มาถึง และบนตัวแสดงผล OnFormFieldFocusChange จะรายงานดัชนีฟิลด์ใหม่ เพื่อให้แผงด้านข้างตามทันสิ่งที่คีย์บอร์ดเพิ่งเลือกไป เมื่อคุณต้องการการแมปย้อนกลับ จากตำแหน่งบนหน้าจอไปยังฟิลด์ พร็อพเพอร์ตี้แบบดัชนี FormFieldAt จะทำการ hit-testing ให้สำหรับพรีวิว tooltip และแผงคลิกเพื่อแก้ไข ทั้งหมดนี้ยังมีผลพลอยได้เงียบ ๆ ด้านการช่วยการเข้าถึงด้วย: เพราะ focus เดินตามลำดับฟิลด์ของเอกสารเอง เส้นทางที่คุณต่อไว้สำหรับปุ่ม Tab จึงเป็นเส้นทางเดียวกับที่โปรแกรมอ่านหน้าจอประกาศ โดยไม่ต้องทำอะไรเพิ่มเลย
การแสดงชื่อฟิลด์แทนเลขดัชนีดิบต้องใช้พร็อพเพอร์ตี้อีกตัวหนึ่ง FormFieldInfo[] คืนค่าเป็นเรคคอร์ด TPdfFormFieldInfo ต่อดัชนีหนึ่งตัว ซึ่งบรรจุชื่อฟิลด์ ประเภท ขนาดฟอนต์ สถานะการติ๊กเลือก ค่า export และการเป็นสมาชิกกลุ่ม ซึ่งเป็นสิ่งที่รายการนำทางควรแสดง ("Field 4 of 17: InvoiceDate" แทนที่จะเป็นแค่ "4") กลุ่มปุ่มตัวเลือก (radio group) เป็นกรณีที่คุ้มค่าจะมีไฟล์ทดสอบเฉพาะ วิดเจ็ตหลายตัวสามารถใช้ชื่อฟิลด์เดียวกันได้ ดังนั้นรายการที่ประกอบขึ้นแบบไร้เดียงสาจากวิดเจ็ตจะแสดงกลุ่มเดียวกันซ้ำหลายครั้งและทำให้ทุกคนที่อ่านสับสน
ทำไมค่าที่กรอกแล้วถึงออกมาว่างเปล่า และฟังก์ชันที่แก้ปัญหานี้
ข้อร้องเรียนอีกอย่างที่ถมคิวซัพพอร์ตน่าตกใจกว่าปุ่ม Tab ที่ทำงานผิดปกติเสียอีก: ฟอร์มถูกกรอกด้วยโปรแกรม ลูกค้าเปิดมันใน Acrobat แล้วทุกฟิลด์ดูว่างเปล่า พอคลิกเข้าไปในฟิลด์ ค่าที่กรอกกลับปรากฏขึ้นทันที ข้อมูลอยู่ในไฟล์มาตลอด สิ่งที่ขาดหายไปคือ "ภาพ" ของข้อมูล และเหตุผลนั้นควรทำความเข้าใจไว้สักครั้งเพราะมันอธิบายบั๊กได้ทั้งตระกูลเลย
ฟิลด์ข้อความของ AcroForm เก็บค่าไว้ในรายการ /V ของ field dictionary (ISO 32000-1 §12.7.3.3) แต่สิ่งที่ตัวแสดงผลวาดออกมาจริง ๆ เป็นอีกเรื่องหนึ่งแยกต่างหาก: appearance stream ของวิดเจ็ตที่อยู่ใต้ /AP (§12.5.5) ซึ่งเป็นชิ้นเนื้อหาเล็ก ๆ ที่เรนเดอร์ไว้ล่วงหน้า เขียน /V แล้วปล่อย /AP ทิ้งไว้เฉย ๆ ทั้งสองอย่างก็จะเริ่มไม่ตรงกัน ค่ามีอยู่จริง แต่เวอร์ชันที่เรนเดอร์ออกมานั้นเก่าหรือไม่มีอยู่เลย Acrobat บังเอิญสร้าง appearance ของฟิลด์ใหม่เมื่อฟิลด์ได้ focus ซึ่งเป็นคำอธิบายทั้งหมดว่าทำไมค่าถึงปรากฏก็ต่อเมื่อคลิกเท่านั้น แฟล็ก NeedAppearances แบบเก่า ซึ่งขอให้ตัวแสดงผลสร้าง appearance ใหม่ให้เอง ไม่เคยทำงานได้สม่ำเสมอและถูกเลิกใช้ (deprecated) แล้วใน PDF 2.0 อีกทั้ง print server และตัวสร้าง thumbnail ก็เพิกเฉยต่อมันโดยสิ้นเชิง โปรแกรมพวกนี้วาดแค่ /AP เท่านั้น ดังนั้นถ้า /AP ว่างเปล่า มันก็จะพิมพ์กล่องเปล่าออกมา
การกำหนดค่าโดยผ่าน FormField[i] เขียนแค่ /V เท่านั้น นี่คือเหตุผลที่การกรอกฟอร์มเป็นลำดับสามขั้นตอน และขั้นตอนที่ทีมมักตกหล่นไปคือขั้นตอนตรงกลาง:
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]; // เขียนแค่ /V เท่านั้น
// สร้าง appearance stream ของ /AP ใหม่; ถ้าไม่ทำขั้นตอนนี้ ฟอร์ม
// จะดูว่างเปล่าใน Acrobat จนกว่าจะมีการคลิกแต่ละฟิลด์
Pdf.GenerateFormAppearances;
Pdf.SaveAs(OutputPath);
end;
GenerateFormAppearances คือทางแก้ทั้งหมดในตัวเดียว มันสร้าง appearance stream ของทุกวิดเจ็ตขึ้นใหม่จากค่าปัจจุบัน ฟอนต์ และการจัดวางข้อความ (quadding) ทำให้ตัวแสดงผลที่ไม่เคยเรียก focus event เลย อย่าง print server หรือตัวสร้าง thumbnail ก็ยังวาดสถานะที่กรอกแล้วออกมาได้ ให้เรียกมันครั้งเดียวหลังจากกำหนดค่าเป็นชุดเสร็จแล้ว ไม่ใช่เรียกทีละฟิลด์ เพราะการสร้าง appearance เป็นงาน layout จริง ๆ การเรียกทีละฟิลด์จะทวีคูณงานนั้นไปทั่วทั้งฟอร์มขนาดใหญ่โดยเปล่าประโยชน์
การสร้าง appearance ใหม่ยังเป็นจังหวะที่ฟอนต์และการจัดตำแหน่งแสดงตัวตนออกมาด้วย ซึ่งเป็นที่มาของความประหลาดใจขั้นที่สอง สตรีมใหม่จะจัดวางแต่ละค่าลงในสี่เหลี่ยมของวิดเจ็ตโดยใช้ฟอนต์ ขนาด และการจัดวางข้อความของฟิลด์นั้น ค่าที่พอดีสบาย ๆ ในฟอร์มทดสอบของคุณ อาจถูกตัดหรือหดตัวในสำเนาของลูกค้าที่ฟิลด์เดียวกันแคบกว่า ฟิลด์แบบปรับขนาดอัตโนมัติ (ขนาดฟอนต์เป็นศูนย์) จะหดข้อความให้พอดี ส่วนฟิลด์ขนาดตายตัวก็แค่ตัดข้อความทิ้ง ทั้งสองแบบถูกต้องตามกติกา และวิธีเดียวที่จะรู้ได้จริงว่าฟอร์มหนึ่ง ๆ ทำแบบไหน คือดูที่ผลลัพธ์ที่สร้างใหม่ ไม่ใช่ดูจากสตริงที่คุณเขียน เมื่อมีคนแจ้งว่าข้อความถูกตัดที่ขอบกล่อง สาเหตุมักจะเป็นเรื่องนี้เกือบทุกครั้ง
ให้ถือว่าการตรวจสอบเป็นส่วนหนึ่งของการทำงานให้เสร็จสมบูรณ์ ไม่ใช่สิ่งที่นึกขึ้นได้ทีหลัง เปิดไฟล์ที่บันทึกแล้วใน Acrobat และยืนยันว่าค่าต่าง ๆ มองเห็นได้ก่อนที่จะแตะฟิลด์ใด ๆ จากนั้นพิมพ์มันออกเป็น PDF หรือเป็นภาพจากตัวแสดงผลอีกตัวหนึ่งที่ไม่สนใจตรรกะของฟอร์มเลย แล้วยืนยันว่าค่าต่าง ๆ รอดผ่านเส้นทางนั้นด้วยเช่นกัน การตรวจสอบสองแบบนี้รวมกันจับความคลาดเคลื่อนระหว่าง /V กับ /AP ได้ทุกรูปแบบ
การตั้งค่าฟิลด์ที่ผ่านการสาธิตแต่พังจริงเมื่อใช้งาน
ฟอร์มสาธิตที่สะอาดหมดจดมักซ่อนกรณีขอบ (edge case) ชุดหนึ่งที่ไฟล์ของลูกค้าไม่ยอมซ่อนไว้ สี่กรณีต่อไปนี้คือต้นเหตุของรายงานแบบ "มันทำงานได้บนเครื่องผม" ส่วนใหญ่
- ค่า export ของ checkbox สถานะ "on" ไม่ได้เป็น
Yesเสมอไป ฟอร์มมีสิทธิ์กำหนดค่า export ของตัวเองได้อย่างอิสระ และการเขียนสตริงผิดจะทำให้กล่องดูเหมือนยังไม่ถูกติ๊กเลือกทั้งที่โค้ดของคุณเชื่อว่าตั้งค่าไปแล้ว ให้อ่านค่า export จากFormFieldInfo[]แทนที่จะเดาเอาเอง - กลุ่มปุ่มตัวเลือกที่ใช้ชื่อร่วมกัน ฟิลด์เดียว แต่มีวิดเจ็ตหลายตัว ค่าที่คุณกำหนดจะเป็นตัวตัดสินว่าวิดเจ็ตตัวไหนจะอ่านว่าถูกเลือก ดังนั้นโค้ด UI ที่สมมติว่าหนึ่งชื่อแมปกับหนึ่งสี่เหลี่ยม จึงลงเอยด้วยการวาดกรอบ focus ผิดปุ่ม
- ฟิลด์ที่คำนวณอัตโนมัติ ยอดรวมที่ดูแลด้วย JavaScript ของเอกสารจะอัปเดตตามเหตุการณ์ของฟิลด์ การกรอกด้วยโปรแกรมที่ข้ามเหตุการณ์เหล่านั้นไปต้องเลือกอย่างใดอย่างหนึ่ง คือสั่งให้คำนวณใหม่ หรือเขียนทับฟิลด์ที่คำนวณโดยตรง ฟอร์มที่รายการย่อยกับยอดรวมไม่ตรงกันแย่ยิ่งกว่าทางแก้ทั้งสองแบบเสียอีก
- ฟิลด์บังคับกรอกที่ถูกซ่อนไว้ ฟอร์มแบบมีเงื่อนไขจะซ่อนฟิลด์ที่ยังคงถูกตั้งค่าเป็นบังคับกรอกอยู่ ตัดสินใจล่วงหน้าว่าการตรวจสอบความถูกต้องของคุณจะยึดตามการมองเห็นได้หรือยึดตามแฟล็กบังคับกรอกดิบ แล้วบันทึกการตัดสินใจนั้นไว้ในที่ที่ทีมซัพพอร์ตหาเจอ
มีความแตกต่างจุดหนึ่งที่ควรทำความเข้าใจให้ชัดก่อนที่มันจะย้อนกลับมากัดคุณ: การสร้าง appearance ไม่ใช่การ flatten GenerateFormAppearances ทำให้ค่ามองเห็นได้ทุกที่โดยยังคงให้แก้ไขฟิลด์ได้อยู่ ส่วนการ flatten จะฝัง appearance ลงไปเป็นเนื้อหาหน้าแบบคงที่และตัดความสามารถในการโต้ตอบทิ้งไปถาวร ซึ่งเหมาะกับสำเนาสำหรับเก็บถาวร แต่ไม่เหมาะกับฟอร์มที่คนถัดไปยังต้องกรอกอยู่ ถ้า FormType รายงานว่าเป็น ftXfaFull แทนที่จะเป็น ftAcroForm พื้นที่การแก้ไขทั้งหมดที่กล่าวมานี้จะใช้ไม่ได้อย่างเรียบร้อยอยู่ดี เพราะเอกสารเรนเดอร์จากเทมเพลต XML ของตัวเอง ให้ตรวจจับกรณีนี้แล้วบอกผู้ใช้ไปตรง ๆ ดีกว่าปล่อยให้เขาไปเจอข้อจำกัดนี้เอง
ระบบย่อย form-fill การไล่ลำดับ focus และการสร้าง appearance ที่แสดงในบทความนี้เป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi, C++Builder และ Lazarus/FPC ถ้าตัวแสดงผลของคุณจัดการมาร์กอัปของผู้ตรวจทานควบคู่ไปกับข้อมูลฟอร์มด้วย บทความการตรวจทานคำอธิบายประกอบครอบคลุมโมเดลที่เกี่ยวข้องนั้น