مقال تقني

مقطّعات جداول Excel في Delphi: رحلة ذهاب وإياب مع HotXLS

ينشئ HotXLS مقطّعات جداول Excel من Delphi وC++Builder باستدعاء واحد — Worksheet.Slicers.Add(Table, ColumnName) — ويكتب المواضع الثمانية كلها في الحزمة التي يحتاجها المقطّع: جزءا slicer وslicerCache، وتجاوزا نوع المحتوى، وعلاقتا ورقة العمل والمصنف مع كتلتي extLst المطابقتين، ومرساة الرسم

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

لماذا يلمس المقطّع الواحد ثمانية مواضع في الحزمة؟

لأن المقطّع ثلاثة مفاهيم منفصلة يفرضها OPC في ثلاثة بيوت منفصلة: حالة الفلتر (على نطاق المصنف)، والعرض (على نطاق ورقة العمل)، والشكل (على نطاق الرسم). يكتب HotXLS ثماني نقاط هبوط بالضبط لكل مقطّع جدول، وكل واحدة منها حاملة للأحمال

  • xl/slicers/slicerN.xml — جزء واحد لكل ورقة عمل، يحمل كل عرض <slicer> على تلك الورقة في مساحة الأسماء http://schemas.microsoft.com/office/spreadsheetml/2009/9/main
  • xl/slicerCaches/slicerCacheN.xml — على مستوى المصنف، واحد لكل عمود جدول مقطَّع، يحمل الربط الفعلي
  • عنصر <Override> في [Content_Types].xml لكل من الجزأين، باستخدام application/vnd.ms-excel.slicer+xml وapplication/vnd.ms-excel.slicerCache+xml (يتطلب ECMA-376 الجزء 2 نوع محتوى لكل جزء، وترفض Excel الحزمة من دونهما)
  • علاقة ورقة عمل من النوع .../office/2007/relationships/slicer، إضافة إلى امتداد <x14:slicerList> داخل extLst الخاص بورقة العمل يشير إلى ذلك الـ rid
  • علاقة مصنف من النوع .../office/2007/relationships/slicerCache، إضافة إلى امتداد <x15:slicerCaches> داخل extLst الخاص بالمصنف يسرد نفس الـ rids
  • مرساة mc:AlternateContent في رسم الورقة تمنح المقطّع موضعه وحجمه
  • كتلة <x15:tableSlicerCache> داخل جزء المخزن المؤقت تربطه بمعرّف جدول ومعرّف عمود جدول

تستخدم كتل الامتداد آلية extLst القياسية في ECMA-376 الجزء 1 §18.2.10: كل عنصر <ext> يحمل سمة uri تعرف الميزة، والقرّاء الذين لا يتعرفون على GUID يتخطون الكتلة. يميز HotXLS مقطّعات الجداول من مقطّعات pivot عبر ذلك GUID وحده — {3A4CF648-6AED-40f4-86FF-DC5316D8AED3} لـ slicerList الخاص بالجداول، و{A8765BA9-456A-4dab-B4F3-ACF838C121DE} لذلك الخاص بـ pivot، وبالمقابل {46BE6895-7355-4a93-B00E-2C351335B9C9} و{BBE1A952-AA13-448e-AADC-164F8A28A991} لامتدادي slicerCaches في المصنف. وربط الجدول نفسه يعيش تحت {2F2917AC-EB37-4324-AD4E-5DD8C200BD13}. تلك الثوابت مكشوفة في الوحدة بأسماء XlsxExtUriSlicerListX15 وXlsxExtUriWbSlicerCachesX15 وXlsxExtUriTableSlicerCache، فلا يحمل مسار الكتابة أي سلسلة سحرية

المواضع الثمانية في الحزمة التي يكتبها HotXLS لمقطّع جدول واحد أُنشئ بـ Worksheet.Slicers.Add: جزءا slicer وslicerCache، وتجاوزا نوع محتوى، وعلاقتا ورقة عمل ومصنف، وكتلتا extLst باسمي x14:slicerList وx15:slicerCaches، ومرساة الرسم، وكل منها حامل للأحمال حين تتحقق Excel من الحزمة
استدعاء Worksheet.Slicers.Add واحد يتفرع إلى ثماني نقاط هبوط عبر الحزمة: جزءان، وتجاوزا نوع محتوى، وعلاقتان، وكتلتا extLst ومرساة رسم واحدة، كلها تُتحقق كوحدة حين تفتح Excel الملف

