מאמר טכני

ייצוא TDataSet ודוחות ל-PDF בדלפי

כדי לייצא TDataSet או דוח ל-PDF בדלפי, losLab PDF Library מציעה שני נתיבים. PDFlibTableExport מתאימה כל TDataSet — שאילתת FireDAC,‏ ClientDataSet, או טבלה בזיכרון — לתוך טבלת PDF עם חלוקה לעמודים, ושלושה גשרים תחת Addons מעבירים דוח FastReport,‏ QuickReport, או ReportBuilder מוכן לאותו כותב PDF. שניהם מייצרים קובץ PDF אמיתי ללא מנהל התקן מדפסת וללא חלון גלוי

שתי הבעיות נראות דומות אך אינן כאלה. ל-DBGrid או לתוצאת שאילתה גולמית אין פריסה משלהם, לכן ייצוא שלהם פירושו המצאת פריסה: עמודות, כותרות עליונות, ומעברי עמודים. מסמך FastReport או ReportBuilder כבר נושא פריסה מעוצבת, ולכן ייצוא שלו פירושו שחזור נאמן של פקודות הציור של מישהו אחר לתוך מרחב ה-PDF.‏ losLab PDF Library שומרת על נושאים אלה ביחידות נפרדות בדיוק מכיוון שמצבי הכשל שונים, והמשך מאמר זה עובר על כל אחד מהם ומצביע על המקומות שבהם הם מפסיקים להיות אמינים

כיצד מייצאים TDataSet ל-PDF בדלפי?

PDFlibTableExport הופכת ערכת נתונים (dataset) לטבלה בקריאה אחת. המייצא עובר על רשימת השדות פעם אחת, מדלג על שדות blob בינאריים באופן אוטומטי — ל-ftBlob,‏ ftGraphic,‏ ftBytes ודומיהם אין טקסט תא שימושי — מיישר לימין עמודות מספריות, ומצייר פסי זברה אופציונליים מעל פס כותרת מעוצב. מתחת למכסה המנוע הוא בונה את הגריד באמצעות CreateTable, ממלא תאים באמצעות SetTableCellContent, ומציג באמצעות DrawTableRows, אותו Table API ציבורי שהיית מפעיל ידנית. מעטפת הנוחות PDFlibExportDataSet מגדירה עבורך מילימטרים וראשית צירים בפינה השמאלית העליונה, כך שהקורא מספק רק עמוד

uses
  Data.DB, FireDAC.Comp.Client, FireDAC.Stan.StorageBin,
  PDFlibrary, PDFlibTableExport;

var
  MemTable: TFDMemTable;
  PDF: TPDFlib;
  Options: TPDFlibTableExportOptions;
begin
  MemTable := TFDMemTable.Create(nil);
  PDF := TPDFlib.Create;
  try
    MemTable.LoadFromFile('customer.FDS');   // any TDataSet works here

    PDF.SetOrigin(1);
    PDF.SetMeasurementUnits(1);              // millimetres
    PDF.SetPageSize('A4');
    PDF.AddStandardFont(4);

    Options := DefaultTableExportOptions;
    Options.Title := 'Customers';
    Options.ColumnWidth := 32;               // 11 fields fit on A4 at 32 mm
    Options.RepeatHeader := True;            // redraw the header on each page

    PDFlibExportDataSet(PDF, MemTable, 'customers.pdf', Options);
  finally
    PDF.Free;
    MemTable.Free;
  end;
end;

הדילוג האוטומטי על blob הוא ברירת מחדל, לא מגבלה קשיחה. כאשר אתה זקוק לשליטה פר-שדה — תתווית עמודה מותאמת אישית, רוחב צר יותר, או השמטת עמודת מפתח פנימית שאינה blob — בנה TPDFlibTableExporter ישירות וחבר את אירוע OnFieldFilter שלו, אשר מופעל פעם אחת לכל שדה ומעניק לך מפרט שניתן לשינוי. הגדר את Include ל-False כדי להשמיט את השדה, או הגדר את ColumnWidth ו-DisplayLabel כדי לעקוף את ברירות המחדל. הוק (hook) זה הוא גם המקום שבו אתה מוציא מכלל כתיבה עמודת תזכיר (memo) רחבה שאינך רוצה שתנפח את העמוד

חלוקה לעמודים: העברת מצב הציור בין עמודים

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

// How the exporter continues a long table across pages
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // Last row = 0 means "draw to the end", bounded by DrawHeight
  PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
  LastDrawn := PDF.GetTableLastDrawnRow(TableID);
  if LastDrawn >= TotalRows then
    Break;                       // the whole table fit on this page
  if LastDrawn < Row then
    Break;                       // safety: no forward progress, bail out
  PDF.NewPage;
  Row := LastDrawn + 1;          // continue from the first undrawn row
  Y := PageHeight - Options.Top;
end;

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

כיצד מייצאים דוח FastReport או ReportBuilder ל-PDF?

