מאמר טכני

השוואת PDF זה לצד זה ב-Delphi עם PDFium Component

שני מסמכים פתוחים בו זמנית, אותו מספר עמוד, כל אחד בחלונית גלילה משלו: זה הליבה של מציג השוואה. PDFium Component מספק זאת דרך מודל אובייקטים ישיר שבו TPdf מחזיק את הקובץ ו-TPdfView מחזיק את התצוגה. מסמך אחד, TPdf אחד, TPdfView אחד. אתה רוצה שלוש חלוניות, יש לך שלושה זוגות. החלקים הקשים אינם קריאות הממשק; הם האריתמטיקה של הפריסה כשהחלון משנה גודל והלוגיקה של סנכרון העמודים כשאתה מחליט איזו תצוגה צריכה לעקוב אחרי מי

פריסת הטופס

טופס ה-VCL מחזיק שלושה מכלי TScrollBox זה לצד זה, בכל אחד TPdfView בפנים המיושר ל-alClient כך שהוא ממלא את התיבה. שני רכיבי TSplitter יושבים בין התיבות כך שהמשתמש יכול להתאים את רוחבי העמודות בזמן ריצה. סרגל כלים מעל החלוניות נושא את כפתורי הפתיחה, את פקדי הזום, ואת המתג בין שתי תצוגות לשלוש

מצב שלוש תצוגות הוא ערך בוליאני שהטופס עוקב אחריו פנימית. כשהוא מתהפך, אתה מחשב מחדש רוחבים ומציג או מסתיר את העמודה השלישית. הגישה הפשוטה ביותר היא לנקות את כל מאפייני ה-Align, להסתיר את המפרידים, ואז לקבוע מיקומים מוחלטים:

תרשים פריסת טופס של מציג השוואת PDF זה לצד זה ב-Delphi שנבנה עם PDFium Component, המראה סרגל כלים, שלוש תיבות גלילה עם חלוניות TPdfView, ומפרידים במצב שתי תצוגות ובמצב שלוש תצוגות
כל חלונית היא תיבת גלילה עם TPdfView בפנים, והמעבר בין שתי תצוגות לשלוש הוא רק קבוצה אחרת של הקצאות רוחב
procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // החל את אותו (ClientHeight פחות גובה סרגל הכלים) על כל שלושת ערכי Height
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

קביעת Align := alNone על שלוש התיבות לפני האריתמטיקה השלמה מונעת ממנוע האילוצים של VCL להיאבק בהקצאות שלך. שחזר את נראות המפרידים אחרי המיקום אם אתה רוצה גרירה לשינוי גודל במצב שתי תצוגות

הגובה של כל תיבת גלילה הוא שטח הלקוח פחות גובה חלונית סרגל הכלים. מכיוון שסרגל הכלים מעוגן בראש עם alTop, ClientHeight - PanelButtons.Height נותן לך את המרחב האנכי השמיש. הקצה את זה לשלוש התיבות בתוך אותה קריאת UpdateLayout כך שלעולם לא יהיה פריים שבו תיבה אחת גבוהה מהאחרות וגורמת להבהוב פריסה

פתיחת מסמך

כל זוג חלוניות זקוק לפרוצדורת פתיחה משלו. התבנית קצרה: השבת את הרכיב, קבע את שם הקובץ, הפעל, ואז בדוק את Active; אם הוא נשאר False, בקש סיסמה ונסה שוב. שים לב ש-TPdfView.Active הוא מה ששולט בשרטוט, אבל TPdf.Active הוא מה שבאמת פותח את הקובץ; הם בלתי תלויים. קביעת PdfView.Active := True כשה-TPdf המקושר אליו עדיין אינו פעיל אינה מזיקה אבל אינה מציגה דבר

