مقاله فنی

خروجی گرفتن از TDataSet و گزارش‌ها به PDF در دلفی

برای خروجی گرفتن از یک TDataSet یا یک گزارش به PDF در دلفی، کتابخانه PDF losLab دو مسیر را ارائه می‌دهد. PDFlibTableExport هر TDataSet — یک پرس‌وجوی FireDAC، یک ClientDataSet، یا یک جدول درون حافظه (in-memory) — را به یک جدول PDF صفحه‌بندی‌شده تبدیل می‌کند، و سه پل ارتباطی در بخش Addons یک گزارش آماده‌شده FastReport، QuickReport، یا ReportBuilder را به همان نویسنده PDF تحویل می‌دهند. هر دو روش یک PDF واقعی بدون نیاز به درایور چاپگر و بدون پنجره مرئی تولید می‌کنند

این دو مسئله مشابه به نظر می‌رسند اما این‌طور نیست. یک DBGrid یا نتیجه یک پرس‌وجوی خام هیچ چیدمان (layout) خاص خود را ندارد، بنابراین خروجی گرفتن از آن به معنای ابداع یک چیدمان است: ستون‌ها، هدرها، و شکستگی‌های صفحه. یک سند FastReport یا ReportBuilder از قبل دارای یک چیدمان طراحی‌شده است، بنابراین خروجی گرفتن از آن به معنای بازپخش وفادارانه دستورات ترسیم شخص دیگری در فضای PDF است. کتابخانه PDF losLab دقیقاً به این دلیل که حالت‌های شکست (failure modes) متفاوت هستند، این موارد را در واحدهای مجزا نگه می‌دارد، و ادامه این مقاله به بررسی هر یک از آن‌ها و جاهایی که دیگر قابل اعتماد نیستند می‌پردازد

چگونه از یک TDataSet به PDF در دلفی خروجی بگیریم؟

PDFlibTableExport یک مجموعه داده (dataset) را با یک بار فراخوانی به جدول تبدیل می‌کند. صادرکننده لیست فیلدها را یک بار پیمایش می‌کند، فیلدهای باینری blob را به طور خودکار نادیده می‌گیرد — ftBlob، ftGraphic، ftBytes و مانند آن‌ها متن سلول مفیدی ندارند — ستون‌های عددی را راست‌چین می‌کند، و یک طرح راه‌راه گورخری (zebra stripe) اختیاری روی باند هدر استایل‌دهی‌شده ترسیم می‌کند. در پشت صحنه، این ابزار جدول را با CreateTable می‌سازد، سلول‌ها را با SetTableCellContent پر می‌کند، و با DrawTableRows رندر می‌نماید، که همان 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 را برای لغو مقادیر پیش‌فرض تنظیم کنید. این رویداد همچنین جایی است که شما یک ستون عریض memo را که نمی‌خواهید صفحه را بیش از حد بزرگ کند، حذف می‌کنید

صفحه‌بندی: انتقال وضعیت ترسیم بین صفحات

DrawTableRows وضعیت صفحه‌بندی را حمل می‌کند و درک نحوه عملکرد آن کلید اصلی کار است. شما آن را با اولین ردیف و آخرین ردیف فراخوانی می‌کنید؛ ارسال ردیف آخر با مقدار کمتر از ۱ به معنای ترسیم تا انتهای جدول است که توسط ارتفاع داده‌شده محدود می‌شود. این تابع ارتفاعی را که واقعاً ترسیم کرده است برمی‌گرداند، و 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;

حلقه همچنین جایی است که هدر تکراری در آن قرار دارد. در هر صفحه بعدی، صادرکننده به صورت اختیاری ردیف ۱ — هدر — را دوباره قبل از ردیف‌های داده ترسیم می‌کند و از ارتفاع برگشتی توسط اولین فراخوانی DrawTableRows برای ایجاد فاصله بدنه زیر آن استفاده می‌کند. به همین دلیل است که DrawTableRows یک ارتفاع را برمی‌گرداند تا مختصات ردیف بعدی: مقدار برگشتی همان چیزی است که به شما امکان می‌دهد یک هدر دوباره ترسیم‌شده و بدنه ادامه‌یافته را بدون نیاز به کدنویسی سخت اندازه هر کدام، روی هم قرار دهید

