مقال تقني

جولة ذهاب وعودة لـ XLSX دون فقدان للبيانات في دلفي: المظهر (Theme)، و extLst، و calcChain

تم بناء HotXLS، وهي مكتبة Excel الأصلية لدلفي و C++Builder، لجولات ذهاب وعودة لـ XLSX دون فقدان للبيانات: افتح مصنفًا، وغير خلية واحدة، واحفظ، وسيصمد كل من مظهر (theme) العميل المخصص، وكتل الامتداد extLst الخارجية، وسلسلة الحساب. وتجعل ثلاث آليات ذلك يعمل — التخزين المؤقت الحرفي لملف xl/theme/theme1.xml، وإعادة التسلسل القائم على الأحداث لكتل <ext> غير المعروفة، وملف xl/calcChain.xml الجديد والصالح للمواصفات عند كل حفظ لمصنف يحتوي على صيغ

ثلاث آليات خلف رحلة XLSX الذهاب والإياب اللاخسارية في HotXLS داخل Delphi: xl/theme/theme1.xml مخزنة كبايتات خام وتُكتب عائدة متطابقة بايتًا ببايت، وكتل extLst الأجنبية ملتقطة من أحداث XML ومعاد تشغيلها، و calcChain.xml طازج مطابق للمواصفة يُصدر عند كل حفظ صيغ
يسلك كل جزء محفوظ طريقه الخاص عبر الحفظ — بايتات السمة حرفياً، وإعادة تشغيل extLst على مستوى الأحداث، وسلسلة حساب معادة التوليد — بينما تُعاد بنية ورقة العمل XML والأنماط من النموذج

السيناريو الذي يدفع بهذه الآليات الثلاث شائع بشكل محبط. تقوم خدمة الفوترة بتحميل قالب صممه العميل في Excel — مظهر ألوان الشركة، وخطوط المؤشرات (sparklines) في عمود KPI، وقاعدة تنسيق شرطي تمت إضافتها بواسطة إصدار Excel أحدث — وتكتب إجمالي فاتورة واحدة في الخلية B3، وتحفظ. ويفتح العميل النتيجة فيجد ألوان العلامة التجارية قد عادت إلى أزرق Office الافتراضي، واختفت خطوط المؤشرات، ويعرض Excel "إصلاح" الملف. ولم يلمس أي شيء في الكود أيًا من تلك الميزات. ولكن المكتبة فعلت ذلك، ببساطة عن طريق الحفظ

لماذا تفقد ملفات Excel تنسيقها بعد تعديلات المكتبة؟

تفقد ملفات Excel تنسيقها بعد تعديلات المكتبة لأن معظم المكتبات لا تعدل الملف — بل تعيد بناءه. وحزمة .xlsx هي ملف ZIP لأجزاء XML: هي xl/workbook.xml، وملف xl/worksheets/sheetN.xml واحد لكل ورقة، و xl/styles.xml، و xl/theme/theme1.xml، و xl/calcChain.xml، وغيرها. وتحلل المكتبة النموذجية تلك الأجزاء إلى نموذج كائن عند الفتح وتجدد كل جزء من هذا النموذج عند الحفظ. وأي ميزة لا يمثلها النموذج — كمظهر لم يحلله أبدًا، أو كتلة امتداد من إصدار Excel أحدث — ليس لها مكان لتعيش فيه في الذاكرة، وبالتالي فإن الجزء المعاد بناؤه يسقطها صمتًا

وقد توقّعت مواصفة ECMA-376 نصف هذه المشكلة. إذ تُعرّف SpreadsheetML عنصر extLst (مواصفة ECMA-376 الجزء 1، "منطقة تخزين بيانات الميزات المستقبلية"، الفقرة §18.2.10 للعنصر على مستوى المصنف) بوصفه نقطة توسعة مخصصة: تودع المنتِجات الأحدث ميزاتها هناك، ملفوفة كل واحدة في عنصر <ext> يحمل السمة uri التي تعرّف الميزة، ويُفترض في المستهلكات الأقدم أن تحافظ على ما لا تفهمه. وبهذه الطريقة تنتقل خطوط Sparkline وأدوات التقطيع Slicer وأنواع التنسيق الشرطي الأحدث. لذا فإن المكتبة التي تُسقط كتل <ext> غير المعروفة ليست فاقدة للبيانات فحسب — بل تخرق عقد التوافق الأمامي الذي صُمّم التنسيق حوله. والسؤال الذي ينبغي طرحه على أي مكتبة جداول بيانات تقيّمها سؤال فجّ: إذا غيّرتُ خلية واحدة، فما الذي يتغير أيضًا

كيف يحافظ HotXLS على المظهر المخصص بايت ببايت؟

