مقال تقني

HotXLS Delphi Component: database export to spreadsheet reports في Delphi

تعامل النسخة المحدثة أنواع الحقول بوصفها عقد التصدير الحقيقي، وتفصل مسار TDataToXLS في تطبيقات VCL عن الحلقة اليدوية للخدمات والمهام الدفعية. كما تحدد أين يتوقف المكوّن وكيفية إنتاج .xlsx والتحقق منه

تحويل نتيجة استعلام إلى تقرير Excel هو في الواقع ثلاث مشكلات ترتدي معطفًا واحدًا. كل نوع حقل في Delphi يجب أن يهبط في خلية بنوع Excel الصحيح، وصف الترويسة يجب أن يُقرأ كتقرير لا كتفريغ لمخطط قاعدة بيانات، والأرقام والتواريخ والمبالغ المالية يجب أن تحمل تنسيقات تنجو من الرحلة. تخطَّ أيًا من هذه، وسيظل الملف يُفتح، ويبدو معقولًا، ثم يفشل في اللحظة التي يحدد فيها مستخدم من قسم المالية عمودًا وينتظر مجموعًا لا يظهر أبدًا. القيم كُتبت كنص، وExcel يعاملها كتسميات، ولم يُثَر أي استثناء لينبّهك

مخطط لمسارَي تصدير من HotXLS من TDataset في Delphi: مكوّن TDataToXLS من VCL يكتب ملفات BIFF8 وحلقة TXLSXWorkbook مكتوبة يدويًا لـ XLSX
TDataToXLS هو مسار الاستدعاء الواحد لأدوات VCL المكتبية الكاتبة لـ .xls، بينما تخدم حلقة TXLSXWorkbook المكتوبة يدوياً الوظائف غير المراقبة وـ .xlsx الأصلي

HotXLS مكتبة جداول بيانات Object Pascal أصلية تكتب ملفات XLS وXLSX مباشرة من Delphi وC++Builder، دون أي أتمتة لـExcel. تقدّم مسارين من TDataset إلى مصنف: مكوّن TDataToXLS الجاهز للاستخدام المباشر، وحلقة مكتوبة يدويًا مقابل API المصنف. هذان المساران غير قابلين للتبادل. المكوّن كائن VCL بالمعنى الكامل مبني على واجهة XLS، لذا فإن الاختيار الصحيح يعتمد على مكان تشغيل الكود وصيغة الملف التي يتوقعها المستهلك. فيما يلي المساران كلاهما، والحد الذي يتوقف عنده المكوّن عن كونه الأداة الصحيحة، وكيفية إبقاء أنواع الحقول سليمة أيًا كان اختيارك

أنواع الحقول هي العقد الحقيقي للتصدير

قبل أي استدعاء API، قرّر كيف يهبط كل نوع حقل في Delphi داخل خلية. الخلية التي تستقبل سلسلة نصية من Delphi تبقى سلسلة نصية. HotXLS لا يخمّن أن '1,234.50' كان يُقصد بها رقم، ولا ينبغي له ذلك، لأن إعادة التحليل المعتمدة على الإعداد المحلي هي بالضبط الطريقة التي تتحول بها الفاصلة العشرية الألمانية إلى فاصل آلاف على خادم إنجليزي. النمط الموثوق هو الإسناد عبر أدوات الوصول المكتوبة النوع: AsFloat أو AsCurrency للحقول الرقمية، وAsDateTime للتواريخ حتى تحمل الخلية رقمًا تسلسليًا حقيقيًا لتاريخ Excel بدلًا من سلسلة منسّقة، وAsString فقط للحقول النصية فعلًا

معالجة القيم الفارغة تستحق قرارًا صريحًا لا افتراضًا ضمنيًا. تحويل قيمة حقل عبر VarToStr يحوّل NULL في SQL إلى سلسلة فارغة، وهي خلية نصية، بينما تخطي الإسناد يترك الخلية فارغة حقًا، وهذا ما تتوقعه AVERAGE وCOUNT ومستهلكو الجداول المحورية. بالنسبة لأعمدة المبالغ المالية، قرّر قبل كتابة الحلقة ما إذا كانت NULL تعني صفرًا أم قيمة غير معروفة. الحالتان تبدوان متطابقتين بمجرد أن ينسّق أحدهم العمود، والفرق يغيّر كل مجموع محسوب لاحقًا

مسار المكوّن: TDataToXLS في تطبيقات VCL

لتطبيق VCL كلاسيكي مع استعلام موصول بالفعل بوحدة بيانات، TDataToXLS هو المسار بنداء واحد. فهو يجتاز أي سليل لـTDataset، سواء كان FireDAC أو ADO أو IBX أو أي شيء آخر يطبّق واجهة مجموعة البيانات المجردة، وينتج ورقة عمل منسّقة بعناوين ترويسة وخطوط وحدود ومجاميع فرعية اختيارية للمجموعات وتقسيمًا تلقائيًا للأوراق عند نتائج كبيرة

var
  Exporter: TDataToXLS;