أي الأجزاء يمكن إعادة تشغيلها حرفياً وأيها يجب إعادة بنائه؟

يجيب HotXLS عن هذا بقاعدة واحدة: قد يعيد الجزء تشغيل بايتاته الأصلية فقط إن لم يكن أي شيء يشير إليه قابلاً لإعادة الترقيم أثناء الحفظ. مخازن مقطّعات الجداول المؤقتة تفشل في ذلك الاختبار، لذا يعيد HotXLS بناءها من النموذج عند كل حفظ، دون شروط — حتى حين لم يلمس المستخدم المقطّع إطلاقاً. السبب هو tableId: عنصر <x15:tableSlicerCache> يعرف جدوله عبر سمة id في <table>، ويعيد HotXLS ترقيم معرّفات الجداول على مستوى المصنف كله حين يكتب. تنفيذ مبكر كان يخزن جزء المخزن المؤقت كبايتات خام عند الفتح ويدفعها للخارج عند الحفظ، وهي الاستراتيجية الصحيحة لمعظم الأجزاء المعتمة. أنتج ملفات تفتح بنظافة في الحالة الشائعة وتشغّل إصلاحاً في الحالة المثيرة: أضف جدولاً في ورقة سابقة، احفظ، فتشير المخازن الآن إلى معرّف جدول يخص شيئاً آخر، أو لا يخص شيئاً أصلاً. العيب غير مرئي في اختبار فتح-وحفظ عادي لأن المعرّفات لا تتحرك مصادفة

مخازن pivot المؤقتة تسلك الطريق المعاكس. حمولتها <pivotTables> مع كتلة <x14:data> تصف العناصر المخزنة وحالة الفرز وفهارس العناصر — بنية لا إعادة ترقيم فيها من جهة HotXLS، وبنية مكلفة أن تُنمذج بأمانة. تُبقى تلك كبايتات خام وتُعاد بدقة، مع تمييز TXLSXSlicerCache.IsPivot وIsTableCache بين النوعين. عروض المقطّع تتبع سياسة ثالثة، بتتبع الوسخ: عنصر <slicer> غير الملموس ومرساة رسمه يُعادان حرفياً، ولحظة تعديل أي عرض على الورقة يُعاد توليد جزء الورقة كاملاً من النموذج. هذا هو الشكل العام للحكم — إعادة تشغيل حرفية حيث تكون المراجع مستقرة، وإعادة بناء من النموذج حيث يعيد الكاتب تعيين الهوية، وتتبع وسخ حيث يقبل الاثنان — وهو ينطبق أبعد من المقطّعات بكثير، كما توضح ملاحظات رحلة الذهاب والإياب بلا فقدان حول السمات وكتل extLst وcalcChain بعمق أكبر

سياسة إعادة التشغيل لكل جزء مقطّع في HotXLS: مخازن مقطّعات الجداول المؤقتة تُعاد بناؤها دائماً من النموذج لأن قيم tableId تُرقَّم على مستوى المصنف كله عند الحفظ، ومخازن pivot تعيد بايتاتها الخام لأن pivotTables وx14:data لا تُرقَّم أبداً، وعروض المقطّع بتتبع الوسخ، تُعاد حرفياً حتى يتغير أي عرض على الورقة
سياسة إعادة التشغيل لكل جزء: مخازن مقطّعات الجداول تُعاد بناؤها دائماً لأن معرّفات الجداول تُرقَّم على مستوى المصنف، ومخازن pivot تعيد البايتات الخام لأن شيئاً فيها لا يُرقَّم، والعروض تتبع الوسخ فيبقى الحفظ غير الملموس نظيف البايتات

إنشاء مقطّع جدول من Delphi

