فئة THPDFBackgroundRenderer في HotPDF هي سليل لـTThread يرسم صفحات PDF المحمَّلة إلى صور نقطية على خيط عامل، بحيث يمكن لعارض Delphi الاستمرار في التمرير وإعادة الرسم بينما لا تزال صفحة ما تُرقَّم في الخلفية. تضع THPDFBackgroundRenderer.RequestPage رقم صفحة في قائمة انتظار ذلك الخيط العامل، وتُسقط CancelAll أي شيء لا يزال منتظرًا، وتُعيد GetCachedBitmap صورة نقطية جاهزة يملكها المستدعي ويجب عليه تحريرها. مرّر عقدًا ممسوحًا ضوئيًا من مائتي صفحة بدقة طباعة على خيط الواجهة وحده وستتوقف النافذة عند كل قلب صفحة حتى ينتهي GDI من رسمها، وهذا بالضبط التقطّع الذي وُجدت THPDFBackgroundRenderer لإزالته
لماذا نرسم صفحات PDF على خيط في الخلفية أصلًا؟
يستحق الخيط الخلفي تعقيده لأن عارض صفحات HotPDF مفسّر تدفق محتوى حقيقي، لا نسخ صورة نقطية رخيص يعود قبل أن يلاحظ أحد ذلك: فهو يجتاز عوامل تشغيل PDF، ويحتفظ بمكدس حالة رسومية، ويُرقّم المسارات والصور والحروف عبر GDI، وهو المحرك نفسه الموصوف في رسم صفحات PDF المحمَّلة إلى TBitmap. تشغيل ذلك العمل بشكل متزامن داخل معالج تمرير أو رسم يوقف حلقة الرسائل عن الضخ حتى يعود الاستدعاء، وهذا هو بالضبط ما تكونه النافذة المجمَّدة. إسقاط Application.ProcessMessages داخل استدعاء الرسم لا يصلح هذا: فهو يسمح بتفريغ قائمة انتظار الرسائل، لكن الرسم نفسه لا يزال يمتلك الخيط المستدعي، بحيث تعيد النافذة رسم محتوى قديم بسرعة أكبر بينما لم يتقدم العمل الفعلي في أي مكان. الطريقة الوحيدة للحفاظ على استجابة عارض أثناء رسم بطيء فعليًا هي تشغيل ذلك الرسم في مكان آخر، ولهذا وُجدت THPDFBackgroundRenderer كفئة فرعية من TThread بدلًا من دالة رد نداء أو مؤقت
إعداد قائمة انتظار طلبات لعارض قابل للتمرير
يأخذ THPDFBackgroundRenderer.Create مثيل THotPDF المحمَّل ودقة نقاط في البوصة تبقى ثابتة طوال عمر ذلك الراسم بالكامل، بحيث تُرسم كل صفحة موضوعة في قائمة انتظار عبر مثيل واحد بدقة واحدة؛ عارض يدعم التكبير يحتاج راسمًا جديدًا، لا خاصية دقة جديدة، كلما تغيّر مستوى التكبير. تُلحق RequestPage رقم صفحة بقائمة انتظار داخلية وتعود فورًا: فهي لا تقوم بأي رسم بنفسها ولا تلمس خيط الواجهة أبدًا. تسحب Execute، وهي نقطة الدخول الموروثة من TThread التي يشغّلها HotPDF بمجرد استدعاء Start، رقمًا واحدًا في كل مرة من مقدمة تلك القائمة، وترسمه عبر ذاكرة التخزين المؤقت لصفحات المستند، وتخزّن نسخة مفهرسة بالصفحة بحيث يمكن لـGetCachedBitmap إعادتها لاحقًا
type
TViewerForm = class(TForm)
RenderPollTimer: TTimer;
procedure RenderPollTimerTimer(Sender: TObject);
private
FDoc: THotPDF;
FRenderer: THPDFBackgroundRenderer;
FPendingPage: Integer;
procedure RequestPageWindow(CenterPage: Integer);
end;
procedure TViewerForm.RequestPageWindow(CenterPage: Integer);
var
I: Integer;
begin
if FRenderer <> nil then
begin
FRenderer.CancelAll;
FRenderer.Free;
end;
FRenderer := THPDFBackgroundRenderer.Create(FDoc, 150);
for I := CenterPage - 1 to CenterPage + 1 do
if (I >= 0) and (I < FDoc.LoadedPageCount) then
FRenderer.RequestPage(I);
FPendingPage := CenterPage;
FRenderer.Start;
end;
procedure TViewerForm.RenderPollTimerTimer(Sender: TObject);
var
Bmp: TBitmap;
begin
if FRenderer = nil then Exit;
Bmp := FRenderer.GetCachedBitmap(FPendingPage);
if Bmp <> nil then
begin
PageImage.Picture.Bitmap.Assign(Bmp);
Bmp.Free;
end;
end;
تُعيد GetCachedBitmap القيمة nil حتى تصبح نسخة تلك الصفحة جاهزة، لذا فإن نمط الاستقصاء بمؤقت كالمثال أعلاه كافٍ؛ ولا يوجد حدث جاهزية منفصل يجب ربطه، إذ يحل HotPDF هذا بفحص nil بسيط بدلًا من واجهة إشعارات أكبر. يغطي القسم التالي ما تفعله CancelAll فعليًا واستدعاء Free ذلك، لأن كليهما يهم بمجرد أن تبدأ الصفحات بالرسم خارج الترتيب أو يحدث تمرير أسرع مما يمكن لقائمة الانتظار تفريغه
الاختصار أحادي الاستدعاء لصفحة واحدة
توجد THotPDF.RenderLoadedPageToBitmapAsync للحالة الشائعة المتمثلة في إطلاق صفحة واحدة بالضبط دون لمس THPDFBackgroundRenderer مباشرة: فهي تُنشئ الراسم داخليًا، وتستدعي RequestPage مرة واحدة، وتبدأ الخيط، وتُعيد مرجع TThread إلى المستدعي، الذي يملكه ويكون مسؤولًا عن تحريره. استرجاع النتيجة يمر عبر THotPDF.GetLoadedCachedRenderedBitmap بدلًا من GetCachedBitmap الخاصة بالراسم نفسه، لأن GetLoadedCachedRenderedBitmap تقرأ ذاكرة التخزين المؤقت المشتركة للمستند المفهرسة برقم الصفحة ودقة النقاط في البوصة، وهي ذاكرة التخزين المؤقت نفسها التي تملؤها بالفعل RenderLoadedPageToBitmapCached ومُحمّل التحميل المسبق المدمج — صفحة رسمها بالفعل جزء آخر من العارض بتلك الدقة يمكن أن تعود فورًا، حتى قبل أن يُجدول نظام التشغيل الخيط الخلفي الذي بدأ للتو
// A simpler alternative to the queue above, for one page at a time.
procedure TViewerForm.RequestSinglePage(PageIndex: Integer);
begin
if FAsyncWorker <> nil then
FAsyncWorker.Free; // waits if a prior page is still rendering
FAsyncWorker := Pdf.RenderLoadedPageToBitmapAsync(PageIndex, 150);
FPendingPage := PageIndex;
end;
procedure TViewerForm.AsyncPollTimerTimer(Sender: TObject);
var
Bmp: TBitmap;
begin
Bmp := Pdf.GetLoadedCachedRenderedBitmap(FPendingPage, 150);
if Bmp <> nil then
begin
PageImage.Picture.Bitmap.Assign(Bmp);
Bmp.Free;
end;
end;
هل يمكن إلغاء صفحة موضوعة بالفعل في قائمة الانتظار؟
تُزيل CancelAll فقط المهام التي لا تزال جالسة في قائمة الانتظار؛ أما صفحة سحبها HotPDF بالفعل من المقدمة وسلّمها إلى استدعاء الرسم الخاص بها فتستمر حتى الإنجاز، لأن THPDFBackgroundRenderer لا تملك آلية لمقاطعة عمل قيد التقدم بالفعل. هذه مقايضة معقولة عمليًا — نادرًا ما يكون رسم صفحة واحدة طويلًا بما يكفي لجعل المقاطعة تستحق التعقيد الإضافي — لكن تمريرًا سريعًا يطلق CancelAll عند كل حدث تمرير لا يزال يدفع ثمن أيًّا كانت الصفحة الواحدة التي كانت قيد الرسم في لحظة كل إلغاء. المرجع الرسمي صريح بخصوص هذا: قد ينتهي الرسم الجاري بالفعل قبل أن ينهي الخيط عمله
لدى Execute سلوك ثانٍ يسهل تفويته: تخرج الحلقة بمجرد أن تجد قائمة الانتظار فارغة، فهي لا تبقى خاملة منتظرة وصول مزيد من العمل. لذا فإن مثيل THPDFBackgroundRenderer عامل دفعي أحادي الاستخدام، لا خدمة خلفية دائمة — ضع بضع صفحات في قائمة الانتظار، واستدعِ Start، وبمجرد رسم آخر صفحة موضوعة في القائمة ينتهي خيط نظام التشغيل الكامن من تلقاء نفسه. استدعاء RequestPage مرة أخرى على المثيل نفسه بعد أن أفرغت Execute قائمة الانتظار بالفعل لا يعيد تشغيلها، وهذا بالضبط سبب استبدال RequestPageWindow أعلاه لمثيل الراسم في كل استدعاء بدلًا من محاولة الاستمرار في تغذية كائن واحد طويل العمر
هل من الآمن لمس TBitmap من خيط في الخلفية في Delphi؟
لمس TBitmap من خيط في الخلفية آمن في تصميم HotPDF طالما أن خيطًا واحدًا فقط يعمل على مثيل صورة نقطية معيّن في أي وقت، وتفرض THPDFBackgroundRenderer ذلك الحد بدلًا من تركه للمستدعي. ترسم Execute كل صفحة داخل قفل الرسم الخاص بالمستند نفسه، وهو القسم الحرج نفسه الذي يتشاركه بالفعل كل استدعاء RenderLoadedPageToBitmapCached ومُحمّل التحميل المسبق المدمج PrefetchLoadedPages، بحيث يحدث رسم GDI الفعلي لصفحة معينة على خيط واحد بالضبط في كل مرة ولا يتداخل أبدًا مع رسم آخر لذلك المستند. الصورة النقطية الناتجة كائن مملوك لخيط عامل لا تنشره THPDFBackgroundRenderer مباشرة إلى أي مستدعٍ
تُخصص GetCachedBitmap بدلًا من ذلك TBitmap جديدة تمامًا وتستدعي Assign عليها تحت قفل منفصل خاص بالراسم، بحيث يحدث النسخ دائمًا بينما تكون Execute محجوبة عن استبدال فتحة ذاكرة التخزين المؤقت تلك من تحته — يحصل الخيط المستدعي على بيانات بكسل، أبدًا على المقبض الأصلي. هذا الفصل هو أيضًا سبب وجوب مقاومة بناء خيط رسم مخصص يستدعي دوال رسم HotPDF مباشرة دون المرور عبر THPDFBackgroundRenderer أو PrefetchLoadedPages: تسابق رسمين على ذاكرات التخزين المؤقت المشتركة ورسم الكائنات لمستند محمَّل واحد نفسه هو بالضبط السيناريو الذي وُجد قفل HotPDF الداخلي لمنعه، وتمنحك فئة الراسم الخلفي ذلك القفل مجانًا بدلًا من إعادة تنفيذه
كيف يختلف هذا عن التحميل المسبق للصفحات المدمج في HotPDF؟
تحل PrefetchLoadedPages وTHPDFBackgroundRenderer مشكلتين مرتبطتين لكن مختلفتين: PrefetchLoadedPages، عند إعطائها نطاق صفحات، ترسم كل ذلك الجوار إلى ذاكرة التخزين المؤقت المشتركة للمستند تلقائيًا على خيط عامل خاص بها، بلا كائن قائمة انتظار يجب على المستدعي إنشاؤه أو إدارته. تستبدل THPDFBackgroundRenderer تلك الأتمتة بالتحكم — يقرر المستدعي بالضبط أي أرقام صفحات تهم وبأي ترتيب، ويمكنه إلغاء ما لا يزال منها في قائمة الانتظار دون لمس أي نطاق يُسخّنه المُحمّل المسبق المدمج في مكان آخر. تصبّ كلتاهما عبر قفل الرسم نفسه، بحيث يمكن لعارض تشغيل PrefetchLoadedPages للحالة العادية للصفحات القليلة التالية والوصول إلى THPDFBackgroundRenderer فقط عندما يظهر شيء خارج ذلك النمط، مثل شريط صور مصغّرة يقفز مباشرة إلى صفحة نقر عليها المستخدم للتو
begin
// PrefetchLoadedPages takes a 1-based "start-end" range string, while
// RequestPage below stays 0-based like every other loaded-page index.
Pdf.PrefetchLoadedPages(Format('%d-%d', [CenterPage + 1, CenterPage + 5]), 150);
// Reach for THPDFBackgroundRenderer only for a page outside that
// window, such as a thumbnail the user just clicked.
FRenderer := THPDFBackgroundRenderer.Create(Pdf, 150);
FRenderer.RequestPage(ClickedThumbnailPage);
FRenderer.Start;
end;
تفصيلان في دورة الحياة يستحقان النقل إلى شيفرة الإنتاج. ذاكرة التخزين المؤقت على مستوى المستند خلف RenderLoadedPageToBitmapCached محدودة بـRenderCacheCapacity، ثماني صفحات افتراضيًا، وتُخلي أقدم إدخال استُخدم بمجرد امتلائها، لكن قائمة نتائج مثيل THPDFBackgroundRenderer نفسه لا تملك حدًا كهذا — فهي تحتفظ بصورة نقطية واحدة لكل رقم صفحة مميز طُلب عبر ذلك المثيل حتى يُحرَّر المثيل نفسه، لذا فإن راسمًا أُبقي حيًا طوال جلسة تمرير كاملة بدقة عالية سيتراكم بسعادة صورة نقطية بدقة كاملة لكل صفحة مرّ بها التمرير. لا يلغي HotPDF أيضًا راسمًا أنشأه المستدعي تلقائيًا بالطريقة نفسها التي يلغي بها مُحمّله المسبق الخاص قبل تحميل مستند أو تدمير نفسه، بما أن مثيل THPDFBackgroundRenderer لا يُسجَّل أبدًا على كائن THotPDF الذي يشير إليه — لذا يجب على الشيفرة المستدعية إلغاء وتحرير كل راسم مبني مقابل مستند قبل إعادة تحميل ذلك المستند أو تحريره، وهو نظام الترتيب نفسه الذي يطبّقه HotPDF على PrefetchLoadedPages داخليًا
THPDFBackgroundRenderer قطعة واحدة من واجهة المستند المحمَّل خلف بنية عارض MVC في HotPDF، وتقترن بشكل طبيعي مع سير عمل مستوى الملف في واجهة الملف المباشر لملفات PDF الكبيرة عندما يكون المستند الذي يجري تمريره نفسه كبيرًا جدًا بحيث لا يمكن تحميله ببساطة أصلًا. الرسم في الخلفية، وقوائم انتظار الطلبات، وذاكرة التخزين المؤقت للرسم الموصوفة هنا كلها جزء من مكوّن HotPDF القياسي لـDelphi وC++Builder