مقالة تقنية

التعليقات المترابطة (Threaded Comments) في ملفات Excel باستخدام دلفي وHotXLS

قم بتوليد مصنف عمل من دلفي، وقم بتعليق خلية باستخدام واجهة برمجة تطبيقات التعليقات الكلاسيكية، وسيقوم Excel 365 بوضع نصك تحت "ملاحظات" (Notes): صندوق أصفر من النمط القديم دون حقل رد، ولا صورة رمزية للكاتب، ولا زر حل. ويكتب HotXLS تعليقات مترابطة حقيقية لـ Excel 365 من خلال AddThreadedComment وAddThreadedReply على محرك XLSX، حيث يصدر جزء xl/threadedComments/threadedCommentN.xml المخصص لكل ورقة ويسجل كل كاتب في جزء الشخص على مستوى مصنف العمل، بحيث تكون المحادثة التي يكتبها كودك هي نفس الكائن الذي يرد عليه المراجع في Excel

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

لماذا تظهر تعليقاتي كملاحظات في Excel 365؟

الجواب القصير: يتشارك جيلان من التعليقات في شبكة واحدة، ويعيشان في أجزاء مختلفة تماماً من الحزمة. فالنموذج القديم — الذي تستهدفه AddComment — يخزن النص في xl/comments1.xml (نوع المحتوى application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml) ويحدد موضع البالون من خلال جزء رسم VML المصاحب xl/drawings/vmlDrawing1.vml. هذا الزوج يسبق شريط الأدوات؛ ولا يزال Excel 365 يقرأه بأمانة، ولكنه يعرض النتيجة كـ "ملاحظة" ولا يقدم واجهة مستخدم للترابط عليها، لأنه لا يوجد شيء في التخزين للترابط. الكاتب هو سلسلة عرض، والموضع هو شكل VML، وهذا هو النموذج بأكمله

التعليقات المترابطة، التي تم إدخالها مع Excel 365، تتجاهل طبقة VML تماماً. وتحمل كل ورقة بها محادثة جزء xl/threadedComments/threadedCommentN.xml الخاص بها تحت نوع المحتوى application/vnd.openxmlformats-officedocument.spreadsheetml.threadedComments+xml، ويحمل مصنف العمل جزءاً واحداً xl/person/person.xml (نوع المحتوى ...spreadsheetml.people+xml) يسرد كل مشارك مرة واحدة. ويكشف HotXLS عن كلا النموذجين في ورقة العمل نفسها: تستمر AddComment في كتابة الزوج القديم، وتكتب AddThreadedComment الأجزاء الجديدة. وإذا ظهرت مخرجاتك كملاحظات، فقد استدعيت واجهة برمجة التطبيقات الأولى وكنت تريد الثانية. ولا يزال النموذج القديم هو الأداة المناسبة لبعض المهام — المقالة السابقة حول تعليقات الخلايا والروابط التشعبية في تدفقات عمل المراجعة تغطي ذلك بالتفصيل، بما في ذلك جانب XLS — ولكن لا شيء فيه يترقى إلى خيط مترابط

ما يكتبه HotXLS في الحزمة

ينمذج HotXLS كل رد ككائن TXLSXThreadedComment يحمل مرساة خلية (Ref)، ومعرفاً بنمط GUID ‏(Id), وحقل ParentId اختيارياً، وAuthorId الذي يتم حله من خلال قائمة الأشخاص، ومرآة DisplayName، وجسم النص، وعلامة Done، وحقل DateTime بنسق ISO-8601. وعند الحفظ، يصبح كل إدخال عنصراً <threadedComment> تحمل سمته t المعرّف، وتحمل سمته dT الطابع الزمني، وتسمي سمته parentT — الموجودة فقط في ردود الأبناء — معرف الأب. ويعيد Excel بناء شجرة المحادثة بمطابقة parentT مع t; لا يوجد تداخل في XML، بل مجرد قائمة مسطحة بالإضافة إلى المراجع

جزء الشخص هو القطعة التي تنساها معظم التطبيقات المصنوعة يدوياً. يجب أن يحل كل authorId في تعليق مترابط إلى إدخال <person> في xl/person/person.xml، موصلاً بعلاقات مصنف العمل، مع تجاوز نوع المحتوى الخاص به — وإلا فلن يكون لدى Excel اسم عرض لإظهاره. ويتعامل HotXLS مع التسجيل تلقائياً: تبحث AddThreadedComment عن الكاتب في قائمة Persons لمصنف العمل عن طريق اسم العرض وتنشئ إدخالاً بمعرف GUID جديد عندما لا يوجد أي منها، بحيث تشترك التعليقات المتتالية لنفس الكاتب في هوية واحدة. وتحصل علاقات ورقة العمل على علاقة threadedComments، وتحصل علاقات مصنف العمل على علاقة الشخص، ويحصل بيان أنواع المحتوى على كلا التجاوزين، دون أن يظهر أي من ذلك في كودك

