مقال تقني

النسخ عبر مصنّفات العمل وإعادة ربط الصيغ في HotXLS بـDelphi

تنسخ طريقة AddCopy في HotXLS ورقة عمل من مصنّف عمل Excel إلى آخر بتفكيك كل صيغة على تلك الورقة إلى نص بنمط A1 وإعادة ترجمة ذلك النص داخل مصنّف العمل الوجهة، بدلًا من نسخ شجرة الصيغة المُترجَمة مباشرة، لأن مراجع سلاسل المخططات، وفهارس خطوط النص المنسّق، وترقيم الروابط الخارجية كلها تُعيَّن بشكل مستقل داخل كل ملف مصنّف عمل

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

لماذا لا يمكن لـAddCopy مجرد نسخ شجرة الصيغة المُترجَمة؟

لا يمكن لـAddCopy نقل شجرة الصيغة المُترجَمة دون تغيير، لأن صيغة BIFF مُترجَمة ليست نصًا قائمًا بذاته — إنها تسلسل من الرموز، وعدد من تلك الرموز أعداد صحيحة صغيرة تُحل بشكل صحيح فقط داخل مصنّف العمل الذي أنتجها. مرجع ثلاثي الأبعاد مثل Sheet2!A1:A10 لا يحمل الاسم الحرفي Sheet2 بمجرد ترجمته؛ بل يحمل حقلًا يسمّيه معيار BIFF ixti (يحتفظ HotXLS بالقيمة نفسها في شجرته المُترجَمة الخاصة تحت اسم الحقل FExternID)، وهو فهرس في جدول EXTERNSHEET الخاص بمصنّف العمل ذاك، مُرقّم بأيًّا كانت الطريقة التي صادف بها ذلك المصنّف تسجيل أوراقه وكتبه الخارجية. انقل الرمز دون تغيير إلى مصنّف عمل بُني جدول EXTERNSHEET الخاص به بترتيب مختلف ولن يعني الفهرس 3 Sheet2 بعد الآن — سيعني أيًّا كانت الورقة التي تصادف احتلال الفتحة 3 هناك، ولا توجد طريقة لدى Excel لتحديد الخطأ، لأنه بقدر ما يتعلق الأمر بصيغة الملف فإن الصيغة سليمة البنية تمامًا. هذا بالضبط الفشل الذي وُجدت TXLSWorksheets.AddCopy لتجنبه: تُستدعى من مجموعة أوراق أي من مصنّفي العمل في شيفرة Delphi أو C++Builder، وتنسخ ورقة عمل — قيم الخلايا، والصيغ، والمخططات، والتعليقات، والدمج، وإعداد الصفحة، وأكثر — من مصنّف عمل مصدر قد يكون أو لا يكون ذلك الذي تستدعيها عليه، وتُلحق النتيجة بالوجهة تحت اسم تختاره أو نسخة مُميَّزة عن الأصل

var
  Summary, Branch: IXLSWorkbook;   // interface-counted: do not Free
begin
  Summary := TXLSWorkbook.Create;
  Branch := TXLSWorkbook.Create;
  Branch.Open('branch-east.xls');

  // Appends a copy of Branch's first sheet onto Summary, renamed to
  // stay unique inside the destination workbook
  Summary.Sheets.AddCopy(Branch.Sheets[1], 'East Detail');
  Summary.SaveAs('consolidated.xls');
end;

الحل: فكّك إلى نص، أعد الترجمة في الوجهة

