مقال تقني

التنقل بين حقول نماذج PDF في Delphi (PDFium Component)

يصل تقرير الخطأ ومعه لقطة شاشة: "أداتكم ملأت النموذج، لكن كل الحقول في Acrobat فارغة. عندما أنقر داخل حقل تظهر القيمة فجأة". البيانات موجودة في الملف؛ بل إن لقطة الشاشة تثبت ذلك، ومع ذلك يبدو النموذج فارغاً لكل من يستلمه. هذا هو العيب الأكثر شيوعاً في العمل البرمجي مع نماذج PDF، وليس خطأ في أي مكتبة: إنه ما يحدث عندما تُكتب قيم الحقول من دون إعادة توليد مظاهر الحقول. فهم السبب يحتاج إلى قسم واحد من مواصفة PDF؛ وإصلاحه يحتاج إلى استدعاء واحد. تستخدم الأمثلة أدناه PDFium Component، وهو مكوّن VCL/LCL مبني على PDFium لـ Delphi وC++Builder وLazarus، لكن آليات صيغة الملف نفسها تنطبق على أي أدوات AcroForm.

حقل واحد وتمثيلان: /V و/AP

يخزن حقل نص AcroForm قيمته في خانة /V من قاموس الحقل (ISO 32000-1 §12.7.3.3). أما ما ترسمه العوارض فعلياً فهو appearance stream الخاص بالـ widget، وهو content stream صغير مسبق التصيير مخزن تحت /AP (§12.5.5). اكتب /V من دون إعادة بناء /AP وسينفصلان: البيانات موجودة، لكن صورة البيانات غير موجودة. يعيد Acrobat رسم مظهر الحقل عندما يكتسب الحقل focus، وهذا بالضبط سبب أن النقر داخل الحقل "يكشف" القيمة في تقرير الخطأ أعلاه.

المخرج التاريخي، وهو علم NeedAppearances الذي يطلب من العوارض إعادة توليد المظاهر بنفسها، لم يكن محترماً بثبات بين العوارض، وهو deprecated في PDF 2.0 (ISO 32000-2). كما أن مسارات الطباعة ومولّدات thumbnails لم تحترمه أصلاً؛ فهي ترسم ما يحتويه /AP، أي لا شيء. لذلك يكون العقد الموثوق: من يكتب القيمة يعيد أيضاً بناء المظهر.

توليد المظهر هو أيضاً الموضع الذي تظهر فيه الخطوط والمحاذاة. يضع stream المعاد توليده القيمة داخل مستطيل widget باستخدام خط الحقل وحجمه وquadding، ولهذا قد تقص قيمة تناسب نموذج الاختبار لديك أو تنكمش في نسخة عميل أضيق من الحقل نفسه. الحقول ذات الحجم التلقائي، حيث حجم الخط صفر، تصغّر النص ليلائم المساحة؛ والحقول ذات الحجم الثابت تقصه. كلاهما نتيجة قانونية، والطريقة الوحيدة لمعرفة ما ينتجه نموذج معين هي فحص الإخراج المعاد توليده بدلاً من القيمة التي كتبتها؛ عندما يبلغ عميل أن النص مقطوع، فهذه الفقرة غالباً هي التفسير الكامل.

فتح نموذج: FormFill وFormType وسؤال XFA

الوصول إلى الحقول يتطلب تمكين نظام form-fill الفرعي، المتحكم به عبر الخاصية FormFill، قبل فتح المستند. بعد تفعيله، يخبرك FormType بنوع النموذج الذي تواجهه، وتغيّر الإجابة مجموعة الميزات التي يمكنك الوعد بها:

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

ملاحظتان عمليتان. AcroForm هو نموذج النماذج القياسي في ISO 32000، وهو الهدف لكل API في هذا المقال؛ أما مستندات XFA فتضم معمارية نماذج XML خاصة بها، ووعد العملاء بتحرير XFA كامل بناءً على عرض AcroForm سريع التزام ستندم عليه. ثانياً، يقوم FormFill أيضاً بتهيئة JavaScript الخاص بالمستند؛ وهذا ما تريده في عارض إدخال بيانات حيث تبقي calculation scripts الإجماليات محدثة، وما لا تريده صراحة في معاينة ملف غير موثوق. يغطي مقال معاينة PDF الآمنة جانب FormFill := False من هذه الموازنة.

تنقل بلوحة المفاتيح يستطيع المستخدم توقعه

مستخدمو إدخال البيانات يعيشون على مفتاح Tab، لذلك يجب أن يتصرف التنقل بين الحقول مثل كل نموذج آخر يستخدمونه. عائلة focus API، وهي FocusFormField وFocusNextFormField وFocusPreviousFormField وFocusedFormFieldIndex وClearFormFieldFocus، تنقل focus النموذج من دون محاكاة إدخال الفأرة:

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // e.g. "Field 4 of 17: InvoiceDate"
end;