يحافظ HotXLS على مظهر المصنف عن طريق تخزين البايتات الأصلية لملف xl/theme/theme1.xml مؤقتًا عند وقت الفتح وكتابتها حرفيًا عند وقت الحفظ. وجزء المظهر (theme) (مواصفة ECMA-376 الجزء 1، الفقرة §14.2.7) هو لغة DrawingML، وليس SpreadsheetML — مخططات الألوان، مخططات الخطوط، مخططات التنسيق — ولا يوجد سبب يجعل محرك جداول البيانات يمثلها بعمق. وكانت إصدارات HotXLS السابقة تجدد مظهر Office ثابتًا عند كل حفظ، وهو بالضبط فشل "ألوان العلامة التجارية عادت للوراء" المذكور أعلاه؛ ومنذ الإصدار v2.89.46 يتم تخزين مظهر الحزمة المفتوحة خامًا وإعادة إرساله دون مساس، ويتم إنشاء مظهر Office المدمج فقط للمصنفات التي تم إنشاؤها من الصفر. والبايتات الخام هي أقوى ضمان ممكن للدقة: لا تحليل، لا إعادة تسلسل، لا فرصة للانحراف

تفوز النسخة الحرفية عمدًا على الوصول البرمجي للمظهر. وتعرض TXLSXWorkbook الخصائص ThemeMajorFont و ThemeMinorFont بحيث يمكنك اختيار خطوط العناوين والجسم للمصنفات الجديدة، ولكن عندما تم التقاط مظهر حرفي عند الفتح، فلن يكون لواضعي تلك الخصائص أي تأثير على الملف المحفوظ — فلجولة الذهاب والعودة الأولوية. وإذا كنت بحاجة فعليًا إلى تغيير مظهر مصنف موجود، فهذه إشارة لتعديل القالب في برنامج Excel نفسه بدلاً من واجهة برمجة تطبيقات موجهة للبيانات. ولا تحتاج الحالة اليومية إلى أي واجهة برمجة تطبيقات على الإطلاق:

يخزن HotXLS مؤقتًا بايتات xl/theme/theme1.xml الخام عند الفتح ويكتبها عائدة متطابقة بايتًا ببايت عند الحفظ، بينما تعيد مكتبة البناء من النموذج توليد سمة Office افتراضية فتقفز ألوان هوية العميل إلى الوراء
التخزين المؤقت لـ theme1.xml حرفياً لا يحتاج نموذج سمة إطلاقاً، وThemeMajorFont مع ThemeMinorFont ينسقان مصنفات لا تحمل سمة ملتقطة فقط
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('branded-invoice.xlsx');
    Book.Sheets[0].Cells[3, 2].Value := 42750.00;  // التعديل الوحيد
    Book.SaveAs('branded-invoice-out.xlsx');
    // theme1.xml في المخرَج مطابق بايتًا ببايت للمدخل
  finally
    Book.Free;
  end;
end;

ماذا يحدث لكتل extLst غير المعروفة عند الحفظ؟

يلتقط HotXLS كل كتلة <ext> على مستوى ورقة العمل لا يمثلها داخليًا ويعيد تشغيلها في extLst لورقة العمل المحفوظة، بحيث تصمد الميزات المكتوبة بواسطة إصدارات Excel الأحدث خلال جولة الذهاب والعودة سليمة. ومنذ الإصدار v2.131.0، تكون الأجزاء الملتقطة مرئية من خلال خاصية للقراءة فقط RawWorksheetExts، وهي TStringList في كل ورقة عمل XLSX، مما يجعل الضمان قابلاً للتدقيق من كود الاختبار بدلاً من كونه مجرد اعتقاد:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  i: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('from-newer-excel.xlsx');
    Sheet := Book.Sheets[0];
    WriteLn(Format('%d foreign ext block(s) captured',
      [Sheet.RawWorksheetExts.Count]));
    for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
      WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // ألقِ نظرة على كل uri
  finally
    Book.Free;
  end;
end;

تفصيل التنفيذ الجدير بالمعرفة هو أن الالتقاط هو إعادة تسلسل على مستوى الأحداث، وليس نسخة بايتات خام. ولا يكشف قارئ XML المتدفق لـ HotXLS عن إزاحات المصدر، وبالتالي يتم إعادة بناء الشجرة الفرعية غير المعروفة من أحداث Element و Text و EndElement أثناء تدفقها. ويخفي هذا النهج فخًا كلاسيكيًا واحدًا: فالعنصر ذاتي الإغلاق مثل <a/> يطلق فقط حدث Element محددًا كفارغ وليس حدث EndElement أبدًا، وبالتالي فإن أي عداد عمق يتناقص فقط عند EndElement لن يرى الشجرة الفرعية تغلق أبدًا. وإذا قمت بمعالجته، فسيكون الجزء المعاد بناؤه مكافئًا دلاليًا للأصل — حيث يتم تطبيع اقتباسات السمات والنماذج ذاتية الإغلاق، لذا فهو ليس متطابقًا في البايتات، ولكن Excel يقرأ المعنى وليس البايتات. وتجعل خاصيتان لمخرجات Excel نفسها إعادة التشغيل آمنة: يعلن Excel عن سمات xmlns اللازمة على عنصر <ext> أو داخله، بحيث يكون كل جزء ملتقط مكتفيًا ذاتيًا من حيث مساحة الأسماء، وهذا الاكتفاء الذاتي نفسه هو السبب في أن نسخ ورقة عمل داخل أو عبر المصنفات يمكن أن ينقل الكتل الخارجية مع تعيين بسيط لقائمة السلاسل