يحل HotXLS مشكلة الفهرسة بعدم السماح أبدًا للشجرة المُترجَمة نفسها بعبور حدود مصنّف العمل. لكل خلية صيغة في نسخ عبر مصنّفات العمل، تفكّك AddCopy الصيغة المصدرية إلى نص بنمط A1 نفسه الذي كان ليراه المستخدم في شريط صيغة Excel، ثم تسلّم ذلك النص إلى مصنّف العمل الوجهة، الذي يحلّله مرة أخرى إلى شجرة باستخدام جداوله الخاصة من الصفر — مرجع مؤهل بالورقة مثل Data!D2:D100 ليس سوى سلسلة نصية في تلك النقطة، وتعني السلسلة الشيء نفسه في أي مصنّف عمل، بحيث إذا كانت الوجهة تملك بالفعل ورقة باسم Data يُحل المرجع بشكل صحيح دون أي ترجمة فهرس على الإطلاق، لأنه لم يكن هناك فهرس خام قيد النقل ليُترجَم أصلًا. لا يدفع HotXLS ثمن هذه الرحلة ذهابًا وإيابًا إلا عندما يضطر لذلك: نسخ ورقة داخل المصنّف نفسه يسلك مسارًا أرخص حيث تُستنسَخ الشجرة المُترجَمة ببساطة في الذاكرة، بما أن كل فهرس داخلها صالح بالفعل حيث تبقى، ولا يعمل التفافة النص إلا بمجرد أن تكتشف AddCopy أن المصدر والوجهة مثيلا مصنّف عمل مختلفان حقًا. يستحق الأمر أيضًا الدقة بشأن ما ليست عليه إعادة الكتابة هذه. فهي لا علاقة لها بإزاحة الصف والعمود التي تعمل عند إدراج أو حذف صفوف داخل ورقة واحدة، والتي تغطيها مقالة مرافقة بتفصيل — يعيد ذلك المحرك كتابة نص A1 في مكانه لتتبع خلايا تحركت بضعة صفوف لأعلى أو لأسفل داخل مصنّف عمل واحد، بينما يعمل هذا عندما تغادر صيغة مصنّف العمل الذي ترجمها كليًا، حيث ليست الصفوف المتحركة هي المشكلة بل الترقيم الخاص بمصنّف العمل

// Conceptually, this is what AddCopy does for each formula cell: turn
// the compiled tree back into text using the source workbook's own
// tables, then let the destination workbook parse that text back into
// a tree using its own tables, from scratch
FormulaText := SourceBook.GetUnCompiledFormula(SourceFormula, Row, Col, SourceSheetID);
DestFormula := DestBook.GetCompiledFormula(FormulaText, DestSheetID);

ماذا لو لم تملك الوجهة تلك الورقة، أو ذلك الاسم، بعد؟

لا تنجح إعادة الترجمة في AddCopy إلا عندما يملك مصنّف العمل الوجهة بالفعل كل ما يشير إليه نص الصيغة، والفجوتان اللتان تظهران عمليًا هما ورقة بالاسم نفسه لم تُنسخ بعد في هذه الدفعة، واسم مُعرَّف بنطاق مصنّف العمل لم يوجد أبدًا في الوجهة على الإطلاق. لا يُطلق HotXLS استثناءً عندما تفشل إعادة الترجمة في منتصف نسخ ورقة — يخزّن تعيين Value الخاص بالخلية بصمت نص الصيغة كسلسلة عادية بدلًا من ذلك، نمط فشل متعمد وقابل للفحص لا صامت، بما أن خلية صيغة تُظهر بشكل غير متوقع نصًا حرفيًا مثل =SUM(Q1!B2:B12) بدلًا من رقم محسوب هي الدليل على أن شيئًا ما في النسخ لم يُحل. قبل الاستسلام، تجرّب AddCopy إصلاحًا واحدًا: تجتاز شجرة قواعد الصيغة الفاشلة جامعةً كل معرّف اسم مُعرَّف تمسّه الصيغة، ولكل اسم بنطاق مصنّف العمل موجود في المصدر لكن ليس بعد في الوجهة، تنسخ الاسم عبرها وتعيد ترجمة النص نفسه مرة ثانية. الأسماء بنطاق ورقة تقع خارج ما يمكن لهذا الإصلاح إصلاحه، بما أن اسمًا مرئيًا فقط لصيغ على ورقة واحدة من مصنّف العمل المصدر ليس له فتحة مكافئة يهاجر إليها، وتُترك وجهة تمتلك بالفعل اسمًا بالتهجئة نفسها دون مساس بدلًا من الاستبدال، على افتراض أن اسمًا أنشأه المستدعي مسبقًا عمدًا هو ما يريد احترامه. داخل مصنّف عمل واحد، يجتاز بحث اسم صيغة عبر الأوراق من نطاق الورقة إلى نطاق مصنّف العمل تلقائيًا، وهي الآلية التي تغطيها مقالة الأسماء المُعرَّفة والصيغ عبر الأوراق في HotXLS؛ عبور حدود مصنّف عمل فعلي يزيل تلك الشبكة الأمان كليًا، ويجب حمل الاسم عبرها عمدًا وإلا تدهورت الصيغة التي تعتمد عليه إلى نص

مراجع سلسلة المخطط تحتاج الإصلاح نفسه، لكن مسار شيفرة مختلف

