مقاله فنی

کامنت‌های سلول اکسل و لینک‌ها در Delphi با HotXLS

یک شیت را در یک کتابچهٔ کاریِ تولیدشده از «Summary» به «Overview» تغییر نام دهید، و هر لینک داخلی که به Summary!A1 اشاره می‌کرد دیگر به هیچ‌جا نمی‌رود. هیچ exception‌ای هنگام ذخیره رخ نمی‌دهد، هیچ‌کدام هنگام باز کردن. لینک همچنان رندر می‌شود، همچنان قابل کلیک به نظر می‌رسد، و بی‌سروصدا به هیچ‌چیز resolve می‌شود. همین نوع خرابی بعد از یک تبدیل save-as یا یک رفت‌وبرگشت .xls/.xlsx هم دیده می‌شود، وقتی یک کامنت یک ستون آن‌طرف‌تر می‌نشیند یا یک لینک نسبی هدفش را از دست می‌دهد. هر دو ویژگی حالت بازبینی‌ای را حمل می‌کنند که افراد واقعی روی آن عمل می‌کنند، پس وقتی خراب می‌شوند، خرابی نامرئی می‌ماند تا وقتی بازبینی‌کننده کلیک کند و هیچ اتفاقی نیفتد

این همان دلیل عملی است که کامنت‌ها و لینک‌ها بیشتر از ظاهر تزئینی‌شان به دقت نیاز دارند. HotXLS به کد Delphi و C++Builder دسترسی نوشتنی مستقیم به هر دو می‌دهد، هم در XLS و هم در XLSX، بدون هیچ اتوماسیون Excel‌ای در مسیر. روی دیگر سکهٔ این کنترل مسئولیت است: کتابخانه دقیقاً همان هدف‌هایی را می‌نویسد که به آن می‌دهید و هیچ‌کدام را اعتبارسنجی نمی‌کند، پس دست‌نخورده نگه‌داشتن گردش‌کار بازبینی وظیفهٔ کد شماست، نه وظیفهٔ Excel

کامنت‌های سلول به‌عنوان رکوردهای بازبینیِ ماشین‌نوشت

در مدل کلاسی XLSX، یک کامنت یک شیء در سطح کاربرگ است: سطر خودش، ستون خودش، یک نویسنده و یک بدنهٔ متنی را می‌شناسد. فیلد نویسنده جایگاه خودش را دارد. وقتی کتابچهٔ کاری‌ای که کد شما تولید کرده از یک زنجیرهٔ بازبینی عبور می‌کند، اولین سؤالی که یک ممیز می‌پرسد این است که چه کسی یک یادداشت مشخص را نوشته، و یادداشتی که بدون نویسنده رها شده این سؤال را با یک جای خالی پاسخ می‌دهد. کامنت‌های تولیدشده را با یک هویت سرویس مهر بزنید تا منشأ آن‌ها هرگز مبهم نباشد

دیاگرام تلاش دوبارهٔ یادداشت HotXLS در Delphi؛ کاوش FindAt یادداشت موجود سلول را به‌روز می‌کند، درحالی‌که تلاش کورکورانهٔ AddComment نسخهٔ تکراری روی هم می‌چیند
یک تلاش مجدد که AddComment را کورکورانه صدا می‌زند یادداشت دومی روی همان سلول انبار می‌کند، در حالی که پروب FindAt یادداشتی را که از قبل آنجاست ویرایش می‌کند
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Note: TXLSXComment;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('reconciliation.xlsx');
    Sheet := Book.Sheets[0];

    // یادداشت نوشته‌شده روی رقم تعدیل‌شده
    Sheet.AddComment(14, 4, 'Manual adjustment: late FX rate, see ticket FIN-2214',
      'recon-service');

    // یادداشت موجود را به‌روزرسانی کنید به‌جای اینکه یک یادداشت دوم روی آن انباشته کنید
    Note := Sheet.Comments.FindAt(14, 4);
    if Note <> nil then
      Note.Text := Note.Text + ' [verified 2026-06-11]';

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

کاوش FindAt اهمیت بیشتری از ظاهرش دارد. یک کار دسته‌ای (batch) که بعد از یک شکست گذرا دوباره تلاش می‌کند، بی‌درنگ AddComment را بار دوم روی سلولی فراخوانی می‌کند که قبلاً یادداشت‌گذاری شده، و سلول با دو یادداشت انباشته‌شده که کسی نخواسته تمام می‌شود. ابتدا با FindAt کاوش کنید، و شیئی را که برمی‌گرداند به‌روزرسانی کنید. مجموعهٔ Comments همچنین DeleteAt و DeleteInRange را در معرض قرار می‌دهد. آن نسخهٔ محدوده‌ای همان چیزی است که باید هنگام پاک‌سازی یک کتابچهٔ کاری پیش از خروج آن از سازمان به سراغش بروید: پاک کردن یادداشت‌های QA داخلی از یک ناحیهٔ کامل یک فراخوانی است، نه یک حلقهٔ دستی روی سلول‌ها