كتابة calcChain.xml بحيث يثق Excel في صيغك

يكتب HotXLS ملف xl/calcChain.xml (جزء سلسلة الحساب، مواصفة ECMA-376 الجزء 1، الفقرة §12.3.1) عندما يحتوي المصنف المحفوظ على صيغ، ويختار بين ترتيبين. فإذا تم بناء مخطط تبعية الصيغ بالفعل وكان حديثًا — كاستدعائك لـ Recalculate بعد تعديلك الأخير — يتم إرسال السلسلة بالترتيب الطوبولوجي الكامل، التابعين قبل المتبوعين، مع إلحاق أي أعضاء مراجع دائرية في النهاية. وإلا يتم إدراج الخلايا بترتيب المستند. وكلاهما صحيح: تعامل ملاحظات تنفيذ مايكروسوفت للتنسيق، [MS-XLSX]، سلسلة الحساب كتلميح يتحقق منه Excel ويعيد ترتيبه أثناء التحميل، وبالتالي فإن أي قائمة كاملة هي قانونية، ويرفض HotXLS عمدًا فرض بناء مخطط داخل SaveAs — فبناء الحواف هو عملية تربيعية في عدد الخلايا، وهي تكلفة خفية غير مقبولة في حفظ يحتوي على مليون خلية

يصدر HotXLS ملف xl/calcChain.xml بترتيب طوبولوجي عندما تكون Recalculate قد بنيت مخطط الاعتماد، وبترتيب المستند وإلا: يعامل Excel أي سرد كامل كتلميح ويعيد ترتيبه أثناء التحميل
يبقى الترتيبان كلاهما قانونياً لأن Excel يعيد التحقق من السلسلة عند التحميل، ولا يجبر HotXLS أبداً بناءَ المخطط ذا الكلفة التربيعية داخل SaveAs
Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// وبتوفير الآن، يدرج calcChain.xml خلايا المعادلات بترتيب المستند.
// بعد Recalculate يوجد بيان الاعتماديات، لذا نفس التوفير
// يُصدر ترتيبًا طوبولوجيًا كاملًا بدلًا من ذلك:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');

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

أين تنتهي جولة الذهاب والعودة دون فقدان للبيانات

الصداقة تهم أكثر من مجرد علامة تسويقية هنا، لذا تستحق الحدود نفس الأهمية. لا يقوم HotXLS بنسخ الحزمة بأكملها بايت ببايت: فيتم تجديد ملفات XML لورقة العمل، والأنماط، والسلاسل المشتركة، وأجزاء المصنف من النموذج المحلل، وبالتالي تكون المخرجات أمينة دلاليًا ولكنها ليست متطابقة ثنائيًا — فتحمل ترويسات ZIP المحلية وحدها طوابع زمنية جديدة لـ DOS. وتعود أجزاء <ext> الملتقطة مطبعة، كما هو موضح أعلاه. ويتم تجاهل التجاوزات البرمجية لخطوط المظهر عند وجود مظهر حرفي. وشبكة الحفظ لها شبكة محددة: ميزات يمثلها HotXLS داخليًا (خطوط المؤشرات، على سبيل المثال، يتم تحليلها وإعادة كتابتها بدلاً من نسخها بشكل أعمى) بالإضافة إلى محتوى extLst الخارجي بالإضافة إلى الأجزاء المخزنة مؤقتًا حرفيًا. والجزء الذي لم يتم تمثيله ولا يقع داخل نقطة امتداد — كجزء مخصص لوظيفة إضافية غريبة، مثلاً — يقع خارج الآليات الثلاث التي يغطيها هذا المقال، لذا اختبر قوالبك الفعلية بدلاً من الافتراض

يكتمل المشهد بأعمال الحفظ المجاورة. فتمر مشاريع VBA ومراجع المصنفات الخارجية عبر الحفظ بنفس فلسفة "احتفظ بما لا تمثله"، والمغطاة في المقال المصاحب حول حفظ مشاريع VBA والروابط الخارجية، وخصائص المستند في docProps لها واجهة برمجة تطبيقات القراءة والكتابة الخاصة بها بدلاً من إسقاطها صمتًا. وعندما تقيم أي مكتبة جداول بيانات، أجرِ اختبار الخلية الواحدة: افتح مصنف إنتاج غني بالميزات، وغير قيمة واحدة، واحفظ، وقارن الأجزاء غير المضغوطة بالأصل. وما تغير خارج الورقة التي لمستها يخبرك عن المكتبة أكثر من أي مصفوفة ميزات

تشحن آليات جولة الذهاب والعودة الموضحة هنا — الاحتفاظ بالمظهر منذ الإصدار v2.89.46، والتقاط extLst الخارجي وإرسال calcChain.xml منذ الإصدار v2.131.0 — في مكون HotXLS لدلفي و Excel الحالي، وتوثق صفحة منتجه مجموعة ميزات قراءة وكتابة XLSX الكاملة لدلفي و C++Builder