تصطدم سلسلة مخطط HotXLS التي ترسم نطاق خلايا بمشكلة الترقيم نفسها بالضبط التي تصطدم بها صيغة خلية عادية، لأن مرجع نطاق بيانات مخطط هو أيضًا تدفق رموز صيغة مُترجَمة — يسمّي معيار BIFF السجل الذي يحمله BRAI ([MS-XLS] القسم 2.4.51) — لكن لا يمكن لـAddCopy إصلاحه بإعادة استخدام مسار تحميل المخطط العادي، لأن ذلك المسار هو بالضبط ما ينشئ الخلل. عندما يُحلَّل سجل مخطط من القرص في السياق العادي لفتح ملف، تُبنى شجرة صيغته بترجمة البايتات الخام عبر أيًّا كان مثيل الحاسبة الذي يقوم بالتحليل؛ غذِّ بايتات BRAI الخام لمخطط مصدر عبر مُحمِّل السجلات العادي الخاص بمصنّف العمل الوجهة بدلًا من ذلك، وسيُحل ixti المضمَّن في تلك البايتات مقابل جدول EXTERNSHEET الخاص بالوجهة، بحيث تشير السلسلة بصمت إلى أيًّا كانت الورقة التي تحتل تلك الفتحة هناك — الفئة نفسها من الخطأ كنسخ شجرة مُترجَمة لخلية دون تغيير، لكنه أصعب في الملاحظة لأن لا أحد يقرأ صيغ سلسلة المخطط بالطريقة التي يقرأ بها صيغ الخلايا. يتجنب HotXLS الفخ بمسار استنساخ مخصص بدلًا من ذلك: تنسخ TXLSCustomChart.AssignFrom بايتات ترويسة كل سجل مخطط غير المتعلقة بالصيغة حرفيًا، ثم تعيد بناء النطاق المرفق عبر أداة فك الترجمة وإعادة الترجمة نفسها المستخدمة للخلايا العادية، بحيث تُبنى الشجرة الجديدة مقابل جدول EXTERNSHEET الخاص بالوجهة من الصفر بدلًا من إعادة تفسيرها مقابله لاحقًا

مشكلة الترقيم نفسها، فهرس خط واحد في كل مرة

ليس كل رقم محلي لمصنّف العمل داخل مخطط أو خلية نص منسّق صيغة، وفهرس الخط هو الفئة نفسها من المشكلة على نطاق مصغّر. تخزّن مقاطع النص المنسّق، جنبًا إلى جنب مع نوعي سجل مخطط إضافيين يحملان خط تسمية توضيحية أو محور، مرجع خط كعدد صحيح خام في جدول خطوط مصنّف العمل المالك، ولا يعني ذلك الفهرس شيئًا في جدول مصنّف عمل مختلف — يمكنه بسهولة أن يشير إلى نمط خط، أو حجم، أو لون مختلف تمامًا هناك. يحل HotXLS هذا بالقيمة لا بالرقم: يبحث عن خصائص الخط الفعلية عند ذلك الفهرس في الجدول المصدر، ويجد أو ينشئ إدخالًا مطابقًا في جدول خطوط الوجهة، ويعيد كتابة الفهرس المخزَّن ليشير إلى تلك الفتحة الجديدة. خاصية غريبة واحدة في الصيغة تجعل البحث نفسه دقيقًا — يتخطى الفهرس المُرقَّم في الملف الفتحة 4، وهي فجوة ترقيم يوثّقها القسم 2.5.339 من [MS-XLS]، بحيث يتعين على الشيفرة إزاحة الفهرس للأسفل بواحد قبل مقارنة الخطوط وللأعلى بواحد قبل كتابة النتيجة

// The file-numbered font index skips slot 4 (MS-XLS section 2.5.339);
// shift into the in-memory slot, migrate the font by value if the
// destination differs, then shift back before writing the result
if Ifnt >= 5 then
  Dec(Ifnt);
if DestFonts.Key[Ifnt] <> SourceFonts.Key[Ifnt] then
  Ifnt := DestFonts.SetKey(0, SourceFonts.Key[Ifnt]);
if Ifnt >= 4 then
  Inc(Ifnt);

ماذا يحدث لصيغة تشير بالفعل إلى خارج مصنّف العمل؟

