تحويل نتيجة استعلام إلى تقرير Excel هو في الواقع ثلاث مشكلات ترتدي معطفًا واحدًا. كل نوع حقل في Delphi يجب أن يهبط في خلية بنوع Excel الصحيح، وصف الترويسة يجب أن يُقرأ كتقرير لا كتفريغ لمخطط قاعدة بيانات، والأرقام والتواريخ والمبالغ المالية يجب أن تحمل تنسيقات تنجو من الرحلة. تخطَّ أيًا من هذه، وسيظل الملف يُفتح، ويبدو معقولًا، ثم يفشل في اللحظة التي يحدد فيها مستخدم من قسم المالية عمودًا وينتظر مجموعًا لا يظهر أبدًا. القيم كُتبت كنص، وExcel يعاملها كتسميات، ولم يُثَر أي استثناء لينبّهك
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 يمنحانك النطاق الذي كُتب للتو لمعالجته لاحقًا
أين يتوقف المكوّن
ثلاثة قيود مبنية داخل 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 تحوّل المصنف الممتلئ باستدعاء واحد:
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