TXLSXSlicers.Add هو واجهة الإنشاء كلها. يسلّم TXLSXTable واسماً لعمود فيجسّد معرّفات أعمدة الجدول، ويجد أو ينشئ TXLSXSlicerCache الداعم، ويحجز الاسم المعرف المطابق، ويرسي عرضاً إلى يمين الجدول. النموذج المحمّل بوسيطين يختار الموضع؛ والنموذج ذو الوسائط الستة يأخذ صندوقاً صريحاً يبدأ من 1 وشاملاً

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Slicer: TXLSXSlicer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('sales.xlsx');
    Sheet := Book.Sheets[0];

    // الموضع الافتراضي: على بُعد عمودين من الجدول،
    // بارتفاع 13 صفًا عند ارتفاع الصف الافتراضي 19pt
    Slicer := Sheet.Slicers.Add(Sheet.Tables[0], 'Region');
    Slicer.Caption   := 'Sales region';
    Slicer.StyleName := 'SlicerStyleLight1';
    Slicer.ColumnCount := 2;

    Book.SaveAs('sales-sliced.xlsx');
  finally
    Book.Free;
  end;
end;

قاعدتا تسمية جديرتان بالمعرفة لأنهما اصطلاحا Excel لا اختراعا HotXLS. يُسمى المخزن Slicer_<column> مع طي الأحرف غير الصالحة للمعرّفات إلى شرطات سفلية، وتلحق التصادمات لاحقة رقمية — Slicer_Region ثم Slicer_Region1 وهكذا. وكل اسم مخزن محجوز أيضاً كاسم معرف على مستوى المصنف تكون صيغته #N/A، وهذا ما يفعله TXLSXSlicers.ReserveCacheDefinedName. تنشئ Excel ذلك الاسم بنفسها، وهذا مهم: أسماء المخازن والأسماء المعرفة تتشارك مساحة أسماء واحدة، فمصنف يحوي اسماً يدعى Slicer_Region سيتصادم لولا ذلك عند الحفظ التالي. إن بنيت الجداول والتحقق معاً، فتفاعلات الأسماء المعرفة في مقال التحقق من البيانات وAutoFilter والجداول تغطي الحالات المجاورة. والمشاركة تتبع نفس قاعدة Excel. يُستشار TXLSXSlicerCaches.FindForTableColumn(ATableId, ATableColumnId) قبل إنشاء مخزن جديد، لذا عرضا مقطّع فوق نفس عمود الجدول — واحد على ورقة لوحة القيادة وواحد على ورقة التفاصيل — يتشاركان جزء مخزن واحداً ويبقيان متزامني الفلترة. وحذف عرض يشغّل الفحص العكسي عبر كل ورقة في المصنف ولا يُسقط المخزن مع اسمه المحجوز #N/A إلا حين يختفي آخر عرض يشير إليه

كيف تبقى معرّفات العلاقات مستقرة في الطرفين؟

تبقى مستقرة لأن HotXLS يحسب rid المقطّع بإعادة تشغيل الترتيب الدقيق الذي يصدر به كل علاقة أخرى، ويُلحق العلاقات الجديدة في الذيل. لا شيء موجود في الملف يتحرك. يبدو هذا مبالغة في الهندسة حتى تصحح أخطاء البديل. معرّفات العلاقات في ملف .rels لورقة عمل يُشار إليها من داخل جزء الورقة — عبر الروابط التشعبية والرسم والتعليقات وأجزاء الجداول — ويحلها ECMA-376 الجزء 2 موضعياً داخل غراف العلاقات، لا بأي هوية مستقرة. إن أدخل الكاتب علاقة المقطّع في الوسط، انزاحت كل مرجعية بعده بمقدار واحد، وبدأت الورقة تشير بهدوء إلى الجزء الخطأ. لذا يمشي XlsxWorksheetSlicerRelNumber السلسلة الحتمية كاملة — الروابط التشعبية الخارجية، ثم VML التعليقات، والتعليقات، وصور الترويسات والتذييلات، والتعليقات المتسلسلة، والرسم، وخلفية الورقة، وإعدادات الطابعة، والجداول، والجداول المحورية، وجداول الاستعلام، وخلايا الجداول المفردة — وعندها فقط يدّعي الرقم التالي للمقطّع. ويفعل XlsxWorkbookSlicerCacheFirstRid الشيء نفسه في جانب المصنف: الأوراق، وsharedStrings، والأنماط، وVBA، والروابط الخارجية، والسمة، والاتصالات، ومخازن pivot، والشخص، وcalcChain، وبيانات الورقة الوصفية، وخرائط XML، وXML المخصص، ثم شوط slicerCache. المشكلة العامة محلولة بالتفصيل في نص حل العلاقات OPC. ثم يُبنى امتداد جانب المصنف من تلك الأرقام بواسطة XlsxBuildWorkbookSlicerCachesExtXml، وإن كان الملف المصدر يحوي كتل امتداد slicerCaches تُجرَّد من extLst الخام أولاً، فيعيد النموذج إصدارها مقابل الـ rids الحالية بدلاً من ترك نسخة عتيقة خلفه

