مقال تقني

تصدير TDataSet والتقارير إلى PDF في Delphi

لتصدير TDataSet أو تقرير إلى PDF في Delphi، تقدّم losLab PDF Library مسارين. تُكيِّف PDFlibTableExport أي TDataSet — استعلام FireDAC أو ClientDataSet أو جدولًا في الذاكرة — إلى جدول PDF مقسّم إلى صفحات، بينما تُسلِّم ثلاثة جسور ضمن Addons تقريرًا مُعدًّا من FastReport أو QuickReport أو ReportBuilder إلى كاتب PDF نفسه. وكلا المسارين ينتج ملف PDF حقيقيًا من دون تعريف طابعة ومن دون نافذة مرئية

تبدو المشكلتان متشابهتين وليستا كذلك. فـ DBGrid أو نتيجة استعلام خام لا تملك تخطيطًا خاصًا بها، لذا فإن تصديرها يعني اختراع تخطيط: أعمدة وترويسات وفواصل صفحات. أما مستند FastReport أو ReportBuilder فيحمل بالفعل تخطيطًا مصمَّمًا، لذا فإن تصديره يعني إعادة تشغيل أوامر رسم شخص آخر بأمانة في فضاء PDF. وتُبقي losLab PDF Library هذين الشاغلين في وحدتين منفصلتين تحديدًا لأن أنماط الفشل تختلف، وبقية هذا المقال تستعرض كلًّا منهما وأين يتوقف عن أن يكون موثوقًا

مساران لتصدير PDF في Delphi يُغذّيان جداول مجموعات البيانات وجسور التقارير إلى كاتب PDF واحد بلا واجهة
تخترع PDFlibTableExport جدولًا مقسّمًا إلى صفحات لأي TDataSet بينما تعيد ثلاثة جسور في Addons تشغيل تخطيطات FastReport و QuickReport و ReportBuilder في كاتب PDF نفسه

كيف تُصدِّر TDataSet إلى PDF في Delphi؟

تحوّل PDFlibTableExport مجموعة بيانات إلى جدول باستدعاء واحد. يجتاز المُصدِّر قائمة الحقول مرة واحدة، ويتخطى حقول blob الثنائية تلقائيًا — فـ ftBlob وftGraphic وftBytes وأقرانها لا تملك نص خلية مفيدًا — ويحاذي الأعمدة الرقمية إلى اليمين، ويرسم تخطيطًا متناوب الألوان اختياريًا فوق شريط ترويسة منسَّق. وتحت الغطاء يبني الشبكة بـ CreateTable، ويملأ الخلايا بـ SetTableCellContent، ويصيّر بـ DrawTableRows، وهي واجهة Table العامة نفسها التي كنت ستقودها يدويًا. ويضبط الغلاف المريح 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');   // أي TDataSet يعمل هنا

    PDF.SetOrigin(1);
    PDF.SetMeasurementUnits(1);              // مليمترات
    PDF.SetPageSize('A4');
    PDF.AddStandardFont(4);

    Options := DefaultTableExportOptions;
    Options.Title := 'Customers';
    Options.ColumnWidth := 32;               // 11 حقلًا تتسع على A4 بعرض 32 مم
    Options.RepeatHeader := True;            // أعد رسم الترويسة في كل صفحة

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

تخطي blob التلقائي إعداد افتراضي لا قيد صارم. فحين تحتاج إلى تحكم لكل حقل — تسمية عمود مخصصة، أو عرض أضيق، أو إسقاط عمود مفتاح داخلي ليس blob — أنشئ TPDFlibTableExporter مباشرة واربط حدثه OnFieldFilter، الذي يُطلَق مرة لكل حقل ويسلّمك مواصفة قابلة للتعديل. اضبط Include على False لإسقاط الحقل، أو اضبط ColumnWidth وDisplayLabel لتجاوز الافتراضيات. وذلك الخطاف هو أيضًا حيث تستبعد عمود memo عريضًا لا تريده أن ينتفخ في الصفحة

التقسيم إلى صفحات: تمرير حالة الرسم بين الصفحات

