تستهلك صفحة A4 واحدة معروضة بتكبير مريح للقراءة بضعة ميجابايتات من صورة نقطية 32 بتة. اضرب ذلك في عقد مكون من 400 صفحة ولن يصبح الحساب مجرداً: اعرض كل صفحة مقدماً وستطلب من ويندوز ما يزيد عن جيجابايت من الصور النقطية التي سينظر إليها المستخدم ملء شاشة واحدة في كل مرة. إما أن ينفد التطبيق من مساحة العنوان في بنية 32 بتة أو يقضي ثوانيه القليلة الأولى متجمداً بينما تقوم وحدة معالجة الرسومات ومحلل الصفحات بالمرور عبر صفحات لم يقم أحد بالتمرير إليها بعد. يجب أن يبدو قارئ التمرير المستمر كشريط طويل واحد من الصفحات، ولكنه لا يمكنه فعلياً الاحتفاظ بها جميعاً في الذاكرة في وقت واحد
هذا التوتر هو المشكلة برمتها هنا. يحل مكون PDFium هذه المشكلة داخل TPdfView، لذا فإن معظم العمل يتمثل في اختيار وضع العرض المناسب وفهم ما يفعله المكون نيابة عنك. القطع التي لا يفعلها من أجلك، مثل تحديد حجم الصفحات لتدفق القراءة والحفاظ على استجابة التمرير السريع، هي المكان الذي يكسب فيه القليل من الكود مكانه. وإذا كنت لا تزال تقوم بتجميع الكروم المحيط (شريط الأدوات، الصور المصغرة، مربع البحث)، فإن دليل عارض الميزات الغني يغطي تلك الأرضية؛ وهنا الموضوع هو التمرير نفسه
التخطيط هو وضع عرض، وليس لوحة من الصور النقطية
تتمثل غريزة العمل مع نموذج VCL في الوصول إلى مربع التمرير وتكديس عناصر التحكم في الصور بداخله، واحدة لكل صفحة. قاوم ذلك. يجبرك هذا التصميم على امتلاك موضع الصفحة وحسابات التمرير وسؤال الذاكرة دفعة واحدة، وسوف تعيد ابتكار كل منها بشكل سيئ. حيث ينمذج 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 ارتفاع كل صفحة من شجرة صفحات المستند، بحيث يمكنها حساب مدى التمرير الإجمالي وموضع كل صفحة دون تنقيط (rasterizing) أي شيء. والتنقيط، الخطوة المكلفة التي تحول تدفق محتوى الصفحة إلى بكسلات، تحدث فقط للصفحات التي تتقاطع حالياً مع منفذ العرض، بالإضافة إلى هامش صغير بحيث تكون الصفحة جاهزة بحلول الوقت الذي يتم تمريرها فيه إلى العرض. ومع التمرير لأسفل، يتم عرض الصفحات التي تدخل منفذ العرض ويتم تحرير الصور النقطية للصفحات التي تغادره. وتبقى الذاكرة متناسبة مع ما يناسب الشاشة، وليس مع طول المستند
هذا يستحق الاستيعاب لأنه يغير طريقة تفكيرك في التكلفة. فتح مستند مكون من 400 صفحة رخيص: فهو يحلل الهيكل وليس المحتوى. والنفقات هي لكل صفحة ويتم دفعها بكسل، في اللحظة التي يتم فيها التمرير بالقرب من الصفحة. العارض الذي يبدو فورياً عند الفتح وسلساً في التمرير لا يقوم بعمل أقل بشكل عام، بل يوزع العمل عبر مسار القراءة الفعلي للمستخدم ويتخلص مما يتخلف عن الركب. والنتيجة العملية هي أنك لا تريد أبداً إجبار عرض الصفحات أمام المستخدم. دع العرض يقرر ما هو مرئي
قم بملاءمة حجم الصفحات مع العرض، ثم اترك التكبير وشأنه
يريد عمود القراءة صفحات بحجم عرض اللوحة، وليس مثبتاً بتكبير مطلق. وتفعل FitMode ذلك وتستمر في فعله مع تغيير حجم النافذة
مع pfmFitWidth يعيد المكون حساب التكبير كلما تم تغيير حجم العرض، بحيث يملأ العمود دائماً العرض المتاح وتتبع ارتفاعات الصفحات، وبالتالي مدى التمرير، ذلك. وهناك فخ واحد يقع فيه الناس: تعيين Zoom مباشرة يعيد FitMode إلى pfmNone. هذا متعمد، لأن التكبير اليدوي والملاءمة التلقائية هما نيتان متعارضتان، ولكنه يعني أن تعييناً شاردًا PdfView.Zoom := 1.0 في مكان ما في كودك يوقف بصمت ملاءمة العرض ويوقف تغيير الحجم التالي إعادة التدفق. وإذا كنت تقدم كلاً من التحكم في التكبير وزر الملاءمة، فعاملهما كمفتاح وضع: تعيين أحدهما يمسح الآخر، وأنت تقرر أيهما يفوز
بالنسبة لعناصر التحكم في التكبير المطلق التي تقرأ بشكل طبيعي، يكشف العرض عن عمليات تكبير الملاءمة كقيم يمكنك تطبيقها أو عرضها: تعيد PageWidthZoom[PageNumber] التكبير الذي من شأنه أن يناسب تلك الصفحة مع العرض، وتناسب PageZoom المطابقة الصفحة بأكملها. وقراءة هذه القيم هي كيفية ملء قائمة "ملاءمة العرض" / "ملاءمة الصفحة" دون ترميز نسب سحرية بشكل يدوي تخطئ في الصفحات الأفقية أو ذات الحجم الزائد
حافظ على استجابة التمرير السريع من خلال العرض التدريجي
يرسم مسار العرض الافتراضي الصفحة حتى تكتمل قبل أن يعود. بالنسبة لصفحة واحدة، هذا أمر جيد. وخلال التمرير السريع عبر مستند كثيف، ليس الأمر كذلك: فكل صفحة تومض بالماضي تطلق عملية تنقيط كاملة، وإذا كان المستخدم يمرر أسرع مما يمكن للملفات أن تعرضه، تتراكم تلك المعروضات وتتلعثم اللوحة لأن العمل يتم لصفحات أصبحت بالفعل خارج الشاشة بحلول الوقت الذي ينتهي فيه. والإصلاح هو جعل العرض قابلاً للإلغاء والتخلي عنه في اللحظة التي ينتقل فيها المستخدم
يعرض 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 Component لدلفي ولازاروس