مقال تقني

إعادة دخول OnExit في محرر حقل نموذج في مكانه في Delphi

يُلزِم عنصر التحكم TPDFlibViewer في PDF Library for Delphi محرر حقل نموذج في مكانه عبر حدث OnExit الخاص بالمحرر نفسه، ويخفي ذلك الاختيار التصميمي فخًا كلاسيكيًا في VCL الخاصة بـDelphi: إخفاء، أو إعادة تأصيل (reparent)، أو تدمير عنصر تحكم يحمل التركيز من داخل معالج OnExit الخاص به يمكن أن يُطلق OnExit مرة ثانية قبل أن يعود الاستدعاء الأول، مرسلًا منطق الإلزام إلى نفسه مرة أخرى

الفشل الذي ينتجه هذا يقاوم إعادة الإنتاج النظيفة. يتنقل مستخدم بسرعة عبر تشغيل من حقول نصية على نموذج طلب ممسوح ضوئيًا، ومن حين لآخر يُطلق العارض خرق وصول، أو أسوأ، يستمر في العمل بينما يكتب بصمت القيمة الخاطئة في حقل يبعد تبويبتين إلى الوراء. أعد إنتاجه عند الطلب ويبدو الخلل واضحًا بأثر رجعي؛ لاحقه من تقرير تعطل عميل واحد ويبدو كشبح، لأن ما إذا كانت OnExit الثانية تُطلَق فعليًا يعتمد على توقيت مقبض النافذة والتركيز الذي يتحول بحسب نوع الحقل، وسرعة الكتابة، وأيًّا كان ما تفعله قائمة انتظار الرسائل في تلك اللحظة

كيف يضع TPDFlibViewer محررًا حقيقيًا فوق صفحة مرسومة

يرسم TPDFlibViewer كل صفحة PDF إلى صورة نقطية ولا يحوّل حقول النموذج إلى عناصر تحكم VCL حية افتراضيًا، بحيث تكون BeginEditFormField هي الطريقة التي تربط بين العالمين: عند استدعائها بفهرس حقل، تبحث عن مستطيل الحقل وتحوّله إلى إحداثيات العميل، ثم تضع TEdit أو TMemo حقيقيًا فوق ذلك المستطيل لحقل نصي، أو TComboBox بنمط csDropDownList لحقل اختيار، كاملًا بقيمة الحقل الحالية محمَّلة بالفعل. يعرّف ISO 32000-2 §12.7 ما هو حقل نموذج نصي أو اختياري داخل PDF، لكن لا شيء في ذلك المعيار يقول كيف يجب لتطبيق ويندوز أن يسمح لأحد بالكتابة في واحد، وتلك بالضبط الفجوة التي وُجدت BeginEditFormField لسدّها. تُربَط كل من OnKeyDown وOnExit بطريقتي العارض نفسيهما، InplaceEditorKeyDown وInplaceEditorExit، على كل محرر يُنشئه TPDFlibViewer، اقتران شُحن دون تغيير منذ أن هبط ملء النماذج التفاعلي لأول مرة في v3.220.0، وOnExit هي حيث تبدأ المشكلة

مخطط لـ TPDFlibViewer يضع محرر VCL نشطًا فوق صورة صفحة PDF المصيَّرة ويربط OnKeyDown و OnExit بمعالجات العارض
يضع BeginEditFormField عنصر تحكم حقيقي مركزّاً فوق الصفحة، وكل محرر من هذا النوع يأتي محمّلاً بـ OnExit — الباب الذي يدخل منه عَود الدخول

لماذا يُطلق إخفاء المحرر OnExit مرة ثانية؟