تحمل DrawTableRows حالة التقسيم إلى صفحات، وفهم عقدها هو اللعبة كلها. تستدعيها بصف أول وصف أخير؛ وتمرير صف أخير أقل من 1 يعني الرسم حتى نهاية الجدول، محدودًا بالارتفاع الذي أعطيته. وتُعيد الدالة الارتفاع الذي رسمته فعليًا، وتُبلغ GetTableLastDrawnRow عن آخر صف اتسع. وهذا الزوج هو الحالة التي تسلّمها من صفحة إلى التالية: حين يكون آخر صف مرسوم أقل من الإجمالي، تفتح صفحة جديدة وتستأنف من الصف الذي يليه. ولا يُعاد قياس أي شيء — يلتقط الاستئناف تمامًا من حيث توقف الاستدعاء السابق

PDF Library for Delphi: عقد التقسيم إلى صفحات لـ DrawTableRows يُظهر صفوف جدول تستمر عبر ثلاث صفحات PDF مع شريط ترويسة متكرر
تقترن DrawTableRows بـ GetTableLastDrawnRow بحيث تستأنف كل صفحة جديدة عند LastDrawn زائد واحد وتعيد رسم صف الترويسة
// كيف يواصل المُصدِّر جدولًا طويلًا عبر الصفحات
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // الصف الأخير = 0 يعني "ارسم حتى النهاية"، محدودًا بـ DrawHeight
  PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
  LastDrawn := PDF.GetTableLastDrawnRow(TableID);
  if LastDrawn >= TotalRows then
    Break;                       // الجدول بأكمله اتسع في هذه الصفحة
  if LastDrawn < Row then
    Break;                       // أمان: لا تقدّم للأمام، اخرج
  PDF.NewPage;
  Row := LastDrawn + 1;          // تابع من أول صف غير مرسوم
  Y := PageHeight - Options.Top;
end;

الحلقة هي أيضًا حيث تقيم الترويسة المتكررة. ففي كل صفحة استئناف يرسم المُصدِّر اختياريًا الصف 1 مرة أخرى — الترويسة — قبل صفوف البيانات، ويستخدم الارتفاع الذي أعاده استدعاء DrawTableRows الأول ذاك لإزاحة المتن تحته. ولهذا تُعيد DrawTableRows ارتفاعًا لا إحداثي الصف التالي: فالقيمة المُعادة هي ما يتيح لك تكديس ترويسة مُعاد رسمها والمتن المستمر من دون ترميز حجم أي منهما ثابتًا

كيف تُصدِّر تقرير FastReport أو ReportBuilder إلى PDF؟

تتبع جسور التقارير النهج المعاكس: فهي لا تخترع تخطيطًا أبدًا، بل تعيد تشغيله. ويرتبط كل جسر بمحركه عند درز التصدير الأصلي للمحرك. تشتق PDFlibFRExport من TfrxCustomExportFilter وتتلقى كائنات memo و picture و shape و line من FastReport، مترجمةً كلًّا منها إلى عنصر أولي في PDF Library for Delphi. وتشتق PDFlibRBDevice من TppFileDevice وتجتاز قائمة DrawCommand في ReportBuilder. أما PDFlibQRExport فتسلك طريقًا ثالثًا بالكامل، يغطيه القسم التالي. وتعمل الثلاثة بلا واجهة، وهذا هو السبب الكامل للجوء إليها على خادم

العمل بلا واجهة ليس مجانيًا، و FastReport هو القصة التحذيرية. فـ TfrxReport.Export يمرّ عبر صفحات المعاينة، التي تستشير خاصية ShowDialog للمرشِّح، وتلك الخاصية ترث افتراضيًا قيمة True. وعلى جهاز بلا سطح مكتب تفاعلي يُعيد الحوار المشروط نتيجة إلغاء ويفشل التصدير بهدوء. اضبط ShowDialog على False قبل استدعاء Export فيُصيَّر التقرير بصمت. ولدى ReportBuilder المفاتيح الموازية — AllowPrintToFile وShowPrintDialog — أما QuickReport، الذي يقود كل شيء عبر Prepare، فلا يحتاج إلى أي كبت للحوارات على الإطلاق

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // ابنِ الصفحات أولًا
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // بلا واجهة: تخطَّ الحوار المشروط، لا إلغاء
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