چگونه یک گزارش FastReport یا ReportBuilder را به PDF صادر کنیم؟

پل‌های گزارش رویکرد مخالفی را در پیش می‌گیرند: آن‌ها هرگز چیدمانی ابداع نمی‌کنند، بلکه آن را بازپخش می‌نمایند. هر پل به نقطه خروجی بومی موتور خود متصل می‌شود. PDFlibFRExport از کلاس TfrxCustomExportFilter ارث‌بری می‌کند و اشیاء memo، تصویر، شکل و خط FastReport را دریافت کرده و هر کدام را به یک دستور پایه PDFlibPas ترجمه می‌کند. PDFlibRBDevice از کلاس TppFileDevice ارث‌بری کرده و لیست DrawCommand در ReportBuilder را پیمایش می‌کند. PDFlibQRExport به طور کامل مسیر سومی را در پیش می‌گیرد که در بخش بعدی به آن پرداخته می‌شود. هر سه به صورت بدون سر (headless) اجرا می‌شوند، که دلیل اصلی استفاده از آن‌ها روی سرور است

اجرای بدون سر (headless) بدون هزینه نیست و FastReport یک نمونه هشداردهنده است. متد TfrxReport.Export از صفحات پیش‌نمایش عبور می‌کند که ویژگی ShowDialog فیلتر را بررسی می‌کنند و این ویژگی مقدار پیش‌فرض True را به ارث می‌برد. در سیستمی بدون دسکتاپ تعاملی، دیالوگ مدال نتیجه لغو (cancel) را برمی‌گرداند و فرآیند خروجی گرفتن بی سر و صدا شکست می‌خورد. قبل از فراخوانی Export، مقدار ShowDialog را روی False قرار دهید تا گزارش بدون سر و صدا رندر شود. ReportBuilder دارای سوئیچ‌های مشابهی است — AllowPrintToFile و 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 آماده‌شده از قبل یک متاسند (metafile) EMF است، بنابراین پل ارتباطی هر صفحه را در یک جریان (stream) ذخیره می‌کند و آن را به ImportEMFFromStream می‌فرستد و اجازه می‌دهد دستگاه-متن و مسیر رندر متاسند کتابخانه هر گلیف و خطی را ترسیم کند. در مقابل، پل‌های 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) بازمی‌گردند یا آن‌ها را نادیده می‌گیرند، بنابراین گزارشی که متکی به متن غنی (rich text)، بارکدها یا پر کردن‌های گرادیان (gradient fills) باشد جزئیات را از دست خواهد داد. هیچ یک از این موارد نقص به شمار نمی‌آید؛ این مرز صادقانه یک لایه ترجمه است

یک هشدار عملیاتی مهم‌تر از بقیه است. هیچ یک از این سه موتور همراه با کتابخانه PDF losLab ارائه نمی‌شوند و پل‌ها به عمد از ساخت اصلی (main build) کنار گذاشته شده‌اند — آنها فقط از پروژه‌ای کامپایل می‌شوند که از قبل FastReport VCL 6.x، QuickReport 8، یا ReportBuilder 20 را در مسیر جستجوی خود داشته باشد. سطوح متاسند و DrawCommand که آن‌ها هدف قرار می‌دهند در چندین نسخه اصلی موتور پایدار مانده‌اند، اما تطابق نسخه بر عهده خودتان است. با انجام درست این کار، این دو مسیر کل دامنه کار را پوشش می‌دهند، از تخلیه ad-hoc یک DBGrid گرفته تا یک فاکتور طراحی‌شده. صادرکننده جدول و پل‌های گزارش هر دو همراه با losLab PDF Library برای دلفی و سی‌پلاس‌پلاس‌بیلدر ارائه می‌شوند