تعامل TWinControl في VCL تغييرًا في Visible أو Parent على عنصر تحكم يحمل التركيز كسبب لنقل التركيز بعيدًا عنه فورًا، ونقل التركيز بعيدًا عن عنصر تحكم هو بالضبط ما يُطلق حدث OnExit الخاص بذلك العنصر، بشكل متزامن، قبل أن يعود حتى تعيين الخاصية الذي أطلقه. تحتاج CommitInplaceEditor، الطريقة التي يستخدمها PDF Library for Delphi لإغلاق المحرر في مكانه وكتابة قيمته مرة أخرى في حقل النموذج، فعل هذين الشيئين بالضبط في طريقها للخارج: ضبط Editor.Visible على False وضبط Editor.Parent على nil بحيث يتوقف عنصر التحكم عن الرسم فوق الصفحة ويتوقف عن استقبال المدخلات. افعل أيًّا من ذلك بينما لا يزال المحرر يحمل التركيز، وهو ما يحدث دائمًا تقريبًا بما أن المستخدم غادره للتو، وتُطلَق OnExit مرة أخرى في منتصف الاستدعاء نفسه الذي كان يُفترض أن يكون آخر شيء تُطلقه OnExit الخاصة بذلك المحرر على الإطلاق

ما الذي ينكسر عندما تعيد CommitInplaceEditor الدخول إلى نفسها؟

تدفع طريقة إلزام ساذجة ثمن هذا بإحدى طريقتين. إما أن تكتب قيمة الحقل مرتين، مرة من الاستدعاء الأصلي ومرة من الاستدعاء المُعاد الدخول الذي تسلل قبل أن ينهي الأول لمس حالته الخاصة، أو تحاول تحرير عنصر تحكم المحرر بينما لا يزال إطار أبعد أسفل مكدس الاستدعاء داخل معالج حدث عنصر التحكم نفسه، وهي منطقة غير معرَّفة في VCL وتظهر كخرق وصول يمكن أن يشير إلى أي سطر تقريبًا، ليس بالضرورة السطر الذي تسببه فعليًا. لا يحتاج أي من الفشلين نموذجًا كبيرًا ليُفعَّل؛ مستند بحقلين كافٍ، شريطة أن يغادر المستخدم الحقل الثاني بسرعة كافية بحيث لا يزال نظام التشغيل يفكك رسائل التركيز من الأول

// النسخة الساذجة: تبدو سليمة عند المراجعة، تفشل فقط عند سرعة كتابة حقيقية
procedure TMyPdfViewer.EditorExit(Sender: TObject);
begin
  CommitEditor;               // ما زالت تعمل داخل OnExit الخاصة بـ FEditor نفسها
end;

procedure TMyPdfViewer.CommitEditor;
begin
  if not Assigned(FEditor) then
    Exit;
  SaveFieldValue(FEditor.Text);
  FEditor.Parent := nil;      // عنصر التحكم المُركَّز يُعاد أبوّته هنا: OnExit
                               // تُطلَق مرة أخرى، مُعيدة الدخول إلى هذه الطريقة نفسها
  FEditor.Free;                // يُحرَّر بينما مستدعٍ أسفل
  FEditor := nil;              // المكدس ما زال داخل معالج OnExit الخاص به
end;

صفّر المرجع قبل لمس عنصر التحكم

الإصلاح الذي يشحنه PDF Library for Delphi إعادة ترتيب واحدة: التقط المحرر في متغير محلي، وامسح الحقل الذي يشير إليه، ولا تبدأ إلا حينها في تغيير خصائص عنصر التحكم. تقرأ CommitInplaceEditor قيمة FInplaceEditor إلى متغير محلي Editor، وتضبط FInplaceEditor على nil فورًا، ولا تعيّن Editor.Visible وEditor.Parent إلا بعد ذلك. استدعاء مُعاد الدخول تُفعّله أي من هذين التعيينين يقرأ FInplaceEditor نفسها، ويجدها بالفعل nil، ويخرج عند سطره الأول بالضبط، قبل أن يتمكن من لمس Editor أو كتابة قيمة الحقل مرة ثانية

procedure TPDFlibViewer.CommitInplaceEditor(Save: Boolean);
var
  Editor: TWinControl;
begin
  Editor := FInplaceEditor;
  if not Assigned(Editor) then
    Exit;                      // استدعاء مُعاد الدخول يصل إلى هنا ويتوقف
  FInplaceEditor := nil;       // افصل قبل مسّ عنصر التحكم على الإطلاق
  if Save then
    SaveEditorValue(Editor);   // آمن: FInplaceEditor بالفعل nil
  Editor.Visible := False;
  Editor.Parent := nil;        // قد تُطلَق OnExit مرة أخرى؛ الحارس أعلاه
                                // يحوّل ذلك الاستدعاء المُعاد الدخول إلى عملية بلا أثر
  ReapDeadEditor;               // حرّر أيًّا كان ما أُوقف في الدورة الأخيرة
  FDeadEditor := Editor;        // أوقف هذا بدلًا من تحريره هنا