كيف يبقي HotXLS معرّفات علاقات المقطّع مستقرة: يمشي XlsxWorksheetSlicerRelNumber السلسلة الحتمية لورقة العمل من الروابط التشعبية الخارجية عبر الرسم حتى خلايا الجداول المفردة ويُلحق علاقة المقطّع في الذيل، بينما يفعل XlsxWorkbookSlicerCacheFirstRid الشيء نفسه في جانب المصنف، فلا يزيح الإدراج أي مرجع rid موجود
تُحل معرّفات العلاقات موضعياً، لذا تُلحق علاقة المقطّع في ذيل ترتيب الإصدار الحتمي على الجانبين — الإدراج في الوسط كان سيُزيح كل مرجع لاحق ويوجه الورقة إلى الجزء الخطأ

مرساة AlternateContent وقيمتها الاحتياطية القديمة

مرساة الرسم هي الموضع الوحيد الذي يصبح فيه ترميز المقطّع غريباً فعلاً، والغرابة مقصودة. يصدر HotXLS الشكل داخل عنصر mc:AlternateContent وفق ECMA-376 الجزء 3، مع فرع Choice تأخذه Excel 2013 وما بعدها وفرع Fallback لكل ما هو أقدم

// ما ينتجه XlsxSlicerAnchorPayloadXml هيكليًا:
//
//   <mc:AlternateContent>
//     <mc:Choice xmlns:sle15=".../drawing/2012/slicer" Requires="sle15">
//       <xdr:graphicFrame>
//         <a:graphicData uri=".../drawing/2010/slicer">
//           <sle:slicer xmlns:sle=".../drawing/2010/slicer" name="Region"/>
//         </a:graphicData>
//       </xdr:graphicFrame>
//     </mc:Choice>
//     <mc:Fallback><xdr:sp>...notice shape...</xdr:sp></mc:Fallback>
//   </mc:AlternateContent>

انظر إلى مساحتي الأسماء. رمز Requires="sle15" يحل إلى مساحة أسماء مقطّعات 2012، وهي بوابة الإصدار — قارئ لا يفهم مقطّعات 2012 يجب أن يتخطى فرع Choice كله. لكن graphicData/@uri وعنصر sle:slicer داخلها يستخدمان مساحة أسماء 2010، لأنها المعرّف على جانب الرسم الذي استخدمه إطار الرسم دائماً. قلب هذه على عكسها ينتج ملفاً تفتحه Excel بلا اعتراض ثم تعرضه كإطار فارغ، وهو عرض أعراض لا يُنسى في عدم نفعه. فرع Fallback يحمل مستطيلاً بسيطاً بنص يوضح أن مقطّعات الجداول غير مدعومة في هذا الإصدار — هذا ما يرسمه قارئ حقبة 2007 مكان العنصر، وهو سبب قدرة Excel قديمة على تمرير المصنف في رحلة ذهاب وإياب دون تدمير أي شيء. يكشف TXLSXSlicer الهندسة كصندوق يبدأ من 1 وشامل عبر SetBounds(ARow1, ACol1, ARow2, ACol2) وخصائص FromRow / ToCol الفردية، إضافة إلى AnchorType لسلوك الحركة والحجم مع الخلايا المعتاد. HasAnchor يساوي False في حالة واحدة محددة: مقطّع متداخل داخل شكل مجموعة. تلك تُعاد بمرساة مجموعتها حرفياً، ولا يركّب HotXLS لهما مرساة ثانية عمداً، لأن فعل ذلك سيصدر الشكل مرتين