גשרי הדוחות נוקטים בגישה ההפוכה: הם אף פעם לא ממציאים פריסה, הם משחזרים אותה. כל גשר מתחבר למנוע שלו בתפר הייצוא המקורי של המנוע. PDFlibFRExport יורש מ-TfrxCustomExportFilter ומקבל את אובייקטי התזכיר, התמונה, הצורה והקו של FastReport, ומתרגם כל אחד מהם לטיפוס בסיסי של PDFlibPas. PDFlibRBDevice יורש מ-TppFileDevice ועובר על רשימת DrawCommand של ReportBuilder. PDFlibQRExport נוקט בנתיב שלישי לחלוטין, שבו עוסק הסעיף הבא. שלושתם פועלים ללא ממשק משתמש (headless), וזו כל הסיבה להשתמש בהם בשרת

הפעלה ללא ממשק משתמש אינה בחינם, ו-FastReport הוא תמרור האזהרה. TfrxReport.Export ניתב דרך דפי התצוגה המקדימה, אשר מתייעצים עם מאפיין ShowDialog של המסנן, ומאפיין זה יורש ברירת מחדל של True. במחשב ללא שולחן עבודה אינטראקטיבי, תיבת הדו-שיח המודאלית מחזירה תוצאת ביטול והייצוא נכשל בשקט. הגדר את ShowDialog ל-False לפני שתקרא ל-Export והדוח יוצג בשקט. ל-ReportBuilder יש מתגים מקבילים — AllowPrintToFile and ShowPrintDialog — ו-QuickReport, המניע הכל דרך Prepare, אינו זקוק להשתקת תיבות דו-שיח כלל

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // build the pages first
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // headless: skip the modal, no cancel
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

שלושה מנועים, שלוש מערכות קואורדינטות

כל גשר מבצע חישוב קואורדינטות משלו, מכיוון שכל מנוע מודד את העולם בצורה שונה. PDFlibFRExport מתייחס למיקומי אובייקטים של FastReport כפיקסלים ב-96 PPI ומבצע התאמת קנה מידה לפי 96/25.4 כדי להגיע למילימטרים. PDFlibRBDevice קורא את פקודות הציור של ReportBuilder באלפיות המילימטר ומחלק ב-1000, והוא רושם את עצמו דרך ppRegisterDevice, כך שהגדרת ppReport.DeviceType ל-'PDFlibPas' וקריאה ל-Print מספיקות כדי לנתב את הפלט דרכו. PDFlibQRExport אינו מבצע מתמטיקת קואורדינטות כלל: דף QuickReport מוכן הוא כבר קובץ מטא-פייל EMF, ולכן הגשר שומר כל דף לזרם (stream) ומזין אותו ל-ImportEMFFromStream, ומאפשר ל-נתיב רינדור מטא-פיילים והקשר התקן (device-context) של הספרייה למקם כל גליף וכלל. גשרי FastReport ו-ReportBuilder, לעומת זאת, פולטים רכיבי ציור וקטוריים מקוריים וטקסט מקורי

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // Prepares the report and appends every page in one call; the bridge
    // turns each page's EMF into PDF through ImportEMFFromStream.
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

היכן שהדיוק מסתיים

דע את הגבולות לפני שאתה מתחייב לזרימת עבודה עם הגשרים האלה. מייצא ערכת הנתונים (dataset) משמיט עמודות בינאריות בשקט, ולכן דוח שחייב להציג תמונה משולבת זקוק לגישה שונה מאשר טבלה ישירה. נתיב QuickReport, מכיוון שהוא עובר דרך EMF, אופה טקסט לתוך גליפים וקטוריים — הדף נראה תקין אך אינו נושא טקסט הניתן לבחירה או לחיפוש וללא מבנה PDF מתויג עבור ננגישות.‏ FastReport ו-ReportBuilder שומרים על טקסט אמיתי, אך המטפלים המוגדרים מכסים את התצוגות הנפוצות — תזכיר (memo), תמונה, צורה, קו — ונסוגים לתיבה חוסמת (bounding box) או מדלגים על האקזוטיים שבהם, כך שדוח הנשען על טקסט עשיר, ברקודים או מילויי מעבר צבעים (gradient) יאבד פרטים. אף אחד מאלה אינו פגם; זהו הגבול הכנה של שכבת תרגום

אזהרה מבצעית אחת עולה בחשיבותה על השאר. אף אחד משלושת המנועים אינו מגיע בתוך losLab PDF Library, והגשרים מוחרגים בכוונה מהבנייה הראשית — הם מתקמפלים רק מפרויקט שכבר כולל את FastReport VCL 6.x,‏ QuickReport 8, או ReportBuilder 20 בנתיב החיפוש שלו. משטחי ה-metafile וה-DrawCommand שהם מכוונים אליהם נותרו יציבים לאורך כמה גרסאות מנוע עיקריות, אך התאמת הגרסאות היא באחריותך. עשה זאת נכון ושני הנתיבים יכסו את כל הטווח, החל מתוצר DBGrid נקודתי ועד לחשבונית מעוצבת. מייצא הטבלאות וגשרי הדוחות מגיעים שניהם עם losLab PDF Library עבור דלפי ו-C++Builder