begin
  Exporter := TDataToXLS.Create(nil);
  try
    Exporter.Dataset := OrdersQuery;          // أي سليل لـ TDataset
    Exporter.WorksheetName := 'Orders';
    Exporter.HeaderSource := hsDisplayLabel;  // عناوين ترويسة، لا أسماء أعمدة خام
    Exporter.GroupFields.Add('CustomerID');   // كتلة مجموع فرعي لكل عميل
    Exporter.RowsPerSheet := 50000;           // ابقَ تحت سقف صفوف BIFF8
    Exporter.VisibleFieldsOnly := True;             // احترم Field.Visible
    Exporter.SaveDatasetAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

خاصيتان تحملان معظم ثقل الإنتاج هنا. HeaderSource := hsDisplayLabel تكتب DisplayLabel الخاص بكل حقل بدلًا من اسم عمود SQL الخام، فيكتب المصنف "Customer Name" بدلًا من CUST_NM. أما RowsPerSheet فموجودة لأن المكوّن يكتب BIFF8، الذي تتوقف شبكته عند 65,536 صفًا في 256 عمودًا؛ ضبطها على 50,000 يقسّم نتيجة كبيرة عبر أوراق قبل أن يقصّها سقف الصيغة. أما المظهر فيُدار عبر خصائص HeaderFont وDetailFont وGroupColor ونمط الحدود، ومجموعة DisableFormat تعطّل فئات تنسيق كاملة عندما يريد المستهلك خلايا بسيطة. ولأي أمر مخصص، حدثا AfterCell وAfterRow يمنحانك النطاق الذي كُتب للتو لمعالجته لاحقًا

مخطط يقابل مستخلصات حقول مجموعات البيانات في Delphi بأنواع خلايا Excel بـ HotXLS، قابلاً معالجة VarToStr لـ NULL بخلية فارغة حقيقية
عقد التصدير هو نوع الحقل: تُهبط الموصولات الموصوفة النوع الأعداد والتواريخ قيم Excel حقيقية، بينما يحوّل VarToStr بصمت SQL NULL إلى خلية نصية

أين يتوقف المكوّن

ثلاثة قيود مبنية داخل TDataToXLS، ومعرفتها مسبقًا تجنبك إعادة تصميم محرجة بعد فوجين من التطوير

  • إنه مكوّن VCL بالمعنى الكامل. وحدته تستورد Forms وControls وDialogs، لذا فإن ربطه في مهمة سطر أوامر أو خدمة Windows يسحب VCL كاملة إلى الملف الثنائي. وحدات المصنف الأساسية لا تحمل مثل هذا الاعتماد. فهي تحتاج فقط Windows وClasses وSysUtils وVariants، ولهذا السبب ينبغي أن يستخدم كود جهة الخادم الحلقة المبيّنة أدناه بدلًا من ذلك
  • إنه مبني على واجهة XLS. يملأ المكوّن IXLSWorkbook ويكتب ‎.xls‎ (BIFF8). لا توجد خاصية تحوّله إلى خرج OOXML
  • أحداثه تتحدث لهجة XLS. المعامل Cell: IXLSRange في AfterCell ينتمي إلى نموذج كائن XLS، فأي تخصيص لكل خلية يُكتب هناك هو كود بأسلوب XLS حتى لو حُوِّل الملف لاحقًا إلى ‎.xlsx

إنتاج ‎.xlsx من خرج المكوّن

عندما يصرّ المستهلك على ‎.xlsx‎ لكن منطق التصدير يعيش بالفعل داخل TDataToXLS، فإن دالة الجسر في وحدة lxXlsxExport تحوّل المصنف الممتلئ باستدعاء واحد:

مخطط يقابل وحدات VCL التي تسحبها TDataToXLS إلى ملف تنفيذي في Delphi بوحدات RTL الأربع التي يحتاجها كود مصنف HotXLS الأساسي
ربط TDataToXLS في خدمة يجرّ Forms وControls وDialogs معه، بينما تحتاج وحدات المصنف الأساسية Windows وClasses وSysUtils وVariants فقط
uses lxXlsxExport;

Exporter.SaveDatasetAs('orders.xls');
// المكوّن يكشف IXLSWorkbook الذي ملأه
SaveXLSWorkbookAsXLSX(Exporter.Workbook, 'orders.xlsx');

عامل الجسر كناقل بيانات جدولية، لا كمحوّل كامل الدقة. فهو ينسخ القيم والصيغ وتنسيقات الأرقام وألوان التعبئة وسمات الخطوط وعروض الأعمدة وإعدادات العرض. وهو عمدًا لا ينسخ الحدود أو النطاقات المدمجة أو التعليقات أو المخططات أو التنسيقات الشرطية. لشبكة مسطّحة من ترويسة وصفوف، هذا كافٍ تمامًا. أما لتقرير منسّق فليس كافيًا، والإصلاح الصادق هو توليد XLSX مباشرة بدلًا من ترقيع الملف المحوّل

الحلقة المكتوبة يدويًا للخدمات والمهام الدفعية

