Rinominare un riferimento a foglio di lavoro codificato in modo fisso in mille modelli di report abilitati per macro esclude di per sé l'apertura di ogni file nell'editor VBA a mano. HotXLS, il componente Excel nativo per Delphi e C++Builder, gestisce questo caso esponendo il sorgente di un modulo VBA come una proprietà SourceCode modificabile e ricomprimendo ogni modifica con l'algoritmo di compressione MS-OVBA che Microsoft definisce per lo storage VBA, riscrivendo il risultato nello storage VBA di un XLS classico, in un file di progetto VBA autonomo, o in una cartella di lavoro XLSM abilitata per macro. Nessuna istanza di Excel, nessun editor VBA e nessun registratore di macro è coinvolto in nessun punto di quel percorso
Perché uno stream di modulo VBA non è un file di testo
Un modulo VBA dentro una cartella di lavoro XLS o un file di progetto VBA autonomo non è testo sorgente seduto in uno stream in attesa di essere letto — è un piccolo contenitore binario. Prima viene una cache di performance compilata, i byte che Office usa per saltare la ricompilazione del modulo al caricamento quando la cache corrisponde ancora alla versione host, e il testo sorgente vero e proprio segue, passato attraverso uno schema di compressione proprietario che MS-OVBA definisce specificamente per lo storage VBA. Quello schema non è zip, non è deflate, e non è nulla che le API di compressione di Windows producano nativamente, ed è esattamente per questo che la maggior parte delle librerie Excel di terze parti riesce a leggere il sorgente di un modulo — la decompressione è la metà più facile del problema — mentre si ferma prima di riscriverlo, poiché la ricompressione è dove un bit sbagliato in modo sottile produce un file che Excel rifiuta di aprire. Esistono trattazioni pubbliche del lato di lettura; implementazioni del lato di scrittura che esercitino realmente la ricompressione, anziché limitarsi a scompattare un modulo esistente per ispezione, sono abbastanza scarse da far sì che questo rimanga uno degli angoli meno documentati dei formati file di Excel
Cosa cambia realmente la proprietà SourceCode di HotXLS?
HotXLS rappresenta ogni modulo VBA come un oggetto TXLSVBAModule con una semplice proprietà SourceCode: WideString, e assegnarle un nuovo valore è esattamente semplice come sembra: il modulo viene marcato come modificato in memoria, e nulla tocca lo stream OLE sottostante finché il progetto non viene salvato. Il progetto stesso proviene da IXLSWorkbook.VBAProject sul motore XLS classico o da TXLSXWorkbook.ParsedVBAProject sul motore OOXML abilitato per macro, entrambi restituiscono un TXLSVBAProject i cui moduli risiedono dietro un indicizzatore Item[] a base 1 e una proprietà Count, quindi una modifica in blocco su ogni modulo di una cartella di lavoro è semplicemente un ciclo su un intervallo intero
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;
Quel ciclo è anche la forma di un passaggio di audit. Prima che mille modelli vengano toccati, la maggior parte dei team vuole prima sapere quanti di essi portano realmente macro e a cosa quelle macro fanno riferimento, che è lo scenario dietro il banco di lavoro di audit e conversione delle cartelle di lavoro — lo stesso Project.Count che guida un ciclo di riscrittura qui diventa lì un conteggio di macro per file
Dentro il contenitore di compressione MS-OVBA
Il formato di compressione di MS-OVBA impacchetta i byte sorgente in ciò che la specifica chiama CompressedContainer: un singolo byte di firma, che deve essere uguale a 0x01, seguito da una sequenza di blocchi CompressedChunk, ciascuno che copre fino a 4096 byte di dato decompresso. Un'intestazione di chunk a 16 bit porta tre campi — una firma a 3 bit che deve essere uguale a 3, un campo dimensione a 12 bit, e un bit CompressedChunkFlag che segna se il payload del chunk sia byte letterali o una sequenza compressa a token. Quando il flag è impostato, il payload è una serie di gruppi di otto token preceduti da un byte flag, e ogni token è o un singolo byte letterale o un CopyToken: un riferimento all'indietro offset/lunghezza a byte già decompressi in precedenza nello stesso chunk, con la larghezza in bit divisa tra offset e lunghezza che varia a seconda di quanto in profondità nel chunk il decompressore si trovi attualmente. Questa parte di MS-OVBA (§2.4.1, Compression and Decompression) è dove un'implementazione scritta a mano più spesso perde una giornata su un errore di uno nel calcolo di quella larghezza in bit
Perché HotXLS scrive chunk grezzi invece di far corrispondere token
Il percorso di scrittura di HotXLS evita del tutto la metà di corrispondenza dei token di quell'algoritmo. Quando ricomprime un modulo modificato, ogni chunk esce con il CompressedChunkFlag azzerato, il che significa che il chunk contiene byte letterali invece di token di riferimento all'indietro — legale secondo MS-OVBA, poiché a un contenitore compresso è permesso consistere interamente di chunk non compressi, e questo rimuove esattamente la parte dell'algoritmo più difficile da ottenere correttamente a mano: trovare riferimenti all'indietro validi e impacchettare una coppia offset/lunghezza in una larghezza in bit che dipende dalla posizione attuale dentro il chunk. Il compromesso si manifesta nella dimensione del file, non nella correttezza — uno stream di modulo riscritto finisce vicino alla dimensione del proprio testo sorgente più un'intestazione di due byte per blocco da 4096 byte, non più piccolo come sarebbe un chunk completamente compresso a token. Ogni lettore che implementi il lato di decompressione della specifica, Excel incluso, apre comunque correttamente il risultato, perché un chunk grezzo è un CompressedChunk valido quanto uno compresso a token
Cosa lascia intatto HotXLS quando riscrive un modulo
La ricompressione sostituisce sempre e solo parte dello stream del modulo. Ogni stream di modulo memorizza prima la propria cache di performance e poi il proprio sorgente compresso, e lo stream dir del progetto registra esattamente dove cade quella separazione per ciascun modulo in una voce MODULEOFFSET; HotXLS legge quell'offset, mantiene esattamente invariato ogni byte precedente così come lo ha trovato, e ricostruisce solo il contenitore compresso a partire da quell'offset in poi
Il testo sorgente stesso viaggia andata e ritorno attraverso la code page propria del progetto VBA piuttosto che UTF-8 — la stessa code page legacy con cui Office ha scritto il progetto in origine. Una modifica di SourceCode che introduce caratteri al di fuori del repertorio di quella code page viene silenziosamente sostituita con caratteri di rimpiazzo best-fit quando HotXLS ricodifica la stringa in byte, non rifiutata, quindi un carattere regionale insolito inserito in un commento o in un letterale stringa è il punto più probabile in cui notare la perdita. I riferimenti esterni e i binding a librerie dentro lo stesso progetto seguono un percorso di preservazione correlato ma separato, trattato nell'articolo di approfondimento sulla preservazione dei link esterni VBA, e vale la pena leggerlo prima che un passaggio di riscrittura tocchi un progetto che collega ad altre cartelle di lavoro o librerie di tipi
Come si riportano le macro riscritte in una cartella di lavoro?
Nulla chiama esplicitamente il passaggio di ricompressione — viene eseguito automaticamente nel momento in cui una cartella di lavoro o un progetto VBA autonomo viene salvato. TXLSVBAProject.ApplyChanges percorre ogni modulo, ricomprime quelli il cui SourceCode è cambiato dall'ultimo salvataggio, e riscrive solo lo stream di quel modulo; sia il classico TXLSWorkbook.SaveAs, quando la destinazione di salvataggio mantiene il formato originale del file, sia l'OOXML TXLSXWorkbook.SaveAs per un pacchetto XLSM abilitato per macro lo chiamano internamente prima che qualcosa venga scritto su disco, e SaveVBAProjectToFile chiama lo stesso metodo quando la destinazione è un file di progetto VBA distaccato piuttosto che una cartella di lavoro completa
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;
Tutte e tre le destinazioni condividono sotto il cofano gli stessi meccanismi di SourceCode e ApplyChanges; l'unica vera differenza tra loro è quale chiamata di salvataggio finisca per scatenare la ricompressione
Dove questo si rompe ancora
Due modalità di fallimento sono abbastanza comuni da pianificare in anticipo prima che un passaggio di riscrittura venga eseguito contro file di produzione. Un progetto VBA firmato digitalmente smette di essere validamente firmato nel momento in cui il suo sorgente cambia, poiché la firma copre il contenuto del progetto; HotXLS non ha modo di rifirmare un progetto per tuo conto, ed Excel elimina o segnala la firma la volta successiva che il file viene aperto, quindi un progetto macro firmato ha bisogno di un passaggio di rifirma a valle se quella firma è qualcosa che il tuo flusso di lavoro effettivamente controlla. La seconda modalità di fallimento appartiene a chiunque sia tentato di reimplementare questo formato di compressione da zero invece di usare una libreria che già lo gestisce: un singolo bit sbagliato in un'intestazione di chunk, nel nibble di firma, nel campo dimensione, o nel flag compresso, produce un file che Excel rifiuta di aprire, di solito dietro un avviso generico di corruzione che non dà alcun indizio su quale byte fosse sbagliato — esattamente la classe di bug che la strategia di scrittura a chunk grezzi descritta sopra esiste per evitare
Nulla di tutto ciò richiede reverse engineering del formato per essere usato. Gli sviluppatori Delphi e C++Builder ottengono accesso in lettura e scrittura a SourceCode, ricompressione conforme a MS-OVBA, e tutte e tre le destinazioni di riscrittura descritte qui come parte del componente HotXLS standard, insieme al resto della sua API per cartelle di lavoro XLS classiche e OOXML