مقال تقني

صلاحية مخطط حقول Pivot في XLSX مع HotXLS لـ Delphi

يكتب HotXLS تعريفات جداول Pivot في XLSX بحيث تتحقق عناصر pivotField وcacheField صلاحيتها مقابل مخطط ECMA-376 الجزء 1 §18.10: سمات المحور تستخدم رموز ST_Axis وهي axisRow وaxisCol وaxisPage، وحقول منطقة القيم تحمل dataField="1"، وقوائم العناصر لا تكون فارغة أبدًا، وحقول المخزن تخزن numFmtId رقميًا. ومنذ v2.384.33 يحترم القارئ أيضًا القيم الافتراضية للمخطط التي كان يسيء قراءتها

تتشارك العيوب وراء هذا التطهير صفة لا تُحمد: ولا واحدة منها أبطلت اختبارًا قط. يكتب HotXLS جدول Pivot، ويقرأه HotXLS، وكل حقل يهبط على محوره الصحيح، وبقي جناح الذهاب والإياب أخضر لسنوات. المشكلة أن الكاتب والقارئ كانا اتفقا بصمت على لهجة خاصة بهما. جدول Pivot مبني من Delphi بدا سليمًا للمكوّن الذي صنعه، بينما كشفت المقارنة مع CT_PivotField وCT_CacheField رموز تعداد غير صالحة، وعنصرًا فارغًا يحرمه المخطط، وأعلامًا تتوقعها Excel ولا تحصل عليها. إن كنت تولد جداول Pivot على خادم وتشحنها إلى أشخاص يفتحونها في Excel أو يغذون بها محللاتهم الخاصة، فالعقد الوحيد العدّ هو المخطط، لا ما يغفره قارؤك أنت مصادفةً

لماذا لم تمسك دورات HotXLS الذهاب والإياب رموز المحور الخاطئة قط؟

لم تمسكها لأن القارئ كان يقبل الصيغتين. كان XlsxPivotAxisAttr القديم يصدر axis="rowAxis" وcolAxis وpageAxis، وهي تُقرأ طبيعيةً بالإنجليزية لكنها غير موجودة في المخطط؛ يعرف ST_Axis أربع قيم بالضبط هي axisRow وaxisCol وaxisPage وaxisValues. وفي الوقت نفسه كان PivotAxisFromToken في lxPivotXml.pas يطابق رمز المخطط والرمز المخترع معًا، فنجحت كل اختبارات الذات. يصدر الكاتب الآن رموز المخطط وحدها، ويواصل القارئ قبول الصيغ القديمة حتى تظل الملفات المحفوظة بإصدارات HotXLS السابقة تُحمَّل بتخطيطها سليمًا

<!-- قبل v2.384.33: قيمة ST_Axis غير صالحة، وCT_Items فارغ -->
<pivotField axis="rowAxis" defaultSubtotal="1"><items count="0"></items></pivotField>

<!-- منذ v2.384.33 -->
<pivotField axis="axisRow" defaultSubtotal="1">
  <items count="4"><item x="0"/><item x="1"/><item x="2" h="1"/><item t="default"/></items>
</pivotField>
XML لحقل pivotField في HotXLS قبل v2.384.33 وبعدها حيث تنتهك قيمة المحور المخترعة rowAxis وعنصر items الفارغ عقدة CT_PivotField حتى يصدر الكاتب رموز ST_Axis مثل axisRow مع عناصر حقيقية وعلم إخفاء محفوظ ومجموع فرعي ختامي يقبله المخطط
قَبِل القارئ المتساهل الصيغتين معًا، فنجحت كل دورة ذهاب وإياب بينما كان الملف يُسقط أي فحص صارم للمخطط — اكتب رموز ST_Axis الأربعة وحدها ودع CT_Items تحمل عنصرًا واحدًا على الأقل

ماذا يشترط CT_PivotField وكان الكاتب القديم يتفاداه؟

يشترط CT_PivotField ثلاثة أمور كان BuildPivotTableXml القديم يتفاداها أو يخطئ فيها. أولها أن الحقل المُجمَّع في منطقة القيم يجب أن يقول ذلك في تعريفه الخاص عبر dataField="1"؛ يضبط الكاتب الآن ذلك العلم على كل حقل تحيل إليه إحدى مدخلات DataFields، لا في قائمة <dataFields> وحدها. وثانيها أن CT_Items يحتاج item واحدًا على الأقل، فالحقل بلا عناصر لم يعد ينال <items count="0"> فارغة بل يُهمَل العنصر كله ببساطة. وثالثها أن كل عنصر يحفظ حالته: h="1" لعنصر مخفي (TXLSPivotItem.IsHidden) وsd="0" لتفاصيل مطوية (IsDetailHidden)، وكلاهما كان الكاتب القديم يرميه في كل حفظ

