مقال تقني

مجموعات المحور الثانوي لمخططات BIFF8 في Delphi مع HotXLS

تكتب HotXLS مجموعات المحور الثانوي في مخطط BIFF8 بإصدار كتلة AxisParent ثانية، لا بإلحاق مجموعة مخطط ثانية بعد المحاور. ففي تدفق مخطط XLS التقليدي تعيش كل مجموعة مخطط — ChartFormat وسجل نوع المخطط وCrtLink — داخل كتلة مجموعة محاور خاصة بها، وترتبط كل سلسلة بواحدة منها عبر SerToCrt. اعكس هذا التداخل، ولن توجد مجموعة مخطط ثانية تستطيع سلسلة الارتباط بها، مهما أصدرت من سجلات محاور

لماذا لا ترتبط مجموعة مخطط ثانية بعد المحاور بأي شيء؟

القواعد النحوية هي الإجابة كلها، وهي سطر واحد من ABNF. تقول قاعدة CHARTFOMATS في [MS-XLS] 2.1.7.20.1: AxesUsed 1*2AXISPARENT، ثم تفصل AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. اقرأ الإنتاجين معًا، وستظهر البنية: مجموعات المخطط أبناء لمجموعة المحاور وليست أشقاء لها. وتدفق المخطط الذي يحتوي على زوجي محاور ومجموعة مخطط لاحقة ليس مخطط محورين مع غرابة تخطيط، بل مخطط ذو مجموعة مخطط واحدة ومجموعة من سجلات المحاور اليتيمة. وهذا مهم لأن SerToCrt ($1045)، الموجود داخل كتلة SERIESFORMAT، يحمل فهرس مجموعة المخطط الذي يبدأ من الصفر لا فهرس محور. فإذا كتبت crt = 1 بينما لا توجد إلا كتلة CRT واحدة، فأنت توجه السلسلة إلى مجموعة مخطط لم تُصدر قط. والحدس الذي يوقع الناس هو اسم السجل: تبدو AXESUSED ($1046) كأنها تعد المحاور، ولذلك تكون الخطوة الطبيعية التالية إصدار محاور أكثر. لكنها تعد مجموعات المحاور، وكل مجموعة محاور تجر معها مساحة رسم كاملة ومجموعة مخطط

وَسم سلسلة لمجموعة المحور الثانوي

في جانب HotXLS يختزل الأمر إلى Boolean واحدة. يحمل TXLSChartSeriesInfo حقل SecondaryAxis، وضبطه على أي سلسلة في المصفوفة التي تسلمها إلى TXLSWorksheets.AddChartSheet يحول المنشئ كله إلى وضع المجموعتين. ولا يوجد استدعاء منفصل لتفعيل المحور الثانوي ولا معامل لعدد المحاور، لأن العدد قابل للاشتقاق: فإذا أرادت أي سلسلة المجموعة الثانوية، احتاج المخطط إلى مجموعتين

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';
    // ... املأ A1:C12 بالفئات والإيراد والهامش ...

    Series[0] := Default(TXLSChartSeriesInfo);   // لا تستخدم FillChar لهذا السجل أبدًا
    Series[0].Name := 'Revenue';
    Series[0].Categories := 'Data!$A$1:$A$12';
    Series[0].Values := 'Data!$B$1:$B$12';

    Series[1] := Default(TXLSChartSeriesInfo);
    Series[1].Name := 'Margin';
    Series[1].Categories := 'Data!$A$1:$A$12';
    Series[1].Values := 'Data!$C$1:$C$12';
    Series[1].SecondaryAxis := True;             // تصبح AXESUSED مساوية لـ2

    Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
      'Revenue vs Margin', '', '', Series);
    Wb.SaveAs('dual-axis.xls', xlExcel97);
  finally
    Wb.Free;
  end;
end;

ليس سطر Default(TXLSChartSeriesInfo) زينة. فـTXLSChartSeriesInfo يخلط حقولًا مُدارة، مثل أسماء WideString ومصفوفات trendline وerror-bar الديناميكية، مع أعضاء Boolean عاديين، ولا يضمن Delphi إلا مسح الحقول المدارة نيابة عنك. اترك SecondaryAxis من دون تهيئة، وسيكون ما كان موجودًا على المكدس، وهو ما يعني عمليًا أن الملف الثنائي نفسه ينتج مخطط محور واحد عند تشغيله من مضيف وحدة تحكم ومخطط محورين تحت مشغل الاختبار. أما نطاقا الفئات والقيم فيُحلان عبر جدول EXTERNSHEET للمصنف قبل أن يراهما المنشئ، وهي آلية الفهارس نفسها التي تغطيها كيفية تصنيف HotXLS لروابط BIFF SupBook وXTI الخارجية، ولذلك يتحول النطاق الذي يسمي ورقة غير معروفة إلى عنصر نائب فارغ من نوع BRAI بدل فشل البناء