end;

SaveEditorValue في تلك القائمة تقف بدلًا من الفرع الحقيقي، الذي يتحقق مما إذا كان Editor هو TComboBox، أو TMemo، أو TEdit ويقرأ قيمته وفقًا لذلك، بما أن PDF Library for Delphi ينشئ عنصر تحكم مختلفًا بحسب ما إذا كان الحقل حقلًا نصيًا أو حقل اختيار. لا يهتم الحارس بأي فرع يعمل، فقط بأن FInplaceEditor تكون nil قبل تنفيذ أي شيء قادر على تحفيز OnExit، وهذا هو قيد الترتيب الواحد الذي يجعل بقية الطريقة آمنة للكتابة بأي أسلوب طبيعي من ناحية أخرى

لا تحرر عنصر تحكم أبدًا من داخل حدثه الخاص

لا تستدعي TPDFlibViewer.CommitInplaceEditor أبدًا Editor.Free مباشرة، وذلك متعمد: تحرير عنصر تحكم غير آمن بينما إطار مكدس ينتمي إلى توزيع حدث عنصر التحكم نفسه قد لا يزال يتفكك فوق الاستدعاء الذي يحرره، سواء كانت OnExit مُعادة الدخول أم لا. يسلّم PDF Library for Delphi بدلًا من ذلك المحرر المفصول إلى نقطة انتظار فتحة واحدة، FDeadEditor، محررًا أيًّا كان ما كان جالسًا هناك من دورة التحرير السابقة عبر مساعد صغير، ReapDeadEditor، يُستدعى في بداية BeginEditFormField التالية ومرة أخرى من CloseDocument؛ كل محرر ينشئه العارض مملوك أيضًا للعارض نفسه، TEdit.Create(Self) بدلًا من TEdit.Create(nil)، بحيث حتى عنصر تحكم لا يزال متوقفًا في FDeadEditor عند تدمير العارض يُجمَع بواسطة ملكية مكوّن VCL العادية بدلًا من التسريب

procedure TPDFlibViewer.ReapDeadEditor;
begin
  if Assigned(FDeadEditor) then
  begin
    FDeadEditor.Free;          // آمن الآن: OnExit الخاصة بعنصر التحكم هذا
    FDeadEditor := nil;        // أنهت دورة تحرير واحدة على الأقل من قبل
  end;
end;

function TPDFlibViewer.BeginEditFormField(FieldIndex: Integer): Integer;
var
  Edit: TEdit;
begin
  Result := 0;
  CommitInplaceEditor(True);   // ألزم أي محرر ما زال مفتوحًا
  // ... حُذف بحث الحقل وتحويل المستطيل ...
  ReapDeadEditor;              // آمن الآن لتحرير محرر الدورة الأخيرة المُوقف
  Edit := TEdit.Create(Self);
  Edit.Parent := Self;
  Edit.OnExit := InplaceEditorExit;
  FInplaceEditor := Edit;
  FInplaceEditor.SetFocus;
  Result := 1;
end;

لماذا يظهر هذا بأقصى قوته أثناء تنقل Tab السريع؟

تستدعي FocusNextFormField، الطريقة التي أضافها PDF Library for Delphi في v3.226.0 لقيادة تنقل Tab وShift+Tab عبر نموذج، BeginEditFormField للحقل التالي المؤهل عند كل قفزة واحدة، وتفتتح BeginEditFormField باستدعاء CommitInplaceEditor(True) لتفريغ أيًّا كان المحرر الذي تركه الحقل السابق مفتوحًا. يعني ذلك أن كل ضغطة Tab يجريها مستخدم أثناء ملء نموذج متعدد الحقول تُشغِّل التسلسل الدقيق فصل-ثم-لمس الموصوف أعلاه مرة واحدة، وهو بالضبط مسار الشيفرة الأكثر احتمالًا لأن يظل عنصر تحكم يحمل تركيزًا فعليًا في لحظة تغيّر Visible وParent، لأن Tab هو التفاعل الوحيد شبه المضمون لترك المحرر الخارج يحمل التركيز حتى يطلبه الجديد