آدرس‌های خارجی و پرش‌های داخل کتابچهٔ کاری، APIهای متفاوتی هستند

OOXML این دو نوع لینک را در جاهای متفاوتی نگه می‌دارد. یک URL خارجی به یک ورودی relationship در بخش .rels شیت تبدیل می‌شود، و سلول با id به آن relationship اشاره می‌کند. یک پرش داخلی اصلاً به لایهٔ relationship دست نمی‌زند؛ این یک رشتهٔ موقعیت ساده مثل Summary!A1 است که مستقیماً روی لینک ذخیره می‌شود. HotXLS این تفاوت را در API آشکار نگه می‌دارد به‌جای این‌که یک متد واحد را اُورلود کند، که یعنی شما با دانستن این‌که هدف کجا زندگی می‌کند فراخوانی درست را انتخاب می‌کنید:

دیاگرام مقایسهٔ ذخیرهٔ URL بیرونی به‌صورت relationship در بخش rels و پرش درونی به‌صورت رشتهٔ مکان ساده، در کتاب‌کارهای تولیدشده با Delphi توسط HotXLS
یک URL بیرونی از لایه relationship عبور می‌کند در حالی که یک پرش داخلی متن ساده است، پس هر نوع به شیوه خودش شکست می‌خورد و قاعده ممیزی خودش را می‌خواهد
Sheet.Cells[2, 1].Value := 'Source record';
Sheet.AddHyperlink(2, 1, 'https://intranet.example.com/records/2214',
  'Open record 2214', 'ERP source entry');

Sheet.Cells[3, 1].Value := 'Totals';
Sheet.AddHyperlinkToCell(3, 1, 'Overview!B12', 'Jump to totals');

روی شیء TXLSXHyperlink برگشتی، Url و Location متقابلاً منحصربه‌فردند، و IsInternal به شما می‌گوید کدام‌یک از این دو پر شده است. این فلگ همان چیزی است که وقتی لینک‌های یک کتابچهٔ کاریِ بازشده را فهرست‌برداری می‌کنید و باید «فایل را ترک می‌کند» و «داخل فایل می‌ماند» را با قواعد متفاوت رفتار کنید، بررسی می‌کنید: یک میزبان خارجی ممکن است با یک allowlist روبه‌رو شود در حالی که یک هدف داخلی فقط باید نام یک شیت موجود را ببرد. لینک‌های داخلی هیچ بخش relationship‌ای پشت سرشان ندارند، که همچنین بازنویسی انبوه آن‌ها را ارزان‌تر می‌کند

خرابی‌ای که در ابتدای مقاله اشاره شد کاملاً در سمت داخلی زندگی می‌کند، و از یک واقعیت پیروی می‌کند: یک رشتهٔ موقعیت یک ارجاع تجزیه‌شده نیست. HotXLS دقیقاً همان متنی را می‌نویسد که به آن می‌دهید، و هیچ‌چیز آن متن را وقتی بعداً یک شیت تغییر نام می‌یابد دوباره اشاره‌گذاری نمی‌کند. دو دفاع در عمل کارآمد هستند. اولی نظم دربارهٔ ترتیب است: هر شیت را پیش از تولید حتی یک لینک تغییر نام دهید، سپس نام‌های شیت را به‌عنوان شناسه‌های منجمد در نظر بگیرید. دومی محکم‌تر است و در برابر تغییرنام‌های بعدی هم دوام می‌آورد. لینک را به یک نام تعریف‌شدهٔ سطح کتابچهٔ کاری اشاره دهید، نه یک آدرس خام Sheet!Cell، چون وقتی شیت زیرین تغییر می‌کند Excel تعریف یک نام را بازنویسی می‌کند، پس لینک به‌طور خودکار همراهش می‌رود. این رویکرد دوم به‌طور طبیعی با تکنیک‌های نام‌های تعریف‌شده و فرمول‌های میان‌شیتی در HotXLS جفت می‌شود

سمت XLS: همان مفاهیم، لوله‌کشی قدیمی‌تر

نمای BIFF8 کامنت‌ها را به محدوده‌ها (ranges) آویزان می‌کند به‌جای یک مجموعهٔ سطح کاربرگ. شما AddComment را روی یک IXLSRange فراخوانی می‌کنید و یک TXLSComment پس می‌گیرید؛ ویژگی Comment محدوده یک یادداشت موجود را می‌خواند، و ClearComments آن‌ها را پاک می‌کند. لبهٔ تیز اینجا موقعیتی است. یک TXLSComment به‌صورت عمومی سطر و ستون خودش را در معرض قرار نمی‌دهد، پس حلقهٔ طبیعیِ «روی هر کامنت قدم بزن و بگو کجا نشسته» برخلاف جهت API اجرا می‌شود. باید از سلول‌ها شروع کنید. یا ممیزی را از فهرست آدرس‌هایی که یادداشت‌گذاری کرده‌اید هدایت کنید، یا هنگام نوشتن لاگ موقعیت خودتان را نگه دارید، چون شیء کامنت بعداً به شما نمی‌گوید کجا زندگی می‌کند