תרשים זרימה של פתיחת מסמך PDF עם PDFium Component ב-Delphi, המראה את בדיקת Active השקטה, ניסיון סיסמה חוזר אחד, ודיאלוג שגיאה עבור קבצים פגומים או מוגני סיסמה
טעינה שנכשלה משאירה את Active במצב False בלי להעלות חריגה, ולכן הזרימה בודקת אותו, מנסה שוב פעם אחת עם סיסמה, ולבסוף מדווחת על הבעיה במקום להציג חלונית ריקה
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // כשלי טעינה שקטים: Active נשאר False במקום להעלות חריגה.
  if not PdfComponent.Active then
  begin
    // ככל הנראה קובץ מוגן בסיסמה; תן למשתמש ניסיון חוזר אחד.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

בדוק תמיד את PdfComponent.Active אחרי ההקצאה; קובץ פגום או סיסמה שגויה גורמים לטעינה להיכשל בשקט בלי להעלות חריגה בנתיב ברירת המחדל. קביעת PdfViewComponent.PageNumber := 1 במפורש אחרי פתיחה מוצלחת מונעת מספר עמוד מיושן מהמסמך הקודם

דיאלוג ההודעה בסוף הוא מכוון: אתה רוצה שקבצים פגומים או לא נתמכים יצוצו מיד ולא ייבלעו כחלונית ריקה שקטה. משתמש שאינו רואה דבר אין לו מושג אם הקובץ נטען והוא פשוט ריק, או שהרכיב דחה אותו. דיווח על הכשל שומר את השגיאה גלויה

מעקב אחר החלונית הפעילה

כשהמשתמש לוחץ בתוך חלונית, אותה חלונית הופכת לפעילה. הטופס עוקב אחרי שדה פרטי FActivePdfView: TPdfView. המשוב החזותי הוא שינוי צבע מסגרת ב-TScrollBox המכיל: קבע אותו ל-clHighlight עבור הפעיל ול-clWindow עבור האחרים. חבר את זה לכל TPdfView.OnClick ולפרוצדורת הפתיחה כך שהמיקוד יעקוב אחרי המסמך שזה עתה פתחת

חלק מהפעולות חלות על כל החלוניות הנראות ולא רק על הפעילה. ערך בוליאני FAllViewsMode על הטופס מניע את הענף הזה. כשהוא אמת, שינויי זום וניווט עמודים מתפרשים לכל חלונית שיש בה מסמך פעיל:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

ניווט עמודים מסונכרן

ניווט מסונכרן הוא אופציונלי אך שימושי לזרימות עבודה של גרסאות מסמך שבהן שני הקבצים מכסים את אותו טווח עמודים. הלוגיקה שייכת למטפל אירוע שנורה אחרי שהמשתמש ניווט בתצוגה אחת. כשתצוגת מקור משנה את ה-PageNumber שלה, המטפל מפיץ את המספר הזה לשאר התצוגות, בכפוף לשומר אחד: תצוגת היעד חייבת להכיל לפחות כל כך הרבה עמודים, אחרת דלג

ה-PageNumber על TPdfView ועל TPdf בלתי תלויים. TPdf.PageNumber עוקב אחרי איזה עמוד רכיב המסמך רואה כנוכחי; TPdfView.PageNumber עוקב אחרי מה שמוצג על המסך. לצורכי ניווט אתה רוצה את מאפיין התצוגה, לא את מאפיין המסמך

תיבת סימון שכותרתה משהו כמו "Sync pages" נותנת למשתמש שליטה. כשהיא אינה מסומנת, כל חלונית מנווטת באופן עצמאי והמטפל יוצא מיד. העצמאות הזו חשובה למקרי שימוש שבהם לשני המסמכים יש מספר עמודים שונה, או שבהם המשתמש רוצה למצוא את הקטע המקביל בתרגום שמתחיל בעמוד אחר. כפיית סנכרון תמיד הייתה הופכת את הכלי לקשה לשימוש יותר מסידור פשוט של שני חלונות בשולחן העבודה

דבר אחד שכדאי לשים לב אליו: קביעת PdfView.PageNumber תוכניתית בתוך מטפל הסנכרון תפעיל בעצמה את אירוע השינוי על אותה תצוגה. התגונן מפני רקורסיה אינסופית עם דגל בוליאני שאתה קובע לפני ההקצאה ומנקה מיד אחריה. הדגל הוא לכל טופס, לא לכל תצוגה, משום ששלוש התצוגות חולקות את אותו מטפל