اعرف سلوك الحدود: تعمل استدعاءات التنقل ضمن ترتيب Tab للصفحة الحالية وتلتف حوله؛ التقدم بعد آخر حقل يعود إلى الأول، وتعيد الدالتان فهرس الحقل الجديد أو -1 عندما لا تحتوي الصفحة على حقول. الانتقال إلى الصفحة التالية قرارك أنت: اكشف الالتفاف بمقارنة الفهارس، ثم تقدم في PageNumber بنفسك إذا كان الهدف تنقلاً على مستوى المستند كله. اربط التنقل بحدث OnFormFieldEnter، وعلى العارض بحدث OnFormFieldFocusChange، لإبقاء لوحة جانبية متزامنة مع المستند، واستخدم الخاصية المفهرسة FormFieldAt عندما تحتاج إلى hit-testing: تحويل موضع الفأرة إلى قيمة حقل من أجل tooltip previews أو click-to-edit panels. يستفيد مستخدمو قارئات الشاشة من التنقل بلا تكلفة إضافية: يتحرك focus عبر ترتيب الحقول الخاص بالمستند، لذلك فإن المسار الذي تبنيه لمفتاح Tab هو أيضاً المسار الذي تتبعه assistive technology.

بالنسبة إلى واجهات metadata-driven، تعيد الخاصية FormFieldInfo[] سجل TPdfFormFieldInfo لكل فهرس، وهي الطريقة التي تسمّي بها الحقول في قائمة تنقل بدلاً من عرض أرقام فهارس عارية. تستحق radio groups ملف regression خاصاً بها هنا: عدة widgets تشترك في اسم حقل واحد، لذلك فإن قائمة مبنية بسذاجة من widgets تعرض تكرارات ظاهرية تربك المستخدمين.

تسلسل التعبئة والحفظ الذي ينجو في Acrobat

كل ما سبق يتقارب إلى تسلسل من ثلاث خطوات، والخطوة الوسطى هي التي تتخطاها الفرق:

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];   // writes /V only

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances هو الإصلاح الكامل لتقرير الخطأ الافتتاحي. يعيد بناء appearance streams الخاصة بالـ widgets من القيم والخطوط وquadding الحالية، بحيث يرسم كل عارض، بما في ذلك العوارض التي لا تنفذ focus events إطلاقاً مثل print servers وthumbnailers، حالة النموذج المعبأ. استدعِه مرة واحدة بعد دفعة الإسنادات بدلاً من كل حقل؛ توليد المظاهر يلمس الخطوط والتخطيط، واستدعاؤه لكل حقل يضاعف تلك الكلفة على النماذج الكبيرة بلا فائدة.

ينتمي التحقق إلى definition of done: افتح الملف المحفوظ في Acrobat وتأكد من أن القيم مرئية من دون النقر على أي حقل، ثم اطبع إلى PDF أو صورة من عارض ثان وتأكد من أن القيم تنجو في مسار يتجاهل منطق النماذج بالكامل. يلتقط هذان الفحصان معاً كل اختلاف من اختلافات /V مقابل /AP.

نماذج إنتاج تكسر التطبيقات النظيفة

قائمة قصيرة من إعدادات الحقول التي تنجح في اختبارات demo وتفشل مع ملفات العملاء:

  • قيم تصدير checkboxes. حالة التشغيل ليست دائماً Yes؛ فالنماذج تعرّف قيماً عشوائية للتصدير، وكتابة القيمة الخطأ تترك المربع غير محدد بصرياً بينما يظن الكود أنه نجح.
  • Radio groups مشتركة الاسم. حقل واحد وعدة widgets. إسناد القيمة يحدد أي widget يظهر محدداً، وكود UI لكل widget يفترض one-name-one-rectangle يرسم حلقة focus في المكان الخطأ.
  • حقول محسوبة. الإجماليات التي يحسبها JavaScript المستند تتحدث عند أحداث الحقول. التعبئة البرمجية التي تتجاوز الأحداث يجب أن تطلق إعادة الحساب أو تكتب الحقول المحسوبة صراحة؛ شحن نموذج تختلف فيه البنود عن الإجمالي أسوأ من الخيارين.
  • حقول مطلوبة مخفية. النماذج الشرطية تخفي حقولاً تظل موسومة كمطلوبة. قرر هل يحترم validation لديك الرؤية أم العلم الخام، ووثّق القرار في مكان يستطيع الدعم العثور عليه.

FAQ

لماذا لا تظهر القيم المعبأة إلا عند النقر على الحقل؟

كُتبت القيم إلى /V، لكن appearance streams في /AP لم تُعد توليدها، لذلك ترسم العوارض المظهر القديم الفارغ حتى يجبر focus event إعادة بناء. استدعِ GenerateFormAppearances بعد إسناد القيم وقبل SaveAs.

هل يعمل التنقل بين الحقول في نماذج XFA؟

افحص FormType أولاً. يمنحك ftAcroForm سطح التنقل والتحرير الكامل الموضح هنا؛ أما ftXfaFull فيعني أن المستند يرسم من قالب XML خاص به وأن التفاعل على مستوى الحقول محدود. اكشف ذلك واعرض رسالة بدلاً من ترك المستخدمين يكتشفونه.

هل flattening هو نفسه توليد المظاهر؟

لا. يحافظ GenerateFormAppearances على الحقول تفاعلية بينما يجعل قيمها مرئية في كل مكان. أما flattening فيحوّل المظهر إلى محتوى صفحة ثابت ويزيل التفاعل نهائياً؛ هذا مناسب للإخراج الأرشيفي، وخاطئ لنموذج يجب أن يحرره الشخص التالي.

نظام form-fill الفرعي، وتنقل focus، وتوليد المظاهر المعروضة هنا جزء من PDFium Component لـ Delphi وC++Builder وLazarus/FPC. إذا كان عارضك يتعامل أيضاً مع reviewer markup بجانب بيانات النماذج، فإن مقال مراجعة annotations يغطي ذلك النموذج المجاور.