صيغة تمتد إلى مصنّف عمل ثالث قبل أن تستدعي AddCopy أصلًا هي الحالة الوحيدة التي لا يمكن لرحلة النص ذهابًا وإيابًا حملها، لأن مفكك صيغة-إلى-نص الخاص بـHotXLS نفسه لا يصنّع عمدًا نص أقواس [Book]Sheet! لمرجع خارجي، ولا يقبل المترجم في الطرف الآخر تلك الصياغة كمدخل أيضًا — لذا فإن هذه الحالة الواحدة تعمل عبر آلية ثانية لا تلمس النص على الإطلاق. عندما يترك إصلاح ترحيل الاسم الموصوف أعلاه خلية كسلسلة نصية بعد، ويكون لمصنّف العمل المصدر اسم ملف حقيقي، تُبدّل AddCopy الاستراتيجية: تنسخ شجرة الصيغة المُترجَمة نفسها نسخًا عميقًا بدلًا من نصها، ثم تسلّم النسخة إلى تمريرة إعادة ربط مخصصة، RebindExternRefsInTree، تجتازها عقدة بعقدة. لكل مرجع نطاق تجده، تحل تلك التمريرة إدخال EXTERNSHEET الخاص بالمصدر مرة أخرى إلى زوج من أسماء الأوراق، وتسجّل، أو تعيد استخدام، إدخالًا مكافئًا في جداول المراجع الخارجية الخاصة بالوجهة، منشئة رابط مصنّف عمل خارجي جديدًا تمامًا إذا لم تشر الوجهة قط إلى ذلك الملف المصدر من قبل

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

ما الذي يكلّفه هذا التصميم

آلية فك الترجمة وإعادة الترجمة في AddCopy ليست مجانية، والتكلفة تستحق التخطيط لها قبل كتابة سكربت لمهمة توحيد كبيرة لا بعده. نسخ ورقة داخل مصنّف العمل نفسه يسلك المسار الرخيص، استنساخ مباشر في الذاكرة للشجرة المُترجَمة، لأن كل فهرس داخلها صالح بالفعل في مصنّف العمل الذي تبقى فيه؛ أما نسخ عبر مصنّفات العمل فيدفع ثمن تحليل حقيقي على كل خلية صيغة بدلًا من ذلك، فك التركيب إلى نص ثم ترجمة ذلك النص مرة أخرى من لا شيء، وبينما لا يستحق الفرق القياس على ورقة بها بضع عشرات من الصيغ، فإن مصنّف عمل مصدر بعشرات الآلاف من خلايا الصيغة، منسوخ كورقة واحدة من بين عشرات في مهمة دفعية، ينبغي أن يتوقع أن تهيمن إعادة الترجمة على وقت التشغيل بدلًا من إدخال/إخراج الملف المحيط به. ترتيب النسخ يهم لسبب ثانٍ يتجاوز السرعة: صيغة تشير إلى ورقة لم تصلها AddCopy بعد في هذه الدفعة تفشل في إعادة ترجمتها للسبب نفسه الذي تفشل به صيغة تشير إلى ورقة غير موجودة حقًا، بحيث ستشهد مهمة تنسخ الورقة B قبل صيغة الورقة A التي تعتمد عليها تلك الصيغة تتدهور تمامًا كما وُصف أعلاه، نص سلسلة أو احتياطي رابط خارجي يشير مرة أخرى إلى الملف المصدر نفسه الذي جاء منه للتو. ولأن كل مصنّف عمل مصدر في دفعة توحيد عادة ما يُؤلَّف بشكل مستقل، يستحق الأمر اختبارًا صريحًا لنمط الفشل الواحد الذي لم يكن بإمكان أي ملف مصدر منفرد أن يحذّرك منه أبدًا — خمسة مصنّفات عمل فرعية يجمع كل منها أرقام فرع نظير يمكن أن تتحد في مرجع دائري حقيقي داخل مصنّف عمل الملخص دون أن يحتوي أي ملف مصدر فردي واحدًا مطلقًا، دورة توجد فقط بمجرد أن تصل كل ورقة إلى المكان نفسه وتعمل إعادة الحساب على المجموعة المُوحَّدة

يُشحن نسخ أوراق العمل عبر مصنّفات العمل كسلوك قياسي لـAddCopy في مكوّن HotXLS لـExcel في Delphi لـDelphi وC++Builder؛ تحمل صفحة المنتج مرجع واجهة برمجة ورقة العمل ومصنّف العمل الكامل، بما في ذلك سلوك المخططات والنص المنسّق والمراجع الخارجية الموصوف هنا