Переименуйте лист с «Summary» на «Overview» в сгенерированной книге, и каждая внутренняя гиперссылка, указывавшая на Summary!A1, перестаёт вести куда бы то ни было. Ни исключения при сохранении, ни при открытии. Ссылка по-прежнему отображается, по-прежнему выглядит кликабельной и тихо ведёт в никуда. Такая же поломка проявляется после конвертации через save-as или обратного цикла .xls/.xlsx, когда комментарий оказывается смещённым на столбец или относительная ссылка теряет свою цель. Обе функции несут состояние рецензирования, на которое реагируют живые люди, так что когда они ломаются, сбой невидим до тех пор, пока рецензент не кликнет и ничего не произойдёт
Это и есть практическая причина, по которой комментарии и гиперссылки заслуживают большей заботы, чем подсказывает их косметический вид. HotXLS даёт коду на Delphi и C++Builder прямой доступ на запись к обоим, в XLS и XLSX, без какой-либо автоматизации Excel в цепочке. Оборотная сторона этого контроля — ответственность: библиотека записывает ровно те цели, что вы ей передаёте, и не проверяет ни одну из них, так что сохранение целостности рабочего процесса рецензирования — задача вашего кода, а не Excel
Комментарии к ячейкам как машинописные записи рецензирования
В объектной модели XLSX комментарий — это объект уровня рабочего листа: он знает свою строку, свой столбец, автора и текстовое тело. Поле автора занимает своё место не зря. Когда книга, сгенерированная вашим кодом, проходит через цепочку рецензирования, первый вопрос, который задаёт аудитор, — кто написал данную заметку, а заметка, оставленная без автора, отвечает на этот вопрос пустотой. Помечайте сгенерированные комментарии сервисной идентичностью, чтобы происхождение никогда не было неоднозначным
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 несёт больше веса, чем кажется на вид. Пакетная задача, повторяющая попытку после временного сбоя, с готовностью вызовет AddComment второй раз на ячейке, которую уже аннотировала, и ячейка получит две наложенные друг на друга заметки, которых никто не просил. Сначала проверяйте через FindAt и обновляйте объект, который он возвращает. Коллекция Comments также открывает DeleteAt и DeleteInRange. Именно к варианту с диапазоном стоит обращаться, когда вы очищаете книгу перед тем, как она покинет здание: удаление внутренних QA-аннотаций из целой области — это один вызов, а не написанный вручную цикл по ячейкам
Внешние URL и переходы внутри книги — разные API
OOXML хранит два вида ссылок в разных местах. Внешний URL становится записью связи в части .rels листа, а ячейка указывает на эту связь по id. Внутренний переход вообще не касается слоя связей; это обычная строка местоположения вроде Summary!A1, хранящаяся прямо на ссылке. HotXLS оставляет это различие видимым в API, а не перегружает один метод, а значит, вы выбираете правильный вызов, зная, где живёт цель:
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, тогда как внутренней цели достаточно назвать существующий лист. За внутренними ссылками не тянутся части связей, что также делает их дешевле для массовой перезаписи
Поломка из начала статьи целиком живёт на внутренней стороне, и она проистекает из одного факта: строка местоположения — это не разобранная ссылка. HotXLS записывает ровно тот текст, что вы ей передали, и ничто не перенаправляет этот текст, когда лист впоследствии переименовывают. На практике держатся две защиты. Первая — дисциплина в порядке действий: переименуйте каждый лист до того, как сгенерируете хоть одну ссылку, а затем относитесь к именам листов как к замороженным идентификаторам. Вторая надёжнее и переживает переименования, сделанные постфактум. Направляйте ссылку на именованный диапазон уровня книги, а не на сырой адрес Sheet!Cell, потому что Excel переписывает определение имени при изменении соответствующего листа, так что ссылка автоматически едет вместе с ним. Этот второй подход естественно сочетается с техниками из статьи об именованных диапазонах и межлистовых формулах в HotXLS
Сторона XLS: те же концепции, более старая сантехника
Фасад BIFF8 вешает комментарии на диапазоны, а не на коллекцию уровня рабочего листа. Вы вызываете 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 — устаревший способ сделать заметку невозможной не заметить: жёлтый бокс остаётся открытым на листе вместо ожидания наведения курсора. TXLSComment идёт на шаг дальше своего аналога из XLSX, открывая TextRuns, так что одна заметка может нести жирное предупреждение рядом с обычным пояснением — форматирование, которое API комментариев XLSX не открывает тем же способом. Гиперссылки на этой стороне поступают через три прогрессивные перегрузки (только адрес, затем с отображаемым текстом, затем со всплывающей подсказкой) и читаются обратно через коллекцию HyperLinks рабочего листа, где каждая ссылка раскрывает Address, SubAddress, DisplayText и ScreenTip
Индексный лист рецензирования превосходит разбросанные заметки
После дюжины с небольшим аннотаций чтение по наведению курсора тихо перестаёт масштабироваться. Заметки копятся на листах, которые рецензент никогда не открывает, а самые важные из них — это как раз те, что легче всего пропустить. Структура, которая держится лучше всего, — это сгенерированный индексный лист: одна строка на аннотированную позицию с указанием имени листа, адреса ячейки, автора и краткой выдержки из заметки. Последний столбец несёт внутреннюю гиперссылку, построенную через AddHyperlinkToCell, которая прыгает прямо к аннотированной ячейке. Теперь рецензент читает список сверху вниз вместо охоты по сетке, а число строк этого индекса заодно служит вашей инвентаризацией комментариев для проверки ниже
Индекс дёшево строить, потому что ваш генератор уже знает каждую позицию, которой коснулся. Добавляйте кортеж (лист, строка, столбец, автор, сводка) в список по мере записи каждого комментария, а затем выдавайте индексный лист последним, чтобы число его строк было окончательным перед сохранением. Два усовершенствования себя оправдывают: упорядочивайте индекс по серьёзности или по листу, а не по порядку вставки, и разместите обратную ссылку в заголовке индекса, чтобы рецензент мог возвращаться наверх после каждого пункта. Поскольку внутренние ссылки — это простые строки местоположения без ничего в слое связей позади них, даже индекс на тысячу строк почти ничего не добавляет к размеру файла или времени сохранения
Тот же лист окупается снова на обратном пути. Когда прорецензированная книга возвращается, ваш код читает значения статуса, введённые в ячейки рядом со строками индекса, вместо повторного сканирования каждого листа на предмет комментариев, которые могли измениться. Столбец структурированных ячеек статуса разбирается чисто; разброс заметок в свободном тексте — нет
Проход аудита перед доставкой, который реально ловит поломку
Ни один из этих API не проверяет цель. Ссылка на удалённый вами лист, опечатка в имени интранет-хоста, файловый ресурс, выведенный из эксплуатации в прошлом квартале, — всё это сохранится без единого звука. ECMA-376 определяет, как хранится ссылка, а не то, что она куда-то указывает. Поэтому книга, несущая метаданные рецензирования, заслуживает собственного короткого этапа аудита, запускаемого прямо перед SaveAs:
- Соберите каждую внутреннюю позицию, записанную во время генерации, и подтвердите, что имя листа перед восклицательным знаком всё ещё существует в коллекции листов книги
- Проверяйте внешние URL по allowlist схем и хостов. Голые пути
file://и UNC утекают детали окружения и ломаются в тот момент, когда файл покидает вашу сеть - Подсчитывайте комментарии по каждому листу и сравнивайте с тем, что ваш генератор собирался записать. Повтор, удвоивший заметки, всплывёт здесь, а не во входящих у рецензента
- Удаляйте только-внутренние аннотации через
DeleteInRange, когда получатель находится вне организации
Команды, строящие свои книги из слоя данных, могут свернуть этот этап в тот же шаг конвейера, который уже проверяет данные, так что проверка метаданных едет бесплатным пассажиром. Механика та же, что описана в статье об экспорте результатов запросов из базы данных в отчёты Excel, только развёрнутая на ссылки и комментарии, а не на строки
Одна деталь с кавычками сбивает с толку, когда строки местоположения строят вручную. Имя листа, содержащее пробел, должно быть заключено в кавычки внутри строки местоположения — ровно так, как его заключает в кавычки строка формул: 'Quarterly Totals'!A1, а не Quarterly Totals!A1. HotXLS применяет те же правила, что использует движок формул для межлистовых ссылок, так что если ссылка работает в формуле рабочего листа, её кавычки сработают и здесь. Передайте ему незаключённое в кавычки имя с пробелом, и вы получите ту же самую тихую мёртвую ссылку, о которой предупреждало начало статьи
Комментарии и гиперссылки — это те части сгенерированной книги, на которые рецензенты реагируют без второй мысли, и именно поэтому цель, указывающая в никуда, наносит реальный ущерб, прежде чем кто-либо это заметит. Постройте проверку один раз, запускайте её на каждой книге перед отправкой, и рабочий процесс рецензирования останется целым при переименованиях и конвертациях. Полная поверхность API для обоих фасадов, XLS и XLSX, задокументирована на странице продукта HotXLS Delphi Component