תרשים של ניווט עמודים מסונכרן במציג השוואת PDF ב-Delphi המשתמש ב-PDFium Component, עם תיבת סימון הסנכרון, שומר מספר עמודים לכל תצוגת יעד, ודגל שומר רקורסיה
מספר העמוד נוסע מתצוגת המקור לכל תצוגה אחרת רק כשהסנכרון מופעל וכל תצוגת יעד באמת מכילה את העמוד הזה

זום לכל חלונית

כל TPdfView נושא מאפיין Zoom משלו, Double באחוזים שבו Zoom := 100 פירושו גודל אמיתי (100%). קביעתו דורסת כל FitMode פעיל. עבור כפתור התאמה לרוחב על החלונית הפעילה, קרא את זום ההתאמה מ-PdfView.PageWidthZoom[PdfView.PageNumber] והקצה אותו. עבור התאמה לעמוד, השתמש ב-PageZoom[PageNumber]. שניהם מאפייני מערך שמאונדקסים לפי מספר עמוד מבוסס אחד, ולכן התגונן מפני מספר עמוד אפס לפני הגישה אליהם

כשאתה מייצא את העמוד הנוכחי לתמונה, קרא את הסיבוב מהתצוגה אבל קרא ל-RenderPage על רכיב ה-TPdf, לא על התצוגה. צורת מפת הסיביות של TPdf.RenderPage מקבלת ממדי פיקסלים מפורשים בתוספת ערך TRotation וקבוצת TRenderOptions. וריאנט הפונקציה מחזיר TBitmap שבבעלות הקורא ושאתה משחרר בעצמך אחרי השמירה:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

המכפיל פי 2 על הרוחב והגובה נותן פלט חד יותר עבור מסמכים עם טקסט דק. ה-try/finally סביב שחרור מפת הסיביות אינו אופציונלי; ביטול של TSaveDialog עדיין פוגע בבלוק ה-finally, ואתה רוצה שמפת הסיביות תשוחרר בלי קשר למה שהמשתמש עשה

דרישות DLL

PDFium Component עוטף את ספריית pdfium הנייטיבית. תהליך מארח של 32 סיביות זקוק ל-pdfium32.dll; מארח של 64 סיביות זקוק ל-pdfium64.dll. וריאנטים עם מנוע ה-JavaScript של V8 מוסיפים את הסיומת v8 ושוקלים כ-23-27 מ"ב לעומת בניות סטנדרטיות של 5-6 מ"ב. עבור מציג השוואה שמשבית מילוי טפסים (Pdf.FormFill := False), הבנייה הסטנדרטית ללא V8 מספיקה ושומרת על הפצה קטנה יותר

הנח את ה-DLL באותה תיקייה של קובץ ההרצה, או בכל תיקייה שנמצאת ב-PATH של המערכת. הרכיב טוען אותו לפי דרישה כשה-TPdf הראשון מופעל, ולכן DLL חסר צץ בנקודה הזו ולא בהפעלת היישום. אם אתה מפיץ מתקין, הגישה האמינה ביותר היא להעתיק את ה-DLL אל תיקיית היישום במהלך ההתקנה במקום להסתמך על תיקיית מערכת שמנהל עשוי לנקות מאוחר יותר

בניות ה-V8 שימושיות בעיקר כשאתה צריך לתקשר עם פעולות JavaScript של PDF, למשל כדי להפעיל שדות חישוב או מטפלי שליחה. למציג השוואה פסיבי אין סיבה להריץ JavaScript; קביעת Pdf.FormFill := False לפני Active := True מדלגת על סביבת מילוי הטפסים לחלוטין, ומשמעות הדבר גם ששום מנוע JS אינו מאותחל אפילו אם נעשה שימוש בבנייה הסטנדרטית. זו ברירת המחדל הנכונה עבור מציג לקריאה בלבד בלי קשר לאיזה וריאנט DLL אתה מפיץ

לפרטים נוספים על PDFium Component ועל הממשק המלא שלו, בקר בעמוד המוצר Delphi PDFium Component