بناء سلسلة ردود باستخدام AddThreadedComment وAddThreadedReply

تنشئ AddThreadedComment جذر المحادثة: يظل ParentId الخاص بها فارغاً، وهو ما يميزها كقمة الخيط المترابط. وتأخذ AddThreadedReply معاملاً ParentRef — مرجع الخلية حيث يعيش الجذر — وتبحث عن الجذر عند تلك المرساة، وتطبع ParentId للرد الجديد بـ Id للجذر. ويعيد كلاهما TXLSXThreadedComment الجديد، بحيث يمكنك تعيين الطابع الزمني أو حالة الحل على الكائن الذي أنشأته للتو:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Root: TXLSXThreadedComment;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('forecast.xlsx');
    Sheet := Book.Sheets[0];

    // Conversation root; ParentId stays empty
    Root := Sheet.AddThreadedComment(8, 3,
      'Q3 figure looks low against the pipeline export', 'Maria Ortiz');
    Root.DateTime := '2026-07-06T09:12:00Z';

    // Replies anchor at the same cell; ParentRef names the root cell
    Sheet.AddThreadedReply(8, 3, Root.Ref,
      'Pipeline export missed the EMEA renewals, re-running', 'Jan Kowalski');
    Sheet.AddThreadedReply(8, 3, Root.Ref,
      'Confirmed, refreshed figure lands tomorrow', 'Maria Ortiz');

    Book.SaveAs('forecast-reviewed.xlsx');
  finally
    Book.Free;
  end;
end;

هناك تفصيلان في هذا الإدراج يستحقان الانتباه. أولاً، يتم تمرير Root.Ref بدلاً من سلسلة نصية مبنية يدوياً: المرجع الذي ولدته المكتبة يضمن مطابقة ما ستبحث عنه AddThreadedReply، مما يزيل فئة كاملة من أخطاء الإرساء بفارق واحد. ثانياً، تم تمرير كلا الكاتبين كأسماء عرض عادية — سجل الشخص، وهويات GUID، وتوصيل authorId حدثت جميعاً خلف الاستدعاءات. افتح الملف المحفوظ في Excel 365 وستجد إرساء الخلية وترتيب الردود والمشاركين المتميزين نشطين: انقر فوق رد وسيقوم Excel بالإلحاق بنفس السلسلة التي بدأها كودك

قراءة محادثة رجوعاً: سلوك الرحلة الدائرية

يقوم HotXLS برحلة دائرية للتعليقات المترابطة اعتباراً من الإصدار 2.122.0: إعادة فتح حزمة محفوظة يعيد بناء قائمة محادثات كل ورقة وقائمة المشاركين في مصنف العمل من الأجزاء، بحيث تنجو المعرفات وروابط الردود وأسماء العرض من دورة قراءة←كتابة كاملة. ويقوم القارئ بتحليل person.xml قبل أي محتوى ورقة، وهو ما يسمح لكل authorId بالحل إلى اسم عرض في وقت التحميل؛ ويتم الاحتفاظ بمعرفات الأشخاص المحللة كما هي بدلاً من تجديدها، بحيث يحافظ مصنف العمل الذي يتنقل بين كودك وExcel على هويات كاتب مستقرة عبر عدد عشوائي من الرحلات الدائرية

var
  i: Integer;
  Cmt: TXLSXThreadedComment;
begin
  Book.Open('forecast-reviewed.xlsx');
  Sheet := Book.Sheets[0];
  for i := 0 to Sheet.ThreadedComments.Count - 1 do
  begin
    Cmt := Sheet.ThreadedComments[i];
    if Cmt.ParentId = '' then
      Writeln('Root at ', Cmt.Ref, ' by ', Cmt.DisplayName, ': ', Cmt.Text)
    else
      Writeln('  reply by ', Cmt.DisplayName, ': ', Cmt.Text);
  end;
  Writeln('Participants: ', Book.Persons.Count);
end;

يظهر تخزين القائمة المسطحة بالإضافة إلى المراجع هنا عمداً: تسرد ThreadedComments الردود بترتيب الجزء، ويحدد ParentId الفارغ كل جذر. وإذا كنت بحاجة إلى شكل الشجرة — على سبيل المثال، لتصدير سجل مراجعة — فقم بالتجمع حسب Ref أولاً ثم ربط ParentId بـ Id داخل مجموعة كل خلية. وهذا السلوك المتمثل في الاحتفاظ بما قمت بتحليله هو حالة واحدة من سياسة أوسع في المكتبة؛ حيث تغطي المقالة حول الرحلة الدائرية الخالية من الفقد للموضوعات وقوائم الامتداد وسلسلة الحساب كيفية تطبيق نفس المبدأ على بقية الحزمة

الحالة التي تم حلها، والطوابع الزمنية، وهوية الكاتب

تحمل ثلاث سمات دلالات المراجعة، وجميعها قابلة للكتابة على الكائنات المعادة. وتتطابق Done مع سمة done — حيث يعرض Excel سلسلة محادثات تم حلها منهارة مع رابط إعادة فتح. وDateTime هو طابع إنشاء UTC بتنسيق ISO-8601 ‏(dT)؛ ويستبدل HotXLS نائباً ثابتاً عندما تتركه فارغاً بحيث يتم التحقق من الجزء دائماً، ولكن الطابع الزمني الحقيقي هو ما يجعل تاريخ الخيط مقروءاً، لذا قم بتعيينه. وعلى جانب الهوية، تكشف كل TXLSXPerson عن ProviderId — عادة None للإدخالات المكتوبة محلياً — وUserId يمكنك ملؤه بـ UPN أو بريد إلكتروني عندما يعرفه تطبيقك:

var
  Root: TXLSXThreadedComment;
  Person: TXLSXPerson;
begin
  Root := Sheet.ThreadedComments.FindAt('D9');
  if Root <> nil then
    Root.Done := True;   // saved as done="1"; Excel shows the thread resolved

  Person := Book.Persons.FindByDisplayName('Maria Ortiz');
  if Person <> nil then
    Person.UserId := 'maria.ortiz@example.com';
end;

هناك حد واحد يجب ذكره بوضوح: يكتب HotXLS عنصر <mentions> فارغاً. ولا يتم نمذجة سجلات الإشارة @-mention — وهي الآلية التي تضيء اسم زميل داخل نص التعليق وتطلق إشعاراً في Microsoft 365 — لذا يتم تخزين النص الذي يحتوي على علامة @ كنص عادي. وبالنسبة لمصنف مراجعة تم توليده نادراً ما يكون هذا خسارة، ولكن إذا كان تدفق عملك يعتمد على إشعارات الإشارة، فخطط لقيام إنسان بإضافة ذلك داخل Excel. وتقترن هوية الكاتب في جزء الشخص بشكل طبيعي مع حقول المنشأ على مستوى مصنف العمل المغطاة في بيانات مصنف العمل الوصفية وخصائص المستند, وهو المكان الذي يعيش فيه باقي مسار تدقيق الملف المولد

ماذا تفعل إصدارات Excel القديمة مع التعليقات المترابطة؟

تسبق إصدارات Excel 2016 والإصدارات الأقدم النموذج المترابط، وهي تتجاهل ببساطة الأجزاء التي لا تفهمها. وعندما يحفظ Excel 365 نفسه محادثة مترابطة، فإنه يكتب أيضاً ظلاً للتعليق القديم — ملاحظة نائبة تقرأ "[Threaded comment]..." — بالضبط حتى تظهر الإصدارات الأقدم شيئاً في الخلية المرسية. ويصدر HotXLS الأجزاء المترابطة فقط، دون أي ظل قديم، لذا فإن الملف المكتوب بكودك لا يظهر أي مؤشر تعليق على الإطلاق عند فتحه في Excel 2016. وإذا كان جمهورك يتضمن عمليات تثبيت ما قبل 365 ويجب أن يكون التعليق التوضيحي مرئياً هناك، فإن واجهة برمجة تطبيقات AddComment القديمة تظل القناة المتوافقة؛ فقط تجنب تكديس كلا النموذجين في نفس الخلية دون اختبار كل إصدار من Excel تشحن إليه، لأن كيفية توفيق القارئ بين ملاحظة وخيط في خلية واحدة هي قرار القارئ، وليست قرارك

الحد الصارم الآخر هو تنسيق الملف نفسه. فالتعليقات المترابطة هي بنية OOXML بدون نظير BIFF8، لذا فإن جانب .xls من HotXLS ليس له واجهة برمجة تطبيقات مترابطة — ومصنف العمل الذي يجب أن يظل في التنسيق الثنائي القديم يقتصر على الملاحظات الكلاسيكية. وعملياً، جدول القرار قصير: عند كتابة .xlsx لـ Excel 365 أو قراء Excel للويب/الجوال، استخدم AddThreadedComment واحصل على محادثات حقيقية؛ وعند استهداف Excel 2016 أو .xls، استخدم AddComment واقبل نموذج الملاحظة؛ وعند تدقيق ملف تلقيته، تحقق من كل من ThreadedComments وComments، لأن مصنف العمل الذي عاش في كلا العالمين يمكنه حمل كليهما بشكل قانوني. ويتم توثيق سطح واجهة برمجة التطبيقات الكامل لكلا الجيلين في صفحة منتج مكون HotXLS Delphi Excel Component