تحرير مقطّع موجود

var
  Slicer: TXLSXSlicer;
  Cache: TXLSXSlicerCache;
begin
  Slicer := Sheet.Slicers.FindByName('Region');
  if Slicer <> nil then
  begin
    Slicer.SetBounds(2, 8, 16, 11);      // 1-based, inclusive
    Slicer.ShowCaption := 1;             // -1 = attribute omitted
    Slicer.LockedPosition := 1;
    Slicer.RowHeightEmu := 19 * XlsxEmuPerPoint;   // 241300 EMU

    Cache := Slicer.Cache;
    if Cache <> nil then
    begin
      Cache.SortOrder   := 'descending';
      Cache.CrossFilter := 'showItemsWithNoData';
    end;
  end;

  // إزالة آخر عرض فوق عمود تُسقط ذاكرة التخزين المؤقت الخاصة به
  // والاسم المعرف المحجوز #N/A معه
  Sheet.Slicers.Delete(Sheet.Slicers.IndexByName('Region'));

خصائص الأعداد الصحيحة ثلاثية الحالة تستحق ملاحظة، لأنها تبدو حادثة نمذجة وهي ليست كذلك. ShowCaption وLockedPosition وColumnCount وStartItem وCustomListSort كلها تستخدم -1 لتعني أن السمة كانت غائبة في الملف المصدر ويجب أن تبقى غائبة عند الكتابة، مع 0 و1 كقيمتين صريحتين. تعامل Excel الغائب والصريح الافتراضي بشكل مختلف في مواضع قليلة، والحفاظ على ذلك التمييز هو ما يبقي فرق البايتات للحفظ غير الملموس نظيفاً. RowHeight وColumnWidth يُحفظان كسلاسل خام لسبب ذي صلة: تكتب Excel كسوراً عشرية في سمتَي EMU تلك، وإعادة تحليلها عبر عدد صحيح ستخسر الدقة في رحلة الذهاب والإياب. RowHeightEmu هو الوصول الملائم الصحيح فوق نفس التخزين، مع XlsxSlicerDefaultRowHeight عند 241300 EMU — 19 نقطة بنسبة XlsxEmuPerPoint البالغة 12700

أين يتوقف هذا

حدّان صادقان. أولاً، ينمذج HotXLS مقطّع الجدول بالكامل لكنه لا ينمذج إنشاء مقطّع pivot — يمكنك فتح مصنف يحوي مقطّعات pivot وقراءة هويتها وإبقاءها سليمة عبر الحفظ وحذفها، لكن جانب المخزن يُعاد كبايتات ولا توجد واجهة لبناء واحد من الصفر. ثانياً، لا يفلتر المقطّع أي شيء بنفسه. إنه واجهة فوق عمود جدول، وحالة الصفوف المرئية التي يقودها من جهة Excel؛ يكتب HotXLS العنصر والربط، لا نتيجة فلتر محسوبة. إن احتجت صفوفاً مفلترة فعلاً في الملف المحفوظ، فعيّن AutoFilter للجدول أيضاً. أي سمة لا ينمذجها HotXLS على <slicer> أو <x15:tableSlicerCache> تُمرَّر دون تغيير، لذا فمقطّع جاء من بناء Excel أحدث ما زال يُعاد تسلسله بسماته غير المألوفة سليمة حتى بعد نقله. تلك هي الخاصية التي تجعل مسار إعادة البناء القذر آمناً: إعادة البناء من النموذج مقبولة فقط إن كان النموذج مجموعة فائقة لما حلله. آلية الامتداد الموسومة بـ GUID من ECMA-376 الجزء 1 §18.2.10 وتعريفات أجزاء المقطّع في [MS-XLSX] هما ما يجعلان ذلك الضمان قابلًا للتعبيـر أصلاً

مقطّعات الجداول تأتي في HotXLS Delphi Component، الذي يقرأ ويكتب مصنفات XLSX وXLSM وBIFF القديمة من Delphi 5 حتى الإصدار الحالي، إضافة إلى C++Builder وFree Pascal، دون تثبيت Excel ودون OLE. مرجع API الكامل ونسخة تجريبية على صفحة منتج HotXLS Delphi Spreadsheet Component