var
  Book: IXLSWorkbook;
  Sheet: IXLSWorksheet;
  Remark: TXLSComment;
begin
  Book := TXLSWorkbook.Create;
  Sheet := Book.Sheets.Add;
  Sheet.Name := 'Review';
  Sheet.Cells.Item[5, 2].Value := 4821.50;

  Remark := Sheet.Cells.Item[5, 2].AddComment('Awaiting sign-off from controller');
  Remark.Visible := True;   // در اولین نمایش، یادداشت را باز نشان بده

  Sheet.AddHyperlink(7, 2, 'https://intranet.example.com/signoff/4821',
    'Sign-off form', 'Opens the controller queue');
  Book.SaveAs('review.xls');
end;

تنظیم Visible روی True روش قدیمی برای غیرقابل‌نادیده‌گرفتن کردن یک یادداشت است: جعبهٔ زرد روی شیت باز می‌ماند به‌جای این‌که منتظر hover بماند. TXLSComment یک قدم فراتر از همتای XLSX خودش می‌رود با در معرض قرار دادن TextRuns، پس یک یادداشت واحد می‌تواند یک هشدار بولد کنار یک توضیح ساده حمل کند، فرمت‌دهی‌ای که API کامنت XLSX به همان شکل در معرض قرار نمی‌دهد. لینک‌ها در این سمت از طریق سه اُورلود پیش‌رونده می‌رسند (فقط آدرس، سپس با متن نمایشی، سپس با یک screen tip) و از طریق مجموعهٔ HyperLinks کاربرگ بازخوانی می‌شوند، جایی‌که هر لینک Address، SubAddress، DisplayText و ScreenTip را نمایان می‌کند

یک شیت فهرست بازبینی بهتر از یادداشت‌های پراکنده است

بعد از حدود یک دوجین یادداشت، خواندنِ با hover بی‌سروصدا از مقیاس‌پذیری می‌افتد. یادداشت‌ها روی شیت‌هایی انباشته می‌شوند که یک بازبینی‌کننده هرگز باز نمی‌کند، و آن‌هایی که بیشترین اهمیت را دارند دقیقاً همان‌هایی هستند که راحت‌تر از همه از قلم می‌افتند. ساختاری که بهترین عملکرد را داشته یک شیت فهرست تولیدشده است: یک سطر برای هر موقعیت یادداشت‌گذاری‌شده، که نام شیت، آدرس سلول، نویسنده و یک گزیدهٔ کوتاه از یادداشت را فهرست می‌کند. ستون آخر یک لینک داخلی حمل می‌کند که با AddHyperlinkToCell ساخته شده و مستقیم به سلول یادداشت‌گذاری‌شده می‌پرد. اکنون بازبینی‌کننده یک فهرست را می‌خواند به‌جای این‌که در سراسر یک شبکه شکار کند، و تعداد سطرهای آن فهرست هم‌زمان به‌عنوان موجودی کامنت‌های شما برای گذر ممیزی زیر عمل می‌کند

ساختن این فهرست ارزان است چون تولیدکنندهٔ شما از قبل هر موقعیتی را که لمس کرده می‌داند. یک تاپل (sheet, row, column, author, summary) را هنگام نوشتن هر کامنت به یک لیست اضافه کنید، سپس شیت فهرست را آخر بسازید تا تعداد سطرهایش پیش از ذخیره نهایی باشد. دو بهبود سودمند است: فهرست را بر اساس شدت یا بر اساس شیت مرتب کنید نه بر اساس ترتیب درج، و یک لینک بازگشت در سربرگ فهرست بگذارید تا بازبینی‌کننده بتواند بعد از هر مورد به بالا برگردد. چون لینک‌های داخلی رشته‌های موقعیت سادهٔ بدون هیچ چیزی در لایهٔ relationship پشت سرشان هستند، حتی یک فهرست هزار-سطری تقریباً هیچ‌چیز به حجم فایل یا زمان ذخیره اضافه نمی‌کند

همان شیت در سفر برگشت هم دوباره سود می‌دهد. وقتی کتابچهٔ کاریِ بازبینی‌شده برمی‌گردد، کد شما مقدارهای وضعیتی را می‌خواند که کنار سطرهای فهرست در سلول‌ها تایپ شده‌اند، به‌جای این‌که هر شیت را دوباره برای کامنت‌هایی که ممکن است تغییر کرده باشند اسکن کند. یک ستون از سلول‌های وضعیت ساختاریافته تمیز parse می‌شود؛ یک پراکندگی از یادداشت‌های متن آزاد چنین نمی‌شود