ما الذي تصدره HotXLS عندما تكون السلسلة ثانوية؟

يغير المُصدّر شكله لا قيمة واحدة فقط. ومن دون سلسلة ثانوية تحمل AXESUSED القيمة 1، أو 0 في pie و3D pie اللذين لا يملكان مجموعات محاور أصلًا، وتتبعها كتلة AxisParent واحدة. ومع وجود سلسلة ثانوية تحمل AXESUSED القيمة 2، ويشغّل المنشئ الكتلة مرتين، مع ضبط iax — الكلمة الأولى من حمولة AxisParent ذات 18 بايتًا ($1041) — على 0 ثم 1. وتصدر كل تمريرة Pos ومحور الفئات ومحور القيم (Axis، $101D) وعلامة PlotArea ($1035) وFrame افتراضيًا، ثم ChartFormat ($1014) وسجل نوع المخطط وCrtLink ($1022) وعلامتي End لإغلاق مجموعة المخطط ومجموعة المحاور. ثم ترتبط السلسلة الثانوية مع SerToCrt crt = 1، بينما تحافظ الأساسية على crt = 0. وهناك سجل واحد لا يتكرر عمدًا: إذ يُصدر legend في المجموعة الأولى فقط، لأن Excel يمنح المخطط legend واحدة مهما بلغ عدد مجموعات المحاور التي يحملها. ويستحق خاصيتان أخريان الذكر بوضوح. فلم يغير إعادة هيكلة المُصدّر لوضع مجموعة المخطط داخل كتلة AxisParent الناتج للمخططات العادية — إذ يكون التدفق الفرعي مطابقًا للبايتات في الإصدار السابق عند عدم وجود سلسلة ثانوية، لأن تهيئة AddAxisParent بالمعامل iax = 0 هي مسار الكود القديم نفسه. كما يواصل المنشئ إصدار زوج محاور كامل لكل مجموعة، ولذلك تصل المجموعة الثانوية دائمًا بمحور فئات خاص بها حتى عندما لا تهتم إلا بمقياس قيمها

كيف يستعيد فحص المخطط ارتباط مجموعة المحاور؟

تتم القراءة في تمريرتين فوق قائمة السجلات، ولا بد من ذلك لأن AXESUSED يصل قبل الكتل التي يصفها. تبحث التمريرة الأولى عن $1046 فقط وتقرأ كلمته الأولى باعتبارها عدد مجموعات المحاور. تبدأ القيمة من 1 وترتفع ولا تنخفض أبدًا: إذ تأخذ HotXLS أكبر القيمة الحالية والقيمة المعلنة، ولذلك لا يستطيع AXESUSED المشوه أو المكرر إرجاع مخطط عُرف مسبقًا أنه يعلن مجموعتين إلى الوراء. وتتتبع التمريرة الثانية مجموعة المحاور الحالية، وتحدثها عند كل AxisParent، ثم تضع ذلك الفهرس على كل سجل Axis تقابله حتى ظهور AxisParent التالي

var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] هي ورقة البيانات، وSheets[2] هي ورقة المخطط
  Model := Wb.Sheets[2]._Chart.GetChartModel;
  try
    if Model.AxisGroupCount = 2 then
      Writeln('AXESUSED declares a secondary axis group');
    for i := 0 to Model.AxisCount - 1 do
      Writeln('axis ', i, ' group ', Model.GetAxis(i).AxisGroup);
    for i := 0 to Model.SeriesCount - 1 do
      Writeln('series ', i, ' chart group ', Model.GetSeries(i).ChartGroup);
  finally
    Model.Free;
  end;
end;

هناك حدان يستحقان التسمية. إذ يبلغ TXLSChartModel.AxisGroupCount ما يعلنه الملف لا عدد كتل AxisParent التي عُثر عليها فعلًا؛ فالملف الذي يقول 2 ويشحن كتلة واحدة سيبلغ 2، وتكون AxisCount هي موضع ملاحظة ذلك. كما أن TXLSChartAxis.AxisGroup ختم موضعي: يسجل الكتلة التي قُرئ المحور داخلها، وهو الشيء الوحيد الذي تخبرك به الصيغة. أما على جانب السلاسل، فيُفك ترميز SerToCrt فقط عند الوجود داخل كتلة Series، لأن معرّف السجل نفسه يظهر في سياقات لا يكون فيها ربط سلسلة، وسيستبدل مفكك غير مقيّد قيمة السلسلة الخطأ بسعادة

التحقق من المحاور الثانوية من دون ملف Excel حقيقي

لم يحتج التحقق هنا إلى ملف Excel ذي محور ثانوي، وهذا هو الجزء المفيد من القصة. فالفك البنيوي خاصية لتسلسل السجلات، ولذلك تثبت سلسلة مصطنعة ذلك بدقة سلسلة ملتقطة. يبني اختبار التراجع AXESUSED بحمولة 2، ثم كتلتَي AxisParent تحتوي كل منهما على محور فئات ومحور قيم، ويتأكد من عودة النموذج مع AxisGroupCount = 2 وأربعة محاور مختومة بالقيم 0 و0 و1 و1، وأنواع المحاور المتوقعة على الزوج الثاني