كود جهة الخادم ينبغي أن يستهدف TXLSXWorkbook مباشرة. لاحظ فرق دورة الحياة بين الواجهتين قبل نسخ أي عيّنة. TXLSWorkbook في جهة XLS تُحمل عبر واجهة معدودة مرجعيًا ويجب ألا تُحرَّر يدويًا، بينما TXLSXWorkbook صنف عادي يتطلب try..finally Free. خلط الاصطلاحين طريقة موثوقة لصنع تسرب ذاكرة أو تحرير مزدوج

procedure ExportOrders(Q: TDataSet; const FileName: string);
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Row: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Orders');
    Sheet.Cells[1, 1].Value := 'Order No';
    Sheet.Cells[1, 2].Value := 'Customer';
    Sheet.Cells[1, 3].Value := 'Ordered';
    Sheet.Cells[1, 4].Value := 'Amount';

    Row := 2;
    Q.First;
    while not Q.Eof do
    begin
      Sheet.Cells[Row, 1].Value := Q.FieldByName('OrderNo').AsInteger;
      Sheet.Cells[Row, 2].Value := Q.FieldByName('Customer').AsString;
      if not Q.FieldByName('Ordered').IsNull then
        Sheet.Cells[Row, 3].Value := Q.FieldByName('Ordered').AsDateTime;
      Sheet.Cells[Row, 4].Value := Q.FieldByName('Amount').AsFloat;
      Inc(Row);
      Q.Next;
    end;

    Book.StreamingWrite := True;  // بث XML الخاص بالورقة مباشرة إلى الأرشيف المضغوط
    Book.SaveAs(FileName);
  finally
    Book.Free;
  end;
end;

الأسطر المهمة هي الإسنادات المكتوبة النوع وحارس IsNull. التواريخ تصل كأرقام تسلسلية، والمبالغ تصل كأعداد ذات فاصلة عائمة، وتواريخ الطلبات NULL تبقى فارغة حقًا بدلًا من أن تصبح سلاسل فارغة. StreamingWrite := True تغيّر مسار الحفظ فقط: XML الخاص بورقة العمل يُبث مباشرة إلى حاوية الأرشيف المضغوط بدلًا من أن يُجمَّع كسلسلة نصية كبيرة واحدة أولًا، وهذا يسطّح ذروة استهلاك الذاكرة عند SaveAs لأعداد صفوف بست خانات. لكل طريقة حفظ أيضًا حمولة زائدة (overload) من نوع TStream، لذا يمكن للمصنف أن يذهب مباشرة إلى استجابة HTTP دون لمس القرص. مقالة الكتابة المتدفقة والمهام الدفعية تشرح نمط النشر ذاك، ومقالة أداء المصنفات الكبيرة تغطي ما ينبغي فعله عندما يستمر عدد الصفوف بالتصاعد

هذه الحلقة هي أيضًا المسار الذي يتوسع عبر الخيوط. كلا المحركين كاتبان أصليان بلغة Object Pascal، تدفقات سجلات BIFF8 من جهة وأرشيف OOXML مضغوط مع XML من الجهة الأخرى، لذا لا يلمس أي جزء من التصدير أتمتة COM ولا يحتاج ترخيص Excel على الخادم. ما يمنحك إياه ذلك هو التوازي دون عنق زجاجة من نسخة واحدة، شريطة أن يبني كل خيط مصنفه الخاص. كائنات المصنف ليست آمنة لتعدد الخيوط في الاستخدام المشترك، لذا فالقاعدة هي نسخة واحدة لكل تصدير، وليست نسخة مشتركة محمية بقفل أبدًا

ثمة حد واحد يستحق معرفته قبل أن تصمم حوله. شبكة XLSX تتوقف عند 1,048,576 صفًا في 16,384 عمودًا، لذا فإن تقسيم الأوراق الذي تتولاه RowsPerSheet على جهة XLS نادرًا ما يُحتاج إليه هنا. مصنف بمليون صف نادرًا ما يكون ما يريده مستهلك بشري أيضًا. عندما تكون مجموعة النتائج بهذا الحجم فعلًا، فإن ملفًا محدَّدًا بفاصل عادة ما يكون العقد الأفضل، ومقالة تصدير CSV وTSV تغطي الفواصل وسلوك BOM وتحذير تقييم الصيغ الذي ينطبق هناك

اختيار نقطة البداية

إذا كان التصدير يعيش داخل أداة سطح مكتب VCL وخرج ‎.xls‎ مقبول، ابدأ بـTDataToXLS ودعمه للتجميع. إنه أقل كود، والجسر عبر SaveXLSWorkbookAsXLSX موجود عندما يطلب أحدهم لاحقًا ‎.xlsx‎، طالما تقبل حدود الدقة الموصوفة أعلاه. أما إذا كان الكود يعمل دون إشراف، أو كان المستهلك يتطلب ‎.xlsx‎ من البداية، فاكتب الحلقة. كلا المسارين يأتي مع مشاريع تجريبية عاملة وهما جزء من حزمة HotXLS Delphi Component