الجزء الدقيق هو عناصر المجموع الفرعي الختامية. حين يكون للحقل عناصر، تسرد Excel عنصر item إضافيًا لكل دالة مجموع فرعي بعد عناصر البيانات، مطبوعًا بنوع ST_ItemType: <item t="default"/> للمجموع التلقائي، ثم sum وcountA وavg وmax وmin وproduct وcount وstdDev وstdDevP وvar وvarP للدوال الصريحة. يشتق HotXLS تلك المدخلات من TXLSPivotField.Subtotals وقت الحفظ ويحسبها داخل items count. الحقول المنشأة عبر AddPivotTable تبدأ بمجموعة Subtotals فارغة، وهي تكتب defaultSubtotal="0" بلا عنصر ختامي، فاطلب المجاميع الفرعية صراحةً حين يحتاجها التقرير. وانتبه لفخ التسمية: xlpsCount يقابل countA (كل المدخلات) وxlpsCountNums يقابل count (الأعداد وحدها)

تشريح قائمة عناصر Pivot في HotXLS حيث تتبع مدخلات عناصر البيانات عناصرَ مجاميع فرعية ختامية مشتقة من TXLSPivotField.Subtotals مثل t=default وt=avg وتحسب داخل items count، مع توضيح فخ التسمية بين xlpsCount الذي يقابل countA وxlpsCountNums الذي يقابل count
حقول AddPivotTable تبدأ بمجموعة Subtotals فارغة تكتب defaultSubtotal=0 بلا عنصر ختامي — اطلب الدوال التي تريدها وسيشتق الكاتب عنصرًا واحدًا لكل دالة داخل العدّ
uses
  lxHandleX, lxPivot;

var
  Book  : TXLSXWorkbook;
  Sheet : TXLSXWorksheet;
  Pivot : TXLSPivotTable;
  Region: TXLSPivotField;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('orders.xlsx');
    Sheet := Book.Sheets[1];                  // يبدأ من الواحد، مثل محرك XLS
    Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 3, 6, 'RegionTotals');
    if Pivot = nil then
      raise Exception.Create('Bad source range or anchor');

    Region := Pivot.AddRowField('Region');    // nil إن لم يوجد حقل كهذا
    if Region <> nil then
      Region.Subtotals := [xlpsDefault, xlpsAverage];  // -> t="default"، t="avg"
    Pivot.AddColumnField('Quarter');
    Pivot.AddDataFieldByName('Revenue', xlpaSum);      // يوسم Revenue بـ dataField="1"

    Book.SaveAs('orders-pivot.xlsx');
  finally
    Book.Free;
  end;
end;

كيف يقرأ HotXLS عناصر المجاميع الفرعية وقيم المخطط الافتراضية الآن؟

يتجاوز قارئ HotXLS الآن أي item تحمل سمة t قيمةً موجودة غير data، لأن مدخلات المجموع الفرعي والمجموع الكلي والفراغ لا تحمل فهرس مخزن. قبل v2.384.34 كانت تلك المدخلات تُحمَّل عناصر عادية بقيمة CacheItemIndex تساوي -1، فكان جدول Pivot من صنع Excel يعود بأعضاء شبحية لا تشير إلى شيء، وعلى أي كود يجوب Items أن يرشحها يدويًا. وبما أن الكاتب يعيد بناء المدخلات الختامية من Subtotals، فمهمة القارئ أن يترجمها إلى تلك المجموعة، لا أن يبقيها بيانات

الإصلاح الثاني في القارئ يخص السمات الغائبة. في المخطط، قيمة defaultSubtotal على CT_PivotField وقيمة containsString على CT_SharedItems تؤولان سلفًا إلى true، وتحذفهما Excel عندما تحملان ذلك الافتراض. كان HotXLS يقرأ السمة الغائبة بوصفها false، فأضاع كل جدول Pivot حفظته Excel مجموعه الفرعي الافتراضي بصمت عند التحميل، وصُنّف حقل مخزن نصي صرف بوصفه مختلطًا بدل نصي. هذه هي الصورة المرآتية لعيب المحور: كاتب يتهجّى كل سمة صراحةً لا يجرب مسار الافتراض قط، فلا يكشفه إلا ملفات من منتج آخر

لماذا كانت numFmtId="General" غير صالحة على حقول المخزن؟

كانت القيمة numFmtId="General" غير صالحة لأن ST_NumFmtId عدد صحيح غير مأشور، لا اسم تنسيق. كان كاتب المخزن القديم يبرمج ذلك النص ثابتًا على كل cacheField، مستعيرًا الاسم الذي يراه المستخدمون في مربع حوار Format Cells. يكتب HotXLS الآن خاصية NumberFormat لحقل المخزن عددًا، وهي 0 (تنسيق General المدمج) ما لم يضبطها أحد. المحلل الصارم الذي يطبع السمات من المخطط يرفض القيمة القديمة رفضًا قاطعًا، وذلك بالضبط صنف الإخفاق الذي يتحول إلى مربع حوار إصلاح؛ تغطي مقالة قواعد OPC والترميز الكامنة وراء مطالبة الإصلاح في Excel كيف تُستدعى تلك الحوارات