// تحقق بنيوي من دون إشراك أي ملف Excel على الإطلاق
Chart := TXLSCustomChart.Create(nil, $0600);
try
  AddWordRecord($1046, [2]);   // AXESUSED: مجموعتا محاور
  AddAxisParentGroup(0);       // AxisParent iax=0 + Begin + محوران Axis + End
  AddAxisParentGroup(1);       // AxisParent iax=1 + Begin + محوران Axis + End

  Model := Chart.GetChartModel;
  Assert.AreEqual(2, Model.AxisGroupCount);
  Assert.AreEqual(4, Model.AxisCount);
  Assert.AreEqual(0, Model.GetAxis(1).AxisGroup);
  Assert.AreEqual(1, Model.GetAxis(2).AxisGroup);
finally
  Model.Free;
  Chart.Free;
end;

هناك ملاحظتان عمليتان إذا أنشأت سجلات بنفسك. إذ يفك TXLSCustomChart.AddData(RecID, Len, nil) مرجع الحمولة عندما تكون Len غير صفرية، ولذلك يجب إضافة علامتي Begin ($1033) وEnd ($1034) بطول صفر، لا عبر blob بقيمة nil وطول قديم. كما تثبت السلسلة المصطنعة المفكك لا قبول Excel لناتجك — فقد بُني جانب الكتابة من ABNF، ثم فُحص عبر إعادة المرور على GetChartModel مع التأكيد على أختام المحاور 0/0/1/1 ومجموعات المخطط للسلاسل 0/1، وكان المسار المطابق للبايتات عند عدم وجود ثانوي شبكة الأمان. وهذا هو الموقف المحافظ نفسه الذي بُني عليه باقي دعم HotXLS للمخططات والصور والرسومات في Delphi: فك ما تقوله السجلات، وارفض تخمين تخطيط ثنائي لم تقرأه في المواصفة. وقد شغلت مجموعة Delphi الكاملة 1650 اختبارًا من أصل 1650 على Win32 وWin64 بعد وصول جانب الكتابة

معاملات مشهد Chart3d وفخ fAuto

هناك تفصيلان متجاوران يضران بمن يتجاوز المخطط الافتراضي. الأول هو Chart3d ($103A، [MS-XLS] 2.4.46)، وهو حمولة مسطحة من 14 بايتًا تُصدر داخل مجموعة المخطط للأنواع ثلاثية الأبعاد: anRot (الدوران من 0 إلى 360)، وanElev (الارتفاع، موقّع، من -90 إلى 90)، وpcDist (مسافة المنظور من 0 إلى 100، وتُهمل ما لم يُضبط fPerspective)، وpcHeight وpcDepth (نسبة مئوية من عرض المخطط، من 5 إلى 500)، وpcGap (من 0 إلى 500)، وgrbit بتاتُه هي fPerspective $0001 وfCluster $0002 وfAutoscale $0004 وf3DScaling $0010 وf2DWalls $0020. وتضيف المواصفة قيودًا لن يفرضها تخطيط السجل نيابة عنك: ففي مخطط شريطي مبدل يجب ألا يتجاوز anRot وanElev القيمة 44، وفي مخطط pie يجب ألا يكون anElev سالبًا

والثاني هو بت fAuto، وهو الذي ينتج بلاغ «تم تجاهل ألواني». تحمل LineFormat ($1007) وAreaFormat ($100A) وMarkerFormat ($1009) جميعًا fAuto في البت 0 من grbit، وعندما يُضبط ذلك البت يطبق Excel نمطه التلقائي ويعامل قيم RGB الصريحة ونمط الخط وشكل العلامة وحجمها الموجودة بجانبه كزينة. وأي مُصدّر يكتب نمطًا مخصصًا لسلسلة يجب أن يمسح البت 0؛ أما المُصدّرات الافتراضية فتتركه مضبوطًا تحديدًا حتى يختار Excel لوحة الألوان. وإذا كنت تعدل مصنفًا موجودًا بدل بنائه، فستختلف قواعد الحفاظ مرة أخرى، وتغطيها كيفية تعديل مخططات Excel من دون فقد ChartML المحفوظ

تأتي مجموعات المحاور الثانوية وربط SerToCrt ونموذج المخطط المهيكل الموضحة هنا في مكوّن جداول HotXLS لـDelphi لـDelphi وC++Builder، الذي يقرأ ويكتب مخططات BIFF8 من دون تثبيت Excel؛ وتضم صفحة المنتج المرجع الكامل لسجلات المخططات وقائمة overloads الخاصة بـAddChartSheet