Přejmenovat natvrdo zadaný odkaz na list napříč tisícem šablon reportů s povolenými makry vylučuje ruční otevírání každého souboru v editoru VBA. HotXLS, nativní komponenta Excelu pro Delphi a C++Builder, tento případ řeší tak, že vystavuje zdrojový kód modulu VBA jako editovatelnou vlastnost SourceCode a rekomprimuje každou úpravu kompresním algoritmem MS-OVBA, který Microsoft definuje pro úložiště VBA, a výsledek zapisuje zpět do úložiště VBA klasického XLS, samostatného souboru projektu VBA, nebo sešitu XLSM s povolenými makry. Žádná instance Excelu, žádný editor VBA a žádný záznamník maker se v této cestě nikde neúčastní
Proč proud modulu VBA není textový soubor
Modul VBA uvnitř sešitu XLS nebo samostatného souboru projektu VBA není zdrojový text sedící v proudu, který čeká na přečtení — je to malý binární kontejner. Nejdřív přichází zkompilovaná výkonnostní cache, bajty, které Office používá k tomu, aby při načtení přeskočil rekompilaci modulu, když cache stále odpovídá hostitelské verzi, a za ní následuje skutečný zdrojový text, protažený proprietárním kompresním schématem, které MS-OVBA definuje výslovně pro úložiště VBA. Toto schéma není zip, není deflate, a není nic, co by API pro kompresi ve Windows produkovala nativně, což je přesně důvod, proč většina knihoven Excelu třetích stran dokáže přečíst zdrojový kód modulu — dekomprese je snadnější polovina problému — ale zastaví se před jeho zápisem zpět, protože rekomprese je místo, kde jemně špatný bit vyprodukuje soubor, který Excel odmítne otevřít. Veřejné popisy čtecí strany existují; implementace zápisové strany, které skutečně provádějí rekompresi, místo aby jen rozbalily existující modul kvůli inspekci, jsou dost vzácné na to, že tohle zůstává jeden z nejméně zdokumentovaných koutů formátů souborů Excelu
Co přesně mění vlastnost SourceCode v HotXLS?
HotXLS reprezentuje každý modul VBA jako objekt TXLSVBAModule s obyčejnou vlastností SourceCode: WideString, a přiřadit jí novou hodnotu je přesně tak jednoduché, jak to vypadá: modul se v paměti označí jako změněný a nic se nedotkne podkladového proudu OLE, dokud se projekt neuloží. Samotný projekt pochází z IXLSWorkbook.VBAProject na klasickém enginu XLS nebo TXLSXWorkbook.ParsedVBAProject na enginu OOXML s podporou maker, oba vracejí TXLSVBAProject, jehož moduly sedí za indexerem Item[] počítaným od 1 a vlastností Count, takže dávková úprava napříč každým modulem v sešitu je jen smyčka přes celočíselný rozsah
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;
Tato smyčka je také tvarem auditního průchodu. Dřív, než se sáhne na tisíc šablon, většina týmů nejdřív chce vědět, kolik z nich vlastně nese makra a na co se ta makra odkazují, což je scénář za pracovním nástrojem pro audit a konverzi sešitů — stejné Project.Count, které zde pohání smyčku přepisu, se tam stává počtem maker na soubor
Uvnitř kompresního kontejneru MS-OVBA
Kompresní formát MS-OVBA balí zdrojové bajty do toho, co specifikace nazývá CompressedContainer: jeden bajt podpisu, povinně rovný 0x01, následovaný sekvencí bloků CompressedChunk, každý pokrývající až 4096 bajtů dekomprimovaných dat. 16bitová hlavička bloku nese tři pole — 3bitový podpis, který se musí rovnat 3, 12bitové pole velikosti, a bit CompressedChunkFlag označující, zda je payload bloku doslovné bajty nebo sekvence komprimovaná tokeny. Když je příznak nastaven, payload je běh skupin osmi tokenů předchozích bajtem příznaku, a každý token je buď jediný doslovný bajt, nebo CopyToken: zpětný odkaz offset/délka do bajtů už dekomprimovaných dřív ve stejném bloku, s bitovou šířkou rozdělenou mezi offset a délku posunující se podle toho, jak daleko do bloku se dekompresor právě dostal. Tato část MS-OVBA (§2.4.1, Compression and Decompression) je místo, kde ručně psaná implementace nejčastěji ztratí den kvůli chybě o jedna v tomto výpočtu bitové šířky
Proč HotXLS zapisuje surové bloky místo hledání shodujících se tokenů
Zápisová cesta HotXLS se úplně vyhýbá polovině algoritmu zabývající se hledáním tokenů. Když rekomprimuje upravený modul, každý blok odejde s vyčištěným CompressedChunkFlag, což znamená, že blok drží doslovné bajty místo tokenů zpětného odkazu — legální podle MS-OVBA, protože komprimovaný kontejner smí sestávat výhradně z nekomprimovaných bloků, a odstraňuje to přesně tu část algoritmu, kterou je nejtěžší ručně udělat správně: najít platné zpětné odkazy a zabalit dvojici offset/délka do bitové šířky, která závisí na aktuální pozici uvnitř bloku. Kompromis se projeví ve velikosti souboru, ne ve správnosti — přepsaný proud modulu skončí blízko velikosti svého zdrojového textu plus dvoubajtová hlavička na blok 4096 bajtů, ne menší, jak by tomu bylo u plně tokeny komprimovaného bloku. Každý čtenář, který implementuje dekompresní stranu specifikace, Excel nevyjímaje, otevře výsledek stále správně, protože surový blok je stejně platný CompressedChunk jako ten komprimovaný tokeny
Co HotXLS nechá nedotčené, když modul přepisuje
Rekomprese vždy nahradí jen část proudu modulu. Každý proud modulu si nejdřív ukládá svou výkonnostní cache a pak komprimovaný zdroj a proud dir projektu zaznamenává přesně, kde toto rozdělení pro každý modul padá, v položce MODULEOFFSET; HotXLS tento posun přečte, každý bajt před ním ponechá přesně tak, jak jej našel, a přestaví jen komprimovaný kontejner od tohoto posunu dál
Samotný zdrojový text prochází obousměrně přes vlastní znakovou stránku projektu VBA, ne UTF-8 — stejnou starší znakovou stránku, se kterou Office projekt původně napsal. Úprava SourceCode, která zavede znaky mimo repertoár této znakové stránky, se při zpětném překódování řetězce na bajty tiše nahradí nejlépe padnoucími náhradními znaky, ne odmítne, takže neobvyklý regionální znak vhozený do komentáře nebo řetězcového literálu je nejpravděpodobnější místo, kde si ztráty všimnete. Externí odkazy a bindingy na knihovny uvnitř stejného projektu se řídí příbuznou, ale samostatnou cestou zachování, popsanou v doprovodném článku o zachování externích odkazů VBA, a vyplatí se jej přečíst dřív, než se průchod přepisu dotkne projektu, který se odkazuje ven na jiné sešity nebo knihovny typů
Jak se přepsaná makra dostanou zpět do sešitu?
Nic krok rekomprese nevolá explicitně — spustí se automaticky v okamžiku, kdy se sešit nebo samostatný projekt VBA uloží. TXLSVBAProject.ApplyChanges projde každý modul, rekomprimuje ty, jejichž SourceCode se od posledního uložení změnil, a přepíše jen proud tohoto modulu; klasický TXLSWorkbook.SaveAs, když cíl uložení podrží původní formát souboru, i OOXML TXLSXWorkbook.SaveAs pro balíček XLSM s povolenými makry oba volají tuto metodu interně dřív, než se cokoli zapíše na disk, a SaveVBAProjectToFile volá stejnou metodu, když je cílem odpojený soubor projektu VBA místo celého sešitu
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;
Všechny tři cíle pod sebou sdílejí stejnou mechaniku SourceCode a ApplyChanges; jediný skutečný rozdíl mezi nimi je, které volání uložení nakonec vyvolá rekompresi
Kde to pořád selhává
Dva režimy selhání jsou dost běžné na to, aby se s nimi počítalo dřív, než se průchod přepisu spustí na produkčních souborech. Digitálně podepsaný projekt VBA přestává být platně podepsaný v okamžiku, kdy se jeho zdroj změní, protože podpis pokrývá obsah projektu; HotXLS nemá způsob, jak by projekt za vás znovu podepsal, a Excel podpis při příštím otevření souboru zahodí nebo označí, takže podepsaný projekt s makry potřebuje po směru krok opětovného podpisu, pokud je tento podpis něco, co váš pracovní postup skutečně kontroluje. Druhý režim selhání patří každému, kdo je v pokušení tento kompresní formát znovu implementovat od nuly místo použití knihovny, která jej už zvládá: jediný špatný bit v hlavičce bloku, v nibblu podpisu, v poli velikosti, nebo v příznaku komprese, vyprodukuje soubor, který Excel odmítne otevřít, obvykle za obecným varováním o poškození, které nedá žádnou nápovědu, který bajt byl špatně — přesně ta třída chyby, které se strategie zápisu surových bloků popsaná dřív snaží vyhnout
Nic z toho nevyžaduje zpětné inženýrství formátu k použití. Vývojáři v Delphi a C++Builderu dostávají přístup ke čtení a zápisu SourceCode, rekompresi vyhovující MS-OVBA a všechny tři cíle zápisu zpět popsané zde jako součást standardní komponenty HotXLS, spolu se zbytkem jejího API pro sešity klasického XLS a OOXML