Перейменування жорстко закодованого посилання на аркуш у тисячі шаблонів звітів із увімкненими макросами виключає варіант відкривати кожен файл у редакторі VBA вручну. HotXLS, рідний компонент Excel для Delphi та C++Builder, обробляє цей випадок, відкриваючи вихідний код модуля VBA як властивість SourceCode, придатну для редагування, і повторно стискаючи кожну правку алгоритмом стиснення MS-OVBA, який Microsoft визначає спеціально для сховища VBA, записуючи результат назад у сховище VBA класичного XLS, окремий файл проєкту VBA чи книгу XLSM з увімкненими макросами. Жодного екземпляра Excel, жодного редактора VBA й жодного записувача макросів не залучено ніде в цьому шляху
Чому потік модуля VBA — не текстовий файл
Модуль VBA всередині книги XLS чи окремого файлу проєкту VBA — не вихідний текст, що сидить у потоці в очікуванні читання, — це невеликий бінарний контейнер. Спочатку йде скомпільований кеш продуктивності, байти, які Office використовує, щоб пропустити повторну компіляцію модуля при завантаженні, коли кеш усе ще відповідає версії хосту, а за ним іде фактичний вихідний текст, пропущений через власницьку схему стиснення, яку MS-OVBA визначає спеціально для сховища VBA. Ця схема — не zip, не deflate і не щось, що API стиснення Windows виробляють нативно, і саме тому більшість сторонніх бібліотек Excel можуть читати вихідний код модуля — декомпресія легша половина проблеми, — але зупиняються, не доходячи до запису назад, оскільки повторне стиснення — саме те місце, де тонко неправильний біт створює файл, який Excel відмовляється відкривати. Публічні описи сторони читання існують; реалізації сторони запису, що справді виконують повторне стиснення, а не просто розпаковують наявний модуль для огляду, достатньо рідкісні, тож це залишається одним із найменш задокументованих закутків форматів файлів Excel
Що насправді змінює властивість SourceCode у HotXLS?
HotXLS представляє кожен модуль VBA як об'єкт TXLSVBAModule зі звичайною властивістю SourceCode: WideString, і присвоєння їй нового значення таке ж просте, яким і виглядає: модуль позначається брудним у пам'яті, і ніщо не торкається базового потоку OLE, доки проєкт не буде збережено. Сам проєкт надходить від IXLSWorkbook.VBAProject у класичному рушії XLS чи TXLSXWorkbook.ParsedVBAProject у рушії OOXML з увімкненими макросами, обидва повертають TXLSVBAProject, чиї модулі сидять за 1-базованим індексатором Item[] та властивістю Count, тож пакетна правка по кожному модулю в книзі — просто цикл по цілочисельному діапазону
var
Wb: TXLSWorkbook;
Project: TXLSVBAProject;
I: Integer;
Updated: WideString;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Open('MonthlyReport.xls');
if Wb.HasVBAProject then
begin
Project := Wb.VBAProject;
for I := 1 to Project.Count do
begin
Updated := StringReplace(Project[I].SourceCode,
'ReportSheet2025', 'ReportSheet2026', [rfReplaceAll]);
if Updated <> Project[I].SourceCode then
Project[I].SourceCode := Updated; // marks the module dirty
end;
Wb.SaveAs('MonthlyReport.xls'); // recompresses on write
end;
finally
Wb.Free;
end;
end;
Цей цикл також є формою проходу аудиту. Перш ніж торкнутися тисячі шаблонів, більшість команд спершу хочуть знати, скільки з них справді несуть макроси й на що ці макроси посилаються, а це саме той сценарій, що стоїть за верстаком аудиту та конвертації книг — той самий Project.Count, що керує циклом переписування тут, там стає підрахунком макросів на файл
Всередині контейнера стиснення MS-OVBA
Формат стиснення MS-OVBA пакує байти джерела у те, що специфікація називає CompressedContainer: єдиний байт сигнатури, що мусить дорівнювати 0x01, за яким іде послідовність блоків CompressedChunk, кожен покриває до 4096 байтів розпакованих даних. 16-бітний заголовок фрагмента несе три поля — 3-бітну сигнатуру, що мусить дорівнювати 3, 12-бітне поле розміру та біт CompressedChunkFlag, що позначає, чи корисне навантаження фрагмента — це буквальні байти чи послідовність, стиснена токенами. Коли прапорець встановлений, корисне навантаження — це серія груп із восьми токенів із префіксом байта прапорців, і кожен токен — це або одиничний буквальний байт, або CopyToken: посилання назад зсув/довжина в байти, вже розпаковані раніше в тому самому фрагменті, з розподілом бітової ширини між зсувом і довжиною, що зсувається залежно від того, як далеко всередині фрагмента наразі сидить декомпресор. Саме ця частина MS-OVBA (§2.4.1, Compression and Decompression) — те місце, де саморобна реалізація найчастіше втрачає день на помилку на одиницю в цьому обчисленні бітової ширини
Чому HotXLS пише сирі фрагменти замість зіставлення токенів
Шлях запису HotXLS повністю обходить половину алгоритму зі зіставленням токенів. Коли він повторно стискає відредагований модуль, кожен фрагмент виходить із очищеним CompressedChunkFlag, тобто фрагмент тримає буквальні байти, а не токени посилання назад — законно за MS-OVBA, оскільки стиснений контейнер може повністю складатися зі нестиснутих фрагментів, і це прибирає саме ту частину алгоритму, яку найважче зробити правильно вручну: знаходження дійсних посилань назад та упаковку пари зсув/довжина в бітову ширину, що залежить від поточної позиції всередині фрагмента. Компроміс проявляється в розмірі файлу, не в коректності — потік переписаного модуля виходить близько до розміру свого вихідного тексту плюс двобайтовий заголовок на кожен блок у 4096 байтів, не менший, як був би повністю стиснений токенами фрагмент. Кожен читач, що реалізує сторону декомпресії специфікації, включно з Excel, усе одно відкриває результат коректно, бо сирий фрагмент такий самий дійсний CompressedChunk, як і стиснений токенами
Що HotXLS залишає незайманим, коли переписує модуль
Повторне стиснення завжди замінює лише частину потоку модуля. Кожен потік модуля зберігає спочатку свій кеш продуктивності, а потім стиснене джерело, і потік проєкту dir записує точно, де падає цей поділ для кожного модуля в записі MODULEOFFSET; HotXLS читає цей зсув, зберігає кожен байт до нього точно таким, яким знайшов, і перебудовує лише стиснений контейнер від зсуву й далі
Сам текст джерела проходить туди й назад через власну кодову сторінку проєкту VBA, а не UTF-8 — ту саму застарілу кодову сторінку, з якою Office спочатку записав проєкт. Правка SourceCode, що вводить символи поза репертуаром цієї кодової сторінки, мовчки замінюється символами найкращого наближення, коли HotXLS перекодовує рядок назад у байти, не відхиляється, тож незвичайний регіональний символ, вкинутий у коментар чи рядковий літерал, — найімовірніше місце, де можна помітити цю втрату. Зовнішні посилання та прив'язки бібліотек усередині того самого проєкту йдуть спорідненим, але окремим шляхом збереження, описаним у супутній статті про збереження зовнішніх посилань VBA, і її варто прочитати перш ніж прохід переписування торкнеться проєкту, що посилається на інші книги чи бібліотеки типів
Як повернути переписані макроси назад у книгу?
Ніщо явно не викликає крок повторного стиснення — він виконується автоматично в момент, коли книгу чи окремий проєкт VBA зберігають. TXLSVBAProject.ApplyChanges обходить кожен модуль, повторно стискає ті, чий SourceCode змінився з часу останнього збереження, і переписує лише потік цього модуля; класичний TXLSWorkbook.SaveAs, коли ціль збереження зберігає оригінальний формат файлу, та OOXML TXLSXWorkbook.SaveAs для пакету XLSM з увімкненими макросами обидва внутрішньо викликають його ще до того, як щось записується на диск, а SaveVBAProjectToFile викликає той самий метод, коли ціль — від'єднаний файл проєкту VBA, а не повна книга
var
Wb: TXLSWorkbook;
begin
Wb := TXLSWorkbook.Create;
try
if Wb.LoadVBAProjectFromFile('LegacyMacros.ole') = 1 then
begin
Wb.VBAProject[1].SourceCode :=
StringReplace(Wb.VBAProject[1].SourceCode, 'OldServer', 'NewServer', [rfReplaceAll]);
Wb.SaveVBAProjectToFile('LegacyMacros_Patched.ole'); // ApplyChanges runs internally
end;
finally
Wb.Free;
end;
end;
var
Xlsx: TXLSXWorkbook;
Project: TXLSVBAProject;
begin
Xlsx := TXLSXWorkbook.Create;
try
Xlsx.Open('Dashboard.xlsm');
Project := Xlsx.ParsedVBAProject;
if Assigned(Project) then
begin
Project[1].SourceCode := StringReplace(Project[1].SourceCode,
'ConnStringV1', 'ConnStringV2', [rfReplaceAll]);
Xlsx.SaveAs('Dashboard.xlsm'); // SyncParsedVBAProject recompresses before the part is written
end;
finally
Xlsx.Free;
end;
end;
Усі три призначення поділяють ту саму механіку SourceCode та ApplyChanges під капотом; єдина справжня відмінність між ними — який виклик збереження врешті спричиняє повторне стиснення
Де це все ще ламається
Два режими збою достатньо поширені, щоб їх варто було спланувати ще до того, як прохід переписування запуститься проти промислових файлів. Цифрово підписаний проєкт VBA перестає бути дійсно підписаним у ту мить, коли змінюється його джерело, оскільки підпис покриває вміст проєкту; HotXLS не має способу повторно підписати проєкт від вашого імені, і Excel відкидає чи позначає підпис наступного разу, коли файл відкривається, тож підписаному проєкту макросів потрібен наступний крок повторного підписання, якщо цей підпис — щось, що ваш робочий процес справді перевіряє. Другий режим збою належить будь-кому, кого спокушає повторно реалізувати цей формат стиснення з нуля замість використання бібліотеки, що вже його обробляє: один неправильний біт у заголовку фрагмента, у ніблі сигнатури, полі розміру чи прапорці стиснення, створює файл, який Excel відмовляється відкривати, зазвичай за загальним попередженням про пошкодження, що не дає жодного натяку, який байт був неправильним, — саме той клас помилок, для уникнення якого існує описана раніше стратегія запису сирих фрагментів
Ніщо з цього не вимагає зворотної розробки формату для використання. Розробники Delphi та C++Builder отримують доступ на читання й запис до SourceCode, повторне стиснення, що відповідає MS-OVBA, і всі три призначення для запису назад, описані тут, як частину стандартного компонента HotXLS, поряд з рештою його API книг класичного XLS та OOXML