مقاله فنی

نمایشگر PDF با اسکرول مداوم در دلفی با PDFium Component

یک صفحه منفرد A4 رندر شده با بزرگنمایی خواندن مناسب، در حدود چند مگابایت بیت‌مپ ۳۲ بیتی است. این را در یک قرارداد ۴۰۰ صفحه‌ای ضرب کنید تا محاسبات دیگر جنبه انتزاعی نداشته باشد: رندر کردن پیشاپیش تک‌تک صفحات به این معنی است که از ویندوز بیش از یک گیگابایت بیت‌مپ درخواست می‌کنید که کاربر هر بار تنها یک صفحه از آن را تماشا خواهد کرد. برنامه یا در نسخه ۳۲ بیتی با کمبود فضای آدرس مواجه می‌شود و یا اولین ثانیه‌های باز شدن خود را در حالت منجمد می‌گذراند در حالی که پردازنده گرافیکی و تجزیه‌کننده صفحات، صفحاتی را که هنوز کسی به آنها اسکرول نکرده پردازش می‌کنند. یک نمایشگر اسکرول مداوم باید حسی شبیه به یک نوار بلند از صفحات داشته باشد، اما در واقع نمی‌تواند همه آنها را به طور همزمان در حافظه نگه دارد

این چالش کل مسئله ما در اینجا است. کامپوننت PDFium آن را در داخل TPdfView حل می‌کند، بنابراین بیشتر کار انتخاب حالت نمایش مناسب و درک کاری است که کامپوننت به نمایندگی از شما انجام می‌دهد. بخش‌هایی که کامپوننت برای شما انجام نمی‌دهد، یعنی تغییر اندازه صفحات برای جریان خواندن و پاسخگو نگه داشتن اسکرول سریع، همان جایی است که نوشتن کمی کد ارزش خود را نشان می‌دهد. اگر هنوز در حال جمع‌آوری بخش‌های اطراف نمایشگر (نوار ابزار، تصاویر بندانگشتی، کادر جستجو) هستید، مقاله آموزش ساخت نمایشگر PDF با ویژگی‌های کامل آن بخش را پوشش می‌دهد؛ در اینجا موضوع خود اسکرول است

چیدمان یک حالت نمایش است و نه پنلی از بیت‌مپ‌ها

حس درونی در کار با فرم‌های VCL این است که به سراغ یک scroll box بروید و کنترل‌های تصویر را در داخل آن, یکی به ازای هر صفحه, روی هم بچینید. در برابر آن مقاومت کنید. آن طراحی شما را مجبور می‌کند تا موقعیت‌دهی صفحه، محاسبات اسکرول و مسئله حافظه را همزمان مدیریت کنید، و در نهایت تک‌تک آنها را به شکل بدی دوباره ابداع خواهید کرد. کنترل 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 همان کار را انجام می‌دهد اما اجازه می‌دهد صفحه اول به تنهایی به عنوان جلد بایستد تا پخش صفحات باقی‌مانده در مرز طبیعی زوج و فرد قرار گیرد. هر سه حالت به طور مداوم اسکرول می‌شوند. سوئیچ بین آنها یک انتساب ساده است، که اضافه کردن یک کامبو باکس حالت نمایش را بعداً بسیار ساده می‌کند

فقط صفحات قابل مشاهده رسترایز (rasterized) می‌شوند

دلیلی که این روش برای یک فایل ۴۰۰ صفحه‌ای مقیاس‌پذیر است، مجازی بودن ستون است. کنترل TPdfView ارتفاع هر صفحه را از درخت صفحات سند می‌داند، بنابراین می‌تواند کل میزان اسکرول و موقعیت هر صفحه را بدون رسترایز کردن چیزی محاسبه کند. رسترایز کردن، یعنی مرحله پرهزینه‌ای که استریم محتوای صفحه را به پیکسل تبدیل می‌کند، فقط برای صفحاتی اتفاق می‌افتد که در حال حاضر با درگاه دید (viewport) تلاقی دارند، به علاوه یک حاشیه کوچک تا صفحه در زمانی که به داخل دید اسکرول می‌شود آماده باشد. همانطور که به پایین اسکرول می‌کنید، صفحاتی که وارد درگاه دید می‌شوند رندر شده و صفحاتی که از آن خارج می‌گردند بیت‌مپ‌هایشان آزاد می‌شود. حافظه متناسب با آنچه روی صفحه قرار می‌گیرد باقی می‌ماند و نه طول سند

این موضوع ارزش درونی کردن را دارد زیرا روش استدلال شما را درباره هزینه تغییر می‌دهد. باز کردن یک سند ۴۰۰ صفحه‌ای ارزان است: ساختار را تجزیه می‌کند و نه محتوا را. هزینه به ازای هر صفحه است و به صورت تنبلانه (lazily) پرداخت می‌شود، در لحظه‌ای که به نزدیکی یک صفحه اسکرول می‌شود. نمایشگری که در هنگام باز شدن فوری و در هنگام اسکرول روان به نظر می‌رسد، در کل کار کمتری انجام نمی‌دهد، بلکه کار را در طول مسیر خواندن واقعی کاربر پخش می‌کند و آنچه را که در پشت سر باقی می‌ماند کنار می‌گذارد. نتیجه عملی این است که شما تقریباً هرگز نمی‌خواهید صفحات را جلوتر از کاربر مجبور به رندر کنید. اجازه دهید نما تصمیم بگیرد چه چیزی قابل مشاهده است

صفحات را هم‌اندازه عرض کنید، سپس بزرگنمایی را رها نمایید

یک ستون خواندن صفحاتی را می‌خواهد که متناسب با عرض پنل باشند، نه اینکه به یک بزرگنمایی مطلق سنجاق شوند. ویژگی FitMode این کار را انجام می‌دهد و با تغییر اندازه پنجره به انجام آن ادامه می‌دهد

PdfView.FitMode := pfmFitWidth;   // each page fills the column width; height follows

با ویژگی 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 برای دلفی و لازاروس هستند