لماذا بُترت جداول Pivot أسفل الصف 65535؟

بُترت جداول Pivot في XLSX الموضوعة عند الصف 65536 أو بعده لأن نموذج Pivot المشترك كان يخزن FirstRow وLastRow وFirstHeaderRow وFirstDataRow ونظيراتها للأعمدة بنوع Word، وكان كود إزاحة الصفوف يقيّدها بـ Min(.., High(Word)). هذا بقايا سجل SxView في BIFF8 حيث تكفي 16 بت، لكن ورقة XLSX تمتد إلى 1,048,576 صفًا. ومنذ v2.384.37 صارت تلك الخصائص على TXLSPivotTable من نوع Integer، وأزيلت القيود، ولا يضيّق القيم إلا كاتب BIFF8. تعيد TXLSXWorksheet.AddPivotTable وAddPivotTableCopy الآن nil لمرساة خارج 1..1048576 في 1..16384، أو لنسخة يمتد نطاقها خارج الشبكة

مرساة Pivot في HotXLS عند الصف 70001 مقابل سقف 16 بت حيث خُزنت FirstRow وLastRow بنوع Word وقُيدت بـ Min مقابل High(Word) عند 65535، فبُترت الجداول عند الخط أو بعده حتى نقل v2.384.37 النموذج إلى حقول Integer مع إعادة nil خارج الشبكة
حقول Word كانت بقايا سجل SxView من BIFF8 في صيغة تمتد أوراقها إلى 1048576 صفًا — كانت المرساة بعد الصف 65536 تلتف داخل المدى 16-بت وتفقد جدولها عند الحفظ
var
  Pivot: TXLSPivotTable;
  Check: TXLSXWorkbook;
begin
  // كان الصف 70001 يلتف داخل المدى 16-بت؛ وينجو الآن من الحفظ والتحميل
  Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 70001, 1, 'LateTotals');
  if Pivot = nil then
    Exit;  // مرساة خارج الورقة أو مدى مصدر لا يُحسم
  Pivot.AddRowField('Region');
  Pivot.AddDataFieldByName('Revenue', xlpaSum);
  Book.SaveAs('late.xlsx');

  Check := TXLSXWorkbook.Create;
  try
    Check.Open('late.xlsx');
    Pivot := Check.Sheets[1].PivotTables.FindByName('LateTotals');
    Assert((Pivot <> nil) and (Pivot.FirstRow = 70001));
  finally
    Check.Free;
  end;
end;

نال محرك XLS الكلاسيكي إصلاحه الموافق في v2.384.38. كان نموذجه يخزن قيم SxView وDConRef الخام القائمة على الصفر ويمرر مراسي AddPivotTable مباشرة، بينما الوثائق والعروض النموذجية ومحرك XLSX كلها استخدمت خلايا قائمة على الواحد مثل Cells[Row, Col]. يبقي كلا المحركين الآن المواضع القائمة على الواحد في النموذج، ويضيف قارئ BIFF8 واحدًا ويطرح الكاتب واحدًا عند حد السجل، فالكود الذي كان يرسو عند (0, 0) عليه الانتقال إلى (1, 1)، لأن AddPivotTable الكلاسيكي يعيد الآن nil لمرساة خارج 1..65536 في 1..256؛ الاستدعاء الجديد يكتب البايتات نفسها التي كان يكتبها القديم. تخطيط السجل نفسه لم يتغير وهو مشروح في مقالة سجلات SX في BIFF8 الكامنة وراء جداول Pivot في .xls الكلاسيكي

تحقق من الصلاحية مقابل المخطط لا مقابل قارئك أنت

تعمم الدرس خارج جداول Pivot: القارئ المتساهل يخفي تجاوزات الكاتب، فالدورة عبر كودك أنت تثبت الاتساق لا الصحة. نجا كل عيب هنا لأن الطرف المتسامح والطرف المعطوب سكنا في المكتبة نفسها. الفحوص التي تمسك هذا الصنف من العيوب فعلًا هي: تحقق مخطط للأجزاء المولدة، وملفات أنتجتها Excel تُمرَّر عبر قارئك مع حذف السمات عند قيمها الافتراضية، وثوابت اختبار تثبّت الرمز الدقيق بدل النتيجة المحللة. جداول Pivot التي تبنيها عبر الـ API، بما فيها الحقول المحسوبة والعناصر المحسوبة وتخطيطات percent-of-total المعروضة في بناء جداول Pivot في XLSX وتحديثها بحقول محسوبة، تنال XML المصحح دون أي تغيير في الكود، بينما تواصل الجداول المحملة من ملفات Excel إعادة تشغيل أجزائها الأصلية حتى تعدلها

تُشحن كل هذه الإصلاحات في مكوّن HotXLS لجداول Delphi الحالي، الذي يقرأ ويكتب XLS وXLSX وجداول Pivot من Delphi وC++Builder دون Excel أو أتمتة COM على الجهاز