ثلاثة محركات، ثلاثة أنظمة إحداثيات

يُجري كل جسر حسابه الإحداثي الخاص، لأن كل محرك يقيس العالم بطريقة مختلفة. تعامل PDFlibFRExport مواضع كائنات FastReport كبكسلات عند 96 PPI وتقيسها بـ 96/25.4 للوصول إلى المليمترات. وتقرأ PDFlibRBDevice أوامر رسم ReportBuilder بأجزاء الألف من المليمتر وتقسم على 1000، وتسجّل نفسها عبر ppRegisterDevice، لذا يكفي ضبط ppReport.DeviceType على 'PDF Library for Delphi' واستدعاء Print لتوجيه الإخراج عبرها. ولا تُجري PDFlibQRExport أي حساب إحداثي على الإطلاق: فصفحة QuickReport المُعدّة هي بالفعل ملف تعريف EMF، لذا يحفظ الجسر كل صفحة إلى دفق ويغذّيها إلى ImportEMFFromStream، تاركًا مسار تصيير سياق الجهاز وملفات التعريف في المكتبة يضع كل حرف رسومي وكل خط. أما جسرا FastReport و ReportBuilder، على النقيض، فيُصدران عناصر رسم متجهي أصلية ونصًا أصليًا

PDF Library for Delphi: تحويلات أنظمة الإحداثيات التي تُجريها جسور تصدير PDF لـ FastReport و ReportBuilder و QuickReport
بكسلات FastReport، وأجزاء الألف من المليمتر في ReportBuilder، وصفحات EMF في QuickReport، يُحوَّل كل منها قبل أن يصل إلى نواة رسم PDF
var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // يُعدّ التقرير ويُلحق كل صفحة في استدعاء واحد؛ الجسر
    // يحوّل EMF كل صفحة إلى PDF عبر ImportEMFFromStream
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

أين تنتهي الأمانة

اعرف الحدود قبل أن تلتزم بسير عمل على هذه الجسور. فمُصدِّر مجموعات البيانات يُسقط الأعمدة الثنائية بصمت، لذا فإن تقريرًا يجب أن يُظهر صورة مضمَّنة يحتاج إلى نهج مختلف عن جدول مباشر. ومسار QuickReport، لأنه يمرّ عبر EMF، يُثبِّت النص كأحرف رسومية متجهية — تبدو الصفحة صحيحة لكنها لا تحمل نصًا قابلًا للتحديد أو البحث ولا بنية PDF موسومة لإمكانية الوصول. ويحتفظ FastReport و ReportBuilder بنص حقيقي، لكن المعالجات المكتوبة بالأنواع تغطي العروض الشائعة — memo و picture و shape و line — وتعود إلى صندوق إحاطة أو تتخطى العروض النادرة، لذا فإن تقريرًا يعتمد على نص منسَّق أو رموز شريطية أو تعبئات متدرجة سيفقد تفاصيل. وليس أي من هذا عيبًا؛ بل هو الحافة الصادقة لطبقة ترجمة

تحذير تشغيلي واحد يعلو على البقية. فلا يُشحن أي من المحركات الثلاثة داخل losLab PDF Library، والجسور مستبعدة عمدًا من البناء الرئيسي — فهي تُجمَّع فقط من مشروع لديه بالفعل FastReport VCL 6.x أو QuickReport 8 أو ReportBuilder 20 على مسار بحثه. وقد بقيت أسطح ملفات التعريف و DrawCommand التي تستهدفها مستقرة عبر عدة إصدارات رئيسية للمحركات، لكن مطابقة الإصدار مسؤوليتك. أصِب ذلك ويغطي المساران الطيف كله، من تفريغ DBGrid عابر إلى فاتورة مصمَّمة. ويُشحن مُصدِّر الجداول وجسور التقارير كلاهما مع losLab PDF Library لـ Delphi و C++Builder