Технічна стаття

Збереження макросів VBA та зовнішніх посилань при переписуванні книг кодом Delphi

Уявіть завдання, що майже нічого не робить: відкрити щомісячну книгу, записати сьогоднішню дату в одну клітинку, зберегти назад. Прожене таке завдання через сервіс достатньо часто — і скарга все одно надійде. Макроси зникли, або пов’язані обмінні курси тепер показують #REF!, і операційна команда переконана, що ваш код їх видалив. Він нічого не видаляв. Зазвичай трапляється те, що книга з увімкненими макросами вийшла під звичайним іменем .xlsx, і Excel підкорився правилам типів вмісту ECMA-376: пакет, чий тип вмісту не оголошує VBA, не може завантажити проєкт VBA, незалежно від того, чи байти фізично там присутні. Файл не зламався. Його перейменували в стан, де Excel зобов’язаний ігнорувати частину нього

Макроси й посилання на зовнішні книги — дві речі, які автоматизація втрачає найнадійніше, з однієї й тієї самої підспідньої причини. Обидві живуть поза сіткою клітинок, якої насправді торкається код редагування, тож код, що міркує в термінах рядків і стовпців, скине їх, жодного разу не видавши команду видалення. HotXLS — нативна бібліотека для Delphi та C++Builder, що читає й записує XLS і XLSX без встановленого Excel, і вона трактує обидва активи як навантаження, яке несе навмисно, а не як дані, що просто копіюються за збігом. Далі — те, чого кожен із них потребує від вашого шляху збереження, і де гарантії закінчуються

Чому ці два активи поводяться по-різному під час перезапису

Проєкт VBA — один непрозорий двійковий блок. У пакеті OOXML це файл vbaProject.bin; у застарілому файлі BIFF це сховище OLE. Втратити його можна рівно двома способами: записувач ніколи не копіює його у вивід, або вивід отримує тип файлу, що забороняє його. Будь-який зі збоїв тотальний і безшумний. Проєкт або присутній, або ні

Зовнішнє посилання — це взагалі не блоб. Це малий граф зв’язків: цільовий шлях чи URL, що вказує на іншу книгу, список назв аркушів, які ця ціль відкриває, і опційний кеш значень, останніми побачених на цих аркушах, щоб Excel міг щось показати, коли ціль офлайн. Ці три частини мають різний час життя під час перезапису, і бібліотека може вірно зберегти одні, мовчки скидаючи інші. Ця асиметрія — та частина, яку варто розуміти точно, бо ніщо в коді редагування клітинок її не покаже

Порівняльна діаграма: блоб проєкту VBA проти трьох частин посилання на зовнішню книгу, які HotXLS проносить крізь переписування Delphi
VBA-проєкт переживає перезапис як бінарний вміст усе-або-нічого, тоді як зовнішнє посилання — маленький граф, чиї ціль, назви аркушів і кешовані значення можна зберегти чи втратити незалежно

Перенесення проєкту 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 з підрахунком посилань, тож ви ніколи не звільняєте його вручну; XLSX TXLSXWorkbook — звичайний об’єкт, який треба обгорнути в 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 як передумову доставки, а не як функцію: кожна ціль має бути досяжною звідти, де файл насправді буде відкрито

Потокова діаграма виклику збереження в Delphi: розширення .xlsm обирає тип вмісту з підтримкою макросів, а .xlsx змушує Excel мовчки відмовити макроси
HotXLS несе сирі байти vbaProject.bin крізь збереження, а розширення .xlsm — саме те, що обирає макро-спроможний тип вмісту, якого вимагає Excel

Що зчитувач XLS зберігає побайтово

Для структур, які він не моделює, бік BIFF дає іншу відповідь: лишити їх точно такими, якими знайдено. Кеші зведених таблиць і подання зведених таблиць (сімейство записів SX*), визначення QueryTable, з’єднання із зовнішніми даними, користувацькі подання, зображення заголовків і записи тем — усе це проходить крізь цикл відкриття-і-збереження як сирі блоки записів, нерозібраними й незміненими. Самі зовнішні посилання переносяться туди й назад через базові записи EXTERNSHEET і SupBook. Для них немає типізованого API створення на боці XLS, але наявне посилання переживає редагування неторканим

Діаграма: які частини зовнішнього посилання на книгу HotXLS переживають переписування, і що відбувається, коли кешовані значення за sheetDataSet відсутні офлайн
HotXLS пропускає ціль посилання та її назви аркушів туди й назад, але кешовані значення клітинок за sheetDataSet не переносяться у згенерований файл

Побайтове збереження — справжня гарантія з гострим краєм. Оскільки ніщо не читає збережену структуру, ваші редагування не можуть її пошкодити. З тієї ж причини ніщо її й не оновлює. Вставте рядки крізь регіон, на який вказує збережений кеш зведеної таблиці чи таблиця запитів, і структура утримує свої початкові координати, поки дані під нею зсуваються. Файл усе ще валідний 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 разом із властивостями аудиту, що дозволяють виявити регульований вміст тієї миті, коли файл надходить