مخطط انسيابي من PDF Library for Delphi يوضح كيف يجعل ضبط Editor.Parent إلى nil في محرر يمتلك التركيز OnExit يشتعل مرة أخرى، مُعيدًا الدخول إلى CommitInplaceEditor قبل أن يعود الاستدعاء الأول
تُرسل عملية إسناد واحدة لتغيير الأب عملية الإرسال إلى داخلها، فتفتح نمطَي فشل الكتابة المزدوجة والتحرير المبكر عبر Free

لا شيء من هذا يجعل الخلل موثوقًا للتوضيح، وذلك يستحق قوله بوضوح بدلًا من التغاضي عنه. ما إذا كان تعيين Visible أو Parent معين يفرض فعليًا OnExit متزامنة يعتمد على حالة التركيز ومقبض النافذة التي يغيّرها مُصحح أخطاء لمجرد إرفاقه، ويمكن لإعادة رسم غير مرتبطة أو مؤقت أن يزعجها، وتتصرف بشكل مختلف بحسب أي من TEdit، أو TMemo، أو TComboBox يصادف أن يكون عنصر التحكم قيد اللعب. حارس لا يُمارَس إلا أحيانًا هو سبب نجاة هذا النوع من العيوب من مراجعة الشيفرة والاختبار اليدوي على حد سواء، وهو أيضًا سبب وجوب أن يكون الإصلاح صحيحًا بالبنية، بتصفير المرجع قبل حدوث أي شيء آخر، بدلًا من صحيح بأيًّا كان السلوك الذي صادفت حفنة من تمريرات الاختبار اليدوي ملاحظته

يمتد الشكل العام لهذا الإصلاح جيدًا إلى ما هو أبعد من عنصر تحكم عارض واحد. أي سطح تحرير مخصص مبني بتراكب عنصر تحكم VCL حي فوق محتوى مرسوم، لا حقل نموذج PDF فقط، يرث الخطر نفسه بمجرد أن يصبح منطق إغلاقه-وإلزامه قابلًا للتحفيز بكل من فعل مستخدم صريح وتغيير تركيز ضمني، وتنطبق الإجابة ذات الجزأين نفسها: امسح المرجع الذي يحدد عنصر التحكم النشط قبل فعل أي شيء قد يحفز حدث خروجه الخاص، ولا تستدعِ Free أبدًا من مسار شيفرة قد لا يزال يعمل أسفل توزيع الحدث الخاص بعنصر التحكم ذاك. سطح ملء النماذج والرسم الأوسع الخاص بـTPDFlibViewer، بما في ذلك كيفية تقريره أي نوع عنصر تحكم ليُظهر لأي حقل، مغطى في نظرة عامة على بناء عنصر تحكم عارض PDF تفاعلي في Delphi VCL عبر PDF Library for Delphi، وذاكرة التخزين المؤقت لصورة الصفحة النقطية التي يجب على SetFormFieldValueAndRefresh إبطالها عند كل تعديل مُلزَم مغطاة بشكل منفصل في المقالة حول ذاكرة تخزين مؤقت للصفحة على القرص لكل شاشة بدقة DPI الخاصة بها في العارض

PDF Library for Delphi: تسلسل إتمام من ست خطوات مرتبة يمسح FInplaceEditor قبل لمس عنصر التحكم، ويعامل OnExit المعاد دخوله كعملية بلا أثر، ويؤجل Free عبر خانة FDeadEditor
تجعل إبطال المرجع أولاً كل استدعاء معاد للدخول يخرج فوراً، وينقل إيقاف المحرر جانباً كل Free إلى دورة سلاسل استدعائها هادئة

تحرير حقل نموذج في مكانه، والتنقل بين الحقول بواسطة Tab، ومسار الإلزام الآمن من إعادة الدخول خلف كليهما جزء من عنصر تحكم العارض التفاعلي المشحون مع PDF Library for Delphi، مكتبة PDF لـDelphi وC++Builder، إلى جانب بقية سطح رسم الصفحات، والتعليقات التوضيحية، وواجهة حقول النماذج البرمجية