Представьте задание, которое почти ничего не делает: открыть месячную книгу, записать в одну ячейку сегодняшнюю дату, сохранить обратно. Прогоните это через сервис достаточно часто — и жалоба всё равно придёт. Макросы пропали или связанные курсы валют теперь читаются как #REF!, и служба эксплуатации убеждена, что их удалил ваш код. Ничего он не удалял. Обычно происходит вот что: книга с макросами уходит под обычным именем .xlsx, и Excel подчиняется правилам типов содержимого ECMA-376: пакет, чей тип содержимого не объявляет VBA, не может загрузить проект VBA независимо от того, лежат ли байты прямо там. Файл не сломался. Его переименовали в состояние, в котором Excel обязан игнорировать его часть
Макросы и внешние ссылки на книги — две вещи, которые автоматизация теряет надёжнее всего, и причина у этого одна. Обе живут вне сетки ячеек, которой на самом деле касается редактирующий код, поэтому код, рассуждающий строками и колонками, потеряет их, ни разу не отдав команды на удаление. HotXLS — нативная библиотека для Delphi и C++Builder, читающая и пишущая XLS и XLSX без установленного Excel, — обращается с обоими активами как с грузом, который несёт намеренно, а не как с данными, которые копируются заодно. Дальше речь о том, что каждому из них нужно от вашего пути сохранения и где заканчиваются гарантии
Почему эти два актива ведут себя при перезаписи по-разному
Проект VBA — это один непрозрачный двоичный блок. В пакете OOXML это файл vbaProject.bin; в устаревшем файле BIFF это хранилище OLE. Потерять его можно ровно двумя способами: писатель не скопировал его в вывод либо вывод получил тип файла, который его запрещает. Любой из этих отказов тотален и беззвучен. Проект либо есть, либо его нет
Внешняя ссылка — вовсе не блоб. Это небольшой граф связей: путь или URL, указывающий на другую книгу, список имён листов, которые эта цель предоставляет, и необязательный кэш значений, в последний раз виденных на этих листах, чтобы Excel мог что-то показать, когда цель недоступна. У этих трёх частей разные сроки жизни при перезаписи, и библиотека может честно сохранить одни и тихо потерять другие. Именно в этой асимметрии и стоит быть точным, потому что ничто в коде правки ячеек её не проявит
Проносим проект VBA через перезапись XLSX
На стороне XLSX TXLSXWorkbook сохраняет груз макросов дословно. Свойство VbaProject держит сырые байты vbaProject.bin внутри AnsiString, и пустая строка — это способ модели сказать, что макросов нет. Вокруг него три операции: HasVbaProject отвечает, присутствует ли проект, ClearVbaProject удаляет его намеренно, а LoadVbaProjectFromFile внедряет проект, извлечённый из шаблона. Последний вызов стоит больше, чем кажется. Он позволяет сгенерированным книгам подхватывать стандартный проект макросов, не таща через конвейер полный файл шаблона
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Refreshed ' + DateTimeToStr(Now);
Book.LoadVbaProjectFromFile('macros\vbaProject.bin');
if not Book.HasVbaProject then
raise Exception.Create('VBA payload failed to load');
// Расширение .xlsm не косметика: оно выбирает
// тип содержимого с поддержкой макросов внутри пакета.
Book.SaveAs('monthly-report.xlsm');
finally
Book.Free;
end;
end;
Строка сохранения — то место, где вся проблема и поворачивается. Книга с проектом VBA обязана записываться с семантикой поддержки макросов, и HotXLS применяет её, когда имя цели заканчивается на .xlsm. Дайте вместо этого .xlsx — и Excel откажет макросам, хотя байты физически присутствуют в пакете и прекрасно бы десериализовались. Расширение здесь не украшение; оно выбирает тип содержимого, который сообщает Excel, что проекту VBA дозволено существовать. Чаще всего груз нужно просто пронести. Когда же требуется в него заглянуть, скажем перечислить имена модулей для отчёта аудита, ParsedVBAProject даёт разобранную модель модулей, а VbaProject остаётся исходными нетронутыми байтами
Переиспользование макросов из устаревших книг XLS
Фасад BIFF повторяет этот набор инструментов с одним дополнительным шагом. HasVBAProject прощупывает загруженный файл, SaveVBAProjectToFile выписывает хранилище проекта на диск, а LoadVBAProjectFromFile читает его обратно в другую книгу. Крюк через файл делает частую задачу модернизации простой: поднять макросы из модели образца 2003 года и посадить их в свежесгенерированный вывод XLS, без исходного шаблона во время выполнения
var
Src, Dst: IXLSWorkbook; // ссылки на интерфейс: Free вручную не нужен
begin
Src := TXLSWorkbook.Create;
if Src.Open('legacy-model.xls') <= 0 then
raise Exception.Create('Cannot open legacy model');
if Src.HasVBAProject then
Src.SaveVBAProjectToFile('extracted-vba.bin');
Dst := TXLSWorkbook.Create;
Dst.Sheets.Add.Name := 'Report2026';
Dst.LoadVBAProjectFromFile('extracted-vba.bin');
Dst.SaveAs('report-with-macros.xls');
end;
Ловушка здесь — модель памяти, и она обратна классу XLSX. TXLSWorkbook удерживается через интерфейс IXLSWorkbook с подсчётом ссылок, поэтому вручную его освобождать нельзя; TXLSXWorkbook из мира XLSX — обычный объект, который нужно обернуть в try..finally и освободить. Смешайте оба соглашения в одном модуле — и последуют падения из-за двойного освобождения. Ещё одна граница, которую стоит уважать: держите извлечение и внедрение внутри одного формата файла. Хранилище проекта BIFF и vbaProject.bin из OOXML — родственники, но не один и тот же контейнер, и конвейеру, обязанному выдавать макросы в обоих форматах, следует держать отдельный шаблон макросов для каждого
Внешние ссылки: карта выживает, кэшированные значения — нет
Для книг XLSX HotXLS открывает внешние ссылки через коллекцию ExternalLinks. Каждая TXLSXExternalLink несёт Target — путь или URL удалённой книги — плюс список SheetNames с именами листов, на которые она ссылается. И то и другое переживает цикл открытия и сохранения нетронутым, а ссылку можно и построить с нуля:
var
Link: TXLSXExternalLink;
begin
Link := Book.ExternalLinks.Add('\\fileserver\finance\fx-rates-2026.xlsx');
Link.SheetNames.Add('FX');
if Book.ExternalLinks.Count > 0 then
Writeln(Format('%d external link(s): delivery requires reachable targets',
[Book.ExternalLinks.Count]));
end;
Граница проходит на уровень глубже списка целей. HotXLS проводит через обратный цикл карту ссылок, то есть цель и имена листов, но он не разбирает и не переписывает кэшированные значения ячеек, которые OOXML держит в элементе sheetDataSet ссылки. Этот кэш и позволяет Excel показать последнее известное число, когда исходный файл недоступен, а сгенерированная книга уходит без него. Последствие достаётся получателю, а не вам. Откройте такой файл там, где цель недостижима — ноутбук вне VPN или переименованный сетевой ресурс, — и формулы, зависящие от ссылки, разрешатся в #REF! или застынут за приглашением обновить. Отсюда следуют два правила. Не обещайте, что сгенерированная книга покажет свои внешние связанные значения без сети. И читайте ненулевой ExternalLinks.Count как условие доставки, а не как возможность: каждая цель обязана быть достижима оттуда, где файл действительно откроют
Что читатель XLS сохраняет побайтово
Для структур, которые он не моделирует, сторона BIFF даёт другой ответ: оставить их ровно такими, какими нашла. Кэши и представления сводных таблиц (семейство записей SX*), определения QueryTable, внешние подключения к данным, пользовательские представления, картинки колонтитулов и записи темы — всё это проходит цикл открытия и сохранения сырыми блоками записей, неразобранными и неизменёнными. Сами внешние ссылки проходят обратный цикл через нижележащие записи EXTERNSHEET и SupBook. Типизированного API создания для них на стороне XLS нет, но существующая ссылка переживает редактирование нетронутой
Побайтовое сохранение — настоящая гарантия с острым краем. Поскольку сохранённую структуру никто не читает, ваши правки не могут её испортить. По той же причине никто её и не обновляет. Вставьте строки через область, на которую указывает сохранённый кэш сводной таблицы или таблица запроса, — и структура удержит исходные координаты, пока данные под ней съезжают. Файл остаётся корректным XML или BIFF; смысл тихо ушёл из соответствия, и никакая ошибка вам об этом не сообщит. Защитимая схема — держать сгенерированные правки на листах, где нет сохранённых структур, и это та же дисциплина, что защищает заблокированные и настроенные под печать листы в нашей статье о защите листов и параметрах страницы
Проверка того файла, который вы действительно записали
Оба режима отказа беззвучны в момент записи, поэтому значимое утверждение делается повторным открытием вывода, а не доверием к коду, который его произвёл. Три проверки покрывают почти всё. Откройте файл заново и убедитесь, что HasVbaProject по-прежнему возвращает true всякий раз, когда макросы ожидались, — это одним тестом ловит и потерянный груз, и неверное расширение. Прочитайте ExternalLinks.Count и сравните его с числом до перезаписи. Затем откройте файл разок в Excel с отключёнными макросами, потому что проверка типов содержимого в Excel строже любой библиотечной, а Excel — та программа, по которой ваши клиенты будут судить о файле
Ничего из этого не требует полного разбора на входе. Когда книги приходят потоком и вам нужно лишь отсортировать, какие из них несут регулируемое содержимое, лёгкое прощупывание из нашей статьи о перечислении листов и лёгкой инспекции книг позволяет направить файлы с макросами и ссылками в более строгий конвейер ещё до первой перезаписи
Несколько вопросов возникают достаточно часто, чтобы ответить на них прямо. HotXLS никогда не выполняет сохраняемые им макросы: в библиотеке нет среды выполнения VBA, только механика хранения, копирования, извлечения и внедрения проекта как данных. На сервере это свойство безопасности, о котором стоит сказать вслух, поскольку враждебный макрос, проходящий через конвейер, остаётся инертным, пока настольный Excel не откроет файл и пользователь не разрешит содержимое. Превратить .xlsm в .xlsx и сохранить макросы невозможно, и это правило формата, а не ограничение библиотеки: тип содержимого .xlsx объявляет книгу без макросов, поэтому честных исходов только два — остаться в .xlsm либо вызвать ClearVbaProject и отправить файл, в котором макросов действительно нет. Тихое переименование — единственный выбор, который не устроит никого. А когда связанные ячейки после перезаписи показывают #REF!, причина в отсутствующем кэше значений, о котором сказано выше: новый файл несёт цель, но не кэшированные числа, поэтому Excel обязан разрешить источник при открытии, а недостижимый или зависящий от окружения путь ему этого не даёт. Либо гарантируйте достижимость цели, либо запишите вычисленные значения в ячейки перед доставкой и снимите зависимость целиком
Редактирование чужих книг — это по большей части работа по сохранению того, чего вы не писали и не понимаете до конца. Описанные здесь средства обратного цикла для VBA и внешних ссылок поставляются с HotXLS Delphi Component для Delphi и C++Builder вместе со свойствами аудита, позволяющими обнаружить регулируемое содержимое в тот же миг, когда файл пришёл