یک گذر ممیزیِ پیش از تحویل که واقعاً خرابی را می‌گیرد

هیچ‌کدام از این APIها یک هدف را اعتبارسنجی نمی‌کنند. یک لینک به شیتی که حذف کرده‌اید، یک میزبان intranet با املای غلط، یک file share که سه‌ماههٔ قبل از رده خارج شده: همهٔ آن‌ها بدون کوچک‌ترین اعتراضی ذخیره می‌شوند. ECMA-376 مشخص می‌کند یک لینک چطور ذخیره می‌شود، نه این‌که به چیزی resolve می‌شود یا نه. پس یک کتابچهٔ کاری که فراداده‌های بازبینی حمل می‌کند شایستهٔ یک مرحلهٔ ممیزی کوتاهِ خودتان است، که درست پیش از SaveAs اجرا می‌شود:

دیاگرام گذر ممیزی پیش از تحویل HotXLS که پیش از SaveAs در Delphi مقصدهای درونی و فهرست مجاز URL و شمار یادداشت‌ها و پاک‌سازی گیرندگان را بررسی می‌کند
چهار بررسی درست پیش از SaveAs اجرا می‌شوند و هر کدام شکستی را می‌گیرند که کتابخانه خودش هرگز گزارش نخواهد داد
  • هر موقعیت داخلی نوشته‌شده در طول تولید را جمع‌آوری کنید و تأیید کنید نام شیتِ پیش از علامت تعجب (!) هنوز در مجموعهٔ شیت‌های کتابچهٔ کاری وجود دارد
  • آدرس‌های URL خارجی را در برابر یک allowlist از scheme‌ها و میزبان‌ها بررسی کنید. مسیرهای file:// خام و UNC جزئیات محیط را افشا می‌کنند و همان لحظه‌ای که فایل شبکهٔ شما را ترک می‌کند خراب می‌شوند
  • تعداد کامنت‌ها را در هر شیت بشمارید و با آنچه تولیدکنندهٔ شما قصد نوشتن آن را داشت مقایسه کنید. یک retry که یادداشت‌ها را دوبرابر کرده اینجا رو می‌شود، نه در inbox بازبینی‌کننده
  • هر وقت گیرنده بیرون از سازمان قرار دارد، یادداشت‌های فقط-داخلی را با DeleteInRange حذف کنید

تیم‌هایی که کتابچه‌های کاری خود را از یک لایهٔ داده می‌سازند می‌توانند این مرحله را در همان گام pipeline‌ای که از قبل داده را اعتبارسنجی می‌کند تا کنند، پس بررسی فراداده رایگان همراه می‌شود. مکانیزم‌ها همان‌هایی هستند که در صدور نتایج کوئری پایگاه‌داده به گزارش‌های اکسل توصیف شده‌اند، که این‌بار به‌سمت لینک‌ها و کامنت‌ها معطوف شده‌اند نه سطرها

یک جزئیات نقل‌قول‌گذاری وقتی افراد رشته‌های موقعیت را دستی می‌سازند گیرشان می‌اندازد. شیتی که نامش شامل یک فاصله است باید داخل موقعیت نقل‌قول شود، دقیقاً همان‌طور که نوار فرمول آن را نقل‌قول می‌کند: 'Quarterly Totals'!A1، نه Quarterly Totals!A1. HotXLS همان قواعدی را اعمال می‌کند که موتور فرمول برای ارجاع‌های میان‌شیتی به کار می‌برد، پس اگر یک لینک در یک فرمول کاربرگ کار کند، نقل‌قول‌گذاری‌اش اینجا هم کار خواهد کرد. اگر یک نام بدون نقل‌قول با یک فاصله به آن بدهید، همان لینک مرده و بی‌صدایی را می‌گیرید که ابتدای مقاله دربارهٔ آن هشدار داد

کامنت‌ها و لینک‌ها همان بخش‌هایی از یک کتابچهٔ کاریِ تولیدشده هستند که بازبینی‌کنندگان بدون نگاه دوم روی آن‌ها عمل می‌کنند، دقیقاً به همین دلیل یک هدفی که به هیچ‌جا اشاره نمی‌کند پیش از این‌که کسی متوجه شود آسیب واقعی وارد می‌کند. گذر اعتبارسنجی را یک‌بار بسازید، آن را روی هر کتابچهٔ کاری پیش از ارسال اجرا کنید، و گردش‌کار بازبینی در سراسر تغییرنام‌ها و تبدیل‌ها دست‌نخورده می‌ماند. کل سطح API برای هر دو نمای XLS و XLSX در صفحهٔ محصول HotXLS Delphi Component مستند شده است