صفحة A4 واحدة عند مستوى تكبير مريح للقراءة تستهلك عادة بضعة ميغابايت من صورة نقطية 32 بت. اضرب ذلك في ملف من 400 صفحة، وستتوقف الحسابات عن كونها مجرد أرقام: إذا عرضت كل صفحة مقدمًا فأنت تطلب من Windows أكثر بكثير من غيغابايت واحد من الصور النقطية التي لن ينظر المستخدم إلا إلى شاشة كاملة منها في كل مرة. إما أن ينفد التطبيق من مساحة العناوين في بنية 32 بت، أو يقضي بضع ثوانٍ أولى متجمّدًا بينما يكدح GPU ومحلل الصفحات عبر صفحات لم يصل إليها أحد بعد. يجب أن يشعر القارئ ذو التمرير المستمر كأنه شريط طويل واحد من الصفحات، لكنه لا يستطيع فعليًا الاحتفاظ بها كلها في الذاكرة في الوقت نفسه.
هذا التوتر هو جوهر المشكلة هنا. PDFium VCL يحلها داخل TPdfView، لذا فمعظم العمل يقتصر على اختيار وضع العرض الصحيح وفهم ما يفعله المكوّن نيابة عنك. أما الأجزاء التي لا يتولاها لك، مثل ضبط الصفحات لسير القراءة والحفاظ على استجابة التمرير السريع، فهي الموضع الذي يستحق فيه القليل من الشفرة نفسه. إذا كنت لا تزال تبني الواجهة المحيطة، مثل شريط الأدوات والصور المصغرة ومربع البحث، فإن الشرح التطبيقي للعارض الغني بالميزات يغطي ذلك الجانب؛ أما هنا فالموضوع هو التمرير نفسه.
التخطيط هو وضع عرض، لا لوحة من صور نقطية
الحدس المعتاد عند العمل على نماذج VCL هو أن تلجأ إلى ScrollBox وتكدّس داخله عناصر صور، واحدًا لكل صفحة. قاوم ذلك. فهذا التصميم يجعلك مسؤولًا عن تموضع الصفحات، وحسابات التمرير، ومسألة الذاكرة كلها دفعة واحدة، وستعيد اختراع كل واحدة منها بطريقة سيئة. TPdfView يمثّل المستند بالفعل كسلسلة متصلة من الصفحات، ويكشف التخطيط عبر الخاصية DisplayMode.
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // one page wide, scrolls vertically
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
هذا هو إعداد التمرير المستمر بالكامل. يرصّ dmSingleContinuous الصفحات في عمود رأسي واحد، مع معالجة الفراغات بينها داخليًا، ويقوم العرض بالتمرير عبر هذا العمود كسطح واحد. لا يوجد تحكم خاص بكل صفحة تحتاج إلى توصيله، ولا معالج تمرير تحتاج إلى كتابته للتنقل العادي. لاحظ التحقق من Pdf.Active بعد الإسناد: فتح المستند لا يرمي استثناءً أبدًا، لذا فإن الملف التالف أو المحمي بكلمة مرور يترك Active عند False من دون استثناء تلتقطه، والقارئ الذي يتجاهل هذا التحقق سيعرض لوحة فارغة ثم يلوم نفسه.
الخاصية نفسها تحمل أوضاع الصفحات المزدوجة. dmTwoPageContinuous يضع الصفحات جنبًا إلى جنب، صفحتين في كل صف، للقراءة بأسلوب الكتاب التي تريدها بعض المستندات؛ أما dmTwoPageContinuousWithCover فيفعل الشيء نفسه لكنه يترك الصفحة الأولى منفردة كغلاف، بحيث تقع الصفحات المتبقية على الحد الطبيعي بين الزوجي والفردي. الأوضاع الثلاثة كلها تتدفق مع التمرير المستمر. التبديل بينها مجرد إسناد واحد، لذلك تصبح إضافة قائمة منسدلة لوضع العرض لاحقًا بسيطة جدًا.
الصفحات المرئية فقط تُحوَّل إلى صور نقطية
السبب في أن هذا يتوسع إلى ملف من 400 صفحة هو أن العمود افتراضي. يعرف TPdfView ارتفاع كل صفحة من شجرة صفحات المستند، لذلك يمكنه حساب المدى الكلي للتمرير وموضع كل صفحة من دون تحويل أي شيء إلى صور نقطية. التحويل إلى صور نقطية، وهي الخطوة المكلفة التي تحوّل تيار محتوى الصفحة إلى بكسلات، يحدث فقط للصفحات التي تتقاطع حاليًا مع نافذة العرض، مع هامش صغير كي تكون الصفحة جاهزة بحلول لحظة دخولها إلى الشاشة. وأنت تمرر إلى الأسفل، تُرسم الصفحات التي تدخل نافذة العرض وتُفرج الصور النقطية للصفحات التي تغادرها. تبقى الذاكرة متناسبة مع ما يتسع له الشاشة، لا مع طول المستند.
من المفيد ترسيخ هذا في ذهنك لأنه يغيّر طريقة تفكيرك في الكلفة. فتح ملف من 400 صفحة رخيص: فهو يحلل البنية، لا المحتوى. الكلفة تُدفع لكل صفحة على حدة، وتُدفع بصورة مؤجلة في اللحظة التي تقترب فيها الصفحة من موضع العرض. القارئ الذي يبدو فوريًا عند الفتح وسلسًا عند التمرير لا يقوم بعمل أقل في المحصلة، بل يوزع العمل على مسار القراءة الفعلي للمستخدم ويتخلص مما يتخلف عنه. والنتيجة العملية أنك نادرًا ما تريد فرض عرض الصفحات قبل المستخدم. دع العرض يقرر ما هو المرئي.
اضبط الصفحات على العرض، ثم اترك التكبير كما هو
عمود القراءة يريد صفحات مضبوطة على عرض اللوحة، لا مثبتة على تكبير مطلق. FitMode يفعل ذلك ويستمر في فعله مع تغيير حجم النافذة.
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
مع pfmFitWidth يعيد المكوّن حساب التكبير كلما تغيّر حجم العرض، لذلك يمتلئ العمود دائمًا بالعرض المتاح، ومن ذلك تتبع ارتفاعات الصفحات، وبالتالي مدى التمرير. وهناك فخ يقع فيه كثيرون: إسناد Zoom مباشرة يعيد FitMode إلى pfmNone. ذلك مقصود، لأن التكبير اليدوي والاحتواء التلقائي نيتان متعارضتان، لكنه يعني أن سطرًا شاردًا مثل PdfView.Zoom := 1.0 في مكان ما من الشفرة يوقف الاحتواء على العرض بصمت، ويجعل تغيير الحجم التالي يتوقف عن إعادة التدفق. إذا وفرت كلًا من عنصر تحكم للتكبير وزر احتواء، فتعامل معهما كمفتاح وضع: تعيين أحدهما يلغي الآخر، وأنت تقرر أيهما ينتصر.
لأدوات التحكم في التكبير المباشر التي تبدو طبيعية، يكشف العرض عن قيم التكبير الملائمة بوصفها قيَمًا يمكنك تطبيقها أو عرضها: تُعيد PageWidthZoom[PageNumber] التكبير الذي يلاءم تلك الصفحة مع العرض، بينما PageZoom المطابقة تلاءم الصفحة كاملة مع العرض. قراءة هذه القيم هي الطريقة لملء قائمة "Fit Width" / "Fit Page" من دون ترميز نسب مئوية سحرية تخطئ مع الصفحات الأفقية أو كبيرة الحجم.
حافظ على استجابة التمرير السريع بالعرض التدرجي
مسار العرض الافتراضي يرسم الصفحة حتى تكتمل قبل أن يعود. لصفحة واحدة، هذا جيد. أثناء التمرير السريع عبر مستند كثيف، لا يكون كذلك: كل صفحة تمر سريعًا تشغّل تحويلًا كاملًا إلى صور نقطية، وإذا كان المستخدم يمرر أسرع مما تستطيع الصفحات أن تُعرض، تتراكم هذه العروض وتبدأ اللوحة في التقطّع لأن العمل يُنجز لصفحات أصبحت خارج الشاشة بالفعل عند اكتماله. الحل هو جعل العرض قابلًا للإلغاء والتخلي عنه في اللحظة التي ينتقل فيها المستخدم إلى الصفحة التالية.
RenderPageProgressive يعرض على دفعات ويفحص رمز إلغاء عند كل حد دفعة، بحيث يمكن إسقاط عرض جارٍ لصفحة غادرت للتو بدلًا من تركه يصل إلى النهاية.
type
TFormMain = class(TForm)
// ...
private
FRenderCancel: IPdfCancellationTokenSource;
procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
end;
procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
Status: TPdfProgressiveStatus;
begin
// Cancel whatever was rendering; the old token is now signaled.
if Assigned(FRenderCancel) then
FRenderCancel.Cancel;
FRenderCancel := TPdfCancellationTokenSource.New;
Pdf.PageNumber := PageNo;
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
FRenderCancel.Token);
case Status of
prsDone: ; // bitmap is complete, paint it
prsCancelled: Exit; // superseded, discard this result
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
الشكل المهم هنا هو قيمة الإرجاع. prsDone تعني أن الصورة النقطية رُسمت بالكامل وتستحق العرض على الشاشة؛ prsCancelled تعني أن موضع تمرير أحدث حل محل هذه الصفحة، لذا تُلقي النتيجة الجزئية بدلًا من عرضها؛ prsFailed يعني خطأ حقيقيًا في تلك الصفحة. الإلغاء يُفحص عند حدود الدفعات لا بصورة استباقية، لذا توقّع تأخرًا من عشرات المللي ثانية بين استدعاء Cancel وتوقف العرض فعليًا. هذا ما يزال أرخص بكثير من السماح لعرض كامل قديم بأن يحجب الطابور. تمرير nil بوصفه الرمز يعرض مباشرة حتى الاكتمال، وهو الخيار الصحيح لعرض لمرة واحدة مثل معاينة الطباعة، حيث لا يوجد ما يمكن إلغاؤه.
عندما تستدعي الشكل الدالي من RenderPage بدلًا من ذلك، أي النسخة التي تعيد TBitmap جديدة، فتذكّر أن المستدعي يملكها ويجب أن يستدعي Free عليها. في حلقة تمرير تخصّص صورة نقطية لكل صفحة، نسيان ذلك تسرب يكبر مع كل صفحة يمر بها المستخدم، وهو بالضبط فشل الذاكرة غير المحدودة الذي كان التصميم المستمر يفترض أن يتجنبه. ارسم داخل صورة نقطية معاد استخدامها كلما أمكن.
ما الذي يبقى لك
قارئ التمرير المستمر يقدمه المكوّن في معظمه. تختار dmSingleContinuous للتخطيط، وتضبط pfmFitWidth كي يعيد العمود التدفق مع النافذة، وتتحقق من Pdf.Active كي يفشل الملف التالف بصوت واضح. الجزء الوحيد الذي يستحق أن تكتبه بنفسك هو العرض القابل للإلغاء، لأن القارئ يُحكم عليه من خلال ما يفعله عندما يسحب أحدهم شريط التمرير إلى أسفل مستند طويل، وهل تواكب اللوحة ذلك أم لا. أما ما بعد ذلك، من تحديد النص عبر الصفحات وإبراز البحث وشجرة الإشارات المرجعية، فهو عمل واجهة يعلو هذا السطح القابل للتمرير لا عملًا داخله.
تُعد واجهات TPdfView وDisplayMode وRenderPageProgressive المعروضة هنا جزءًا من مكوّن PDFium VCL الخاص بـ Delphi وLazarus.