Technical Article

إعادة استخدام مثيل THotPDF عبر عدة مستندات في Delphi

يظهر الخطأ بالنص Please load the document before using BeginDoc، وغالبًا ما يظهر عند المحاولة الثانية. يكتب المستند الأول بنجاح، ثم يُطلب من نفس مثيل THotPDF أن يبدأ مستندًا ثانيًا، فيرفع BeginDoc استثناءً، وتوجّه الرسالة إلى تحميل مستند، وهو عكس ما يحاول الكود فعله. هذا التباين بين العرض والرسالة هو ما يجعل المشكلة لافتة. أما الموضوع الحقيقي فهو دورة حياة المكون، وبمجرد أن تتضح تزول غرابة الخطأ

دورة حياة مستند THotPDF التي توضح Create وBeginDoc وEndDoc وFree لكل ملف مخرجات
مثيل واحد من THotPDF يقابل مستندًا واحدًا: Create وBeginDoc ثم الرسم ثم EndDoc ثم Free

مثيل THotPDF واحد يساوي مستندًا واحدًا، وليس مصنعًا للمستندات

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

إذًا الرسالة مضللة، لكن الشرط الحارس يؤدي عمله. فهو يرفض السماح لك ببدء مستند جديد فوق مكون ما زال يعتقد أنه في منتصف مستند. الحل ليس كسر هذا الحارس، بل التوقف عن إعادة استخدام مثيل مستهلك

دورة الحياة بالترتيب الذي يجب أن تحدث به

كل مستند يكتبه HotPDF من الصفر يمر بالإيقاع نفسه، والترتيب هنا غير قابل للتفاوض. ينشئ Create المكون. يفتح BeginDoc المستند ويثبت القرارات البنائية، لذلك يجب ضبط كل ما يؤثر في الملف كله، مثل حجم الصفحة والضغط والتشفير واسم ملف الخرج، بين Create وBeginDoc. ثم تبدأ في الرسم. ثم يكتب EndDoc البايتات إلى القرص. وأخيرًا يحرر Free المثيل. أما استدعاءات الرسم الموضوعة قبل BeginDoc فلا تملك صفحة تستقر عليها، وخصائص المستند بأكمله التي تُعيَّن بعده تُتجاهل من دون اعتراض

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // opens the document
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // writes invoice.pdf, closes it out
  finally
    Pdf.Free;                            // one instance, one document
  end;
end;

اعتبر هذا وحدة العمل. Create واحد، وBeginDoc واحد، وEndDoc واحد، وFree واحد، وملف واحد على القرص. في اللحظة التي تحتاج فيها إلى ملف ثانٍ، فأنت تبدأ وحدة عمل جديدة، وهذا يعني مثيلًا جديدًا

ما الذي يجب أن تعنيه "إعادة الاستخدام": مثيل جديد لكل ملف

النسخة التي تفشل تحاول الاقتصاد في التخصيص: تنشئ المكون مرة واحدة، ثم تدور على دفعة من الملفات وتستدعي BeginDoc وEndDoc داخل الحلقة. عند التكرار الثاني يظهر الاستثناء. أما النسخة التي تعمل فتتعامل مع كل مخرج ككائن قصير العمر مستقل، وتكلفة إنشاء المكون تافهة مقارنة بعمل تنسيق ملف PDF وتسلسله، لذلك لا يوجد ما يمكن توفيره من خلال الاحتفاظ بالمثيل

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // new instance each pass
    try
      Pdf.FileName := Names[I] + '.pdf';
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('Arial', [], 12);
      Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
      Pdf.EndDoc;
    finally
      Pdf.Free;
    end;
  end;
end;

الجزء الجدير بالدفاع عنه في المراجعة هو وجود try/finally داخل الحلقة. فإذا رمى BeginDoc أو أي استدعاء للرسم استثناءً في منتصف مستند ما، فإن مثيل تلك الدورة يحرر قبل بدء الدورة التالية، فلا يعلق سجل سيئ واحد مكونًا نصف مبني ولا يفسد بقية التشغيل. أما إذا رفعت Create إلى أعلى الحلقة بحجة "التحسين"، فستعود إلى الخطأ الأصلي لكن داخل حلقة دفعات

تعديل ملف موجود هو نقطة دخول مختلفة

هناك معنى ثانٍ لـ"إعادة الاستخدام" وهو مشروع تمامًا: أنت لا تريد مستندًا فارغًا، بل تريد فتح ملف PDF موجود وتعديله. هذا المسار لا يمر عبر BeginDoc أصلًا، ولهذا السبب بالضبط تشير الرسالة إلى التحميل. أنت تحمّل الملف، تعدله، ثم تحفظه بالاسم الذي تختاره

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('contract.pdf');
    if PageCount > 0 then
    begin
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
      Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
      Pdf.SaveLoadedDocument('contract-reviewed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

LoadFromFile يعيد عدد الصفحات، والقيمة صفر أو أقل تعني أن التحميل فشل، لذا من المفيد التحقق منها قبل لمس CurrentPage. والاقتران هنا مهم: المستند الذي فتحته بـ LoadFromFile تحفظه بـ SaveLoadedDocument، لا بزوج BeginDoc وEndDoc الذي يخص المستندات التي تؤلفها من الصفر. خلط المسارين هو أكثر الطرق شيوعًا لتشويش آلة الحالة نفسها التي أنتجت الخطأ الأصلي. افصل المسارين ذهنيًا: BeginDoc ... EndDoc ينشئ، وLoadFromFile ... SaveLoadedDocument يعدل

مشكلة قفل الملفات حقيقية، والحل ليس إغلاق نوافذ برامج العرض

غالبًا ما يرافق خطأ إعادة الاستخدام شكوى ثانية، ويتشابك الاثنان لأنهما يظهرا في سير العمل نفسه الخاص بإعادة توليد الملف. يفتح المستخدم ملف الـ PDF الذي أنشأته للتو، ويتركه مفتوحًا في Acrobat أو Foxit، ثم يطلق إعادة البناء. يحاول EndDoc الكتابة إلى المسار نفسه، فيرفض نظام التشغيل لأن العارض يحتفظ بمشاركة قراءة تمنع الكتابة، فتظهر لك محاولة فاشلة بسبب رفض الوصول. هذه مشكلة قفل ملفات في Windows فعلًا، وليست مشكلة حالة مكون، وتستحق حلًا حقيقيًا بدل حيلة ملتوية

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

في الخوادم عالية الحجم التي تعيد توليد المستندات باستمرار، تكون الممارسة الأنظف هي كتابة كل مخرج باسم فريد، مثل طابع زمني أو معرف مهمة، حتى لا تتنافس عمليتان على المسار نفسه، ثم ترك سياسة احتفاظ منفصلة تتولى تنظيف الملفات القديمة. وفي كلتا الحالتين يبقى المبدأ واحدًا: صمم النظام بحيث يكون الملف الذي تكتبه ملكك وحدك في لحظة الكتابة. يختفي القفل لا لأنك أغلقت نافذة بالقوة، بل لأن لا شيء آخر يلمس البايتات

شكل الإصلاح

إذا أعدت المشكلتين إلى جذورهما، ستجد أنهما في النهاية عن احترام الحدود. خطأ آلة الحالة يريد منك احترام حد المثيل: THotPDF واحد، مستند واحد، ثم تتركه وتبدأ آخر. أما خطأ قفل الملف فيريد منك احترام حد الملف: اكتب في مكان لا يقرأ منه أحد، ثم انقل النتيجة إلى موضعها. ولا واحدة من هاتين المشكلتين تتطلب ترقيع المكتبة أو كتابة سكريبتات لسطح المكتب. كلتاهما تنتج من التعامل مع كل مستند كوحدة عمل مستقلة، تُنشأ طازجة، وتُكتب نظيفًا، ثم تُحرر، وهو النمط نفسه الذي يجعل سلوك بقية المكون قابلاً للتنبؤ

الاستدعاءات BeginDoc وEndDoc وLoadFromFile وSaveLoadedDocument المعروضة هنا هي جزء من HotPDF Component لـ Delphi وC++Builder