Renomear uma referência codificada a uma folha de cálculo em mil modelos de relatório com macros ativadas exclui abrir cada ficheiro no editor VBA à mão. O HotXLS, o componente Excel nativo para Delphi e C++Builder, trata esse caso expondo o código-fonte de um módulo VBA como uma propriedade editável SourceCode e recomprimindo cada edição com o algoritmo de compressão MS-OVBA que a Microsoft define para armazenamento VBA, escrevendo o resultado de volta no armazenamento VBA de um XLS clássico, num ficheiro de projeto VBA autónomo, ou num livro de cálculo XLSM com macros ativadas. Nenhuma instância do Excel, nenhum editor VBA, e nenhum gravador de macros está envolvido em qualquer ponto desse caminho
Porque não é um fluxo de módulo VBA um ficheiro de texto
Um módulo VBA dentro de um livro de cálculo XLS ou de um ficheiro de projeto VBA autónomo não é texto-fonte pousado num fluxo à espera de ser lido — é um pequeno contentor binário. Uma cache de desempenho compilada vem primeiro, os bytes que o Office usa para saltar a recompilação do módulo no carregamento quando a cache ainda corresponde à versão hospedeira, e o texto-fonte real segue-se, passado por um esquema de compressão proprietário que o MS-OVBA define especificamente para armazenamento VBA. Esse esquema não é zip, não é deflate, e não é nada que as APIs de compressão do Windows produzam nativamente, o que é exatamente a razão pela qual a maioria das bibliotecas Excel de terceiros consegue ler o código-fonte de um módulo — a descompressão é a metade mais fácil do problema — enquanto param antes de o voltar a escrever, uma vez que a recompressão é onde um bit subtilmente errado produz um ficheiro que o Excel se recusa a abrir. Existem textos públicos sobre o lado da leitura; implementações do lado da escrita que efetivamente exercitem a recompressão, em vez de apenas desempacotar um módulo existente para inspeção, são suficientemente raras para que isto continue a ser um dos cantos menos documentados dos formatos de ficheiro do Excel
O que muda efetivamente a propriedade SourceCode do HotXLS?
O HotXLS representa cada módulo VBA como um objeto TXLSVBAModule com uma simples propriedade SourceCode: WideString, e atribuir-lhe um novo valor é exatamente tão simples como parece: o módulo é marcado como modificado em memória, e nada toca no fluxo OLE subjacente até o projeto ser guardado. O próprio projeto vem de IXLSWorkbook.VBAProject no motor XLS clássico ou TXLSXWorkbook.ParsedVBAProject no motor OOXML com macros ativadas, ambos devolvendo um TXLSVBAProject cujos módulos se situam por trás de um indexador Item[] baseado em 1 e uma propriedade Count, pelo que uma edição em lote através de todos os módulos de um livro de cálculo é apenas um ciclo sobre um intervalo de inteiros
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;
Esse ciclo é também a forma de uma passagem de auditoria. Antes de mil modelos serem tocados, a maioria das equipas quer primeiro saber quantos deles efetivamente transportam macros e a que essas macros se referem, que é o cenário por trás do banco de trabalho de auditoria e conversão de livros de cálculo — o mesmo Project.Count que conduz um ciclo de reescrita aqui torna-se ali uma contagem de macros por ficheiro
Dentro do contentor de compressão MS-OVBA
O formato de compressão do MS-OVBA empacota bytes de origem naquilo que a especificação chama CompressedContainer: um único byte de assinatura, obrigado a ser igual a 0x01, seguido de uma sequência de blocos CompressedChunk, cada um cobrindo até 4096 bytes de dados descomprimidos. Um cabeçalho de bloco de 16 bits transporta três campos — uma assinatura de 3 bits que tem de ser igual a 3, um campo de tamanho de 12 bits, e um bit CompressedChunkFlag que assinala se o payload do bloco é bytes literais ou uma sequência comprimida por tokens. Quando a flag está definida, o payload é uma sequência de grupos de oito tokens prefixados por byte de flag, e cada token é ou um único byte literal ou um CopyToken: uma referência de retorno deslocamento/comprimento para bytes já descomprimidos anteriormente no mesmo bloco, com a largura de bits dividida entre deslocamento e comprimento a variar consoante quão longe dentro do bloco o descompressor atualmente se situa. Esta parte do MS-OVBA (§2.4.1, Compression and Decompression) é onde uma implementação feita à mão mais frequentemente perde um dia com um erro de um em algo nesse cálculo de largura de bits
Porque escreve o HotXLS blocos em bruto em vez de corresponder tokens
O caminho de escrita do HotXLS contorna inteiramente a metade de correspondência de tokens desse algoritmo. Quando recomprime um módulo editado, cada bloco sai com a CompressedChunkFlag desativada, significando que o bloco contém bytes literais em vez de tokens de referência de retorno — legal ao abrigo do MS-OVBA, uma vez que um contentor comprimido pode consistir inteiramente em blocos não comprimidos, e remove precisamente a parte do algoritmo mais difícil de acertar à mão: encontrar referências de retorno válidas e empacotar um par deslocamento/comprimento numa largura de bits que depende da posição atual dentro do bloco. A compensação aparece no tamanho do ficheiro, não na correção — um fluxo de módulo reescrito fica perto do tamanho do seu texto-fonte mais um cabeçalho de dois bytes por bloco de 4096 bytes, não mais pequeno da forma como um bloco totalmente comprimido por tokens seria. Todo o leitor que implemente o lado de descompressão da especificação, o Excel incluído, continua a abrir o resultado corretamente, porque um bloco em bruto é um CompressedChunk tão válido quanto um comprimido por tokens
O que o HotXLS deixa intocado quando reescreve um módulo
A recompressão só alguma vez substitui parte do fluxo de módulo. Cada fluxo de módulo armazena primeiro a sua cache de desempenho e depois o seu código-fonte comprimido, e o fluxo dir do projeto regista exatamente onde essa divisão cai para cada módulo numa entrada MODULEOFFSET; o HotXLS lê esse deslocamento, mantém cada byte antes dele exatamente como o encontrou, e reconstrói apenas o contentor comprimido a partir desse deslocamento em diante
O próprio texto-fonte faz o percurso de ida e volta através da própria página de código do projeto VBA, em vez de UTF-8 — a mesma página de código legada com que o Office escreveu o projeto em primeiro lugar. Uma edição de SourceCode que introduza caracteres fora do repertório dessa página de código é silenciosamente substituída por caracteres de substituição de melhor ajuste quando o HotXLS recodifica a cadeia de volta para bytes, não rejeitada, pelo que um caractere regional invulgar colocado num comentário ou literal de cadeia é o sítio mais provável para notar a perda. As referências externas e vinculações de biblioteca dentro do mesmo projeto seguem um caminho de preservação relacionado mas separado, abordado em o artigo complementar sobre preservação de ligações externas VBA, e vale a pena ler antes de uma passagem de reescrita tocar num projeto que se liga a outros livros de cálculo ou bibliotecas de tipos
Como se coloca as macros reescritas de volta num livro de cálculo?
Nada chama o passo de recompressão explicitamente — corre automaticamente no momento em que um livro de cálculo ou um projeto VBA autónomo é guardado. TXLSVBAProject.ApplyChanges percorre cada módulo, recomprime os cujo SourceCode mudou desde a última gravação, e reescreve apenas o fluxo desse módulo; o TXLSWorkbook.SaveAs clássico, quando o alvo da gravação mantém o formato original do ficheiro, e o TXLSXWorkbook.SaveAs OOXML para um pacote XLSM com macros ativadas, ambos o chamam internamente antes de qualquer coisa ser escrita em disco, e SaveVBAProjectToFile chama o mesmo método quando o alvo é um ficheiro de projeto VBA destacado em vez de um livro de cálculo completo
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;
Os três destinos partilham por baixo a mesma mecânica de SourceCode e ApplyChanges; a única diferença real entre eles é qual chamada de gravação acaba por disparar a recompressão
Onde isto continua a falhar
Dois modos de falha são suficientemente comuns para se planear antecipadamente antes de uma passagem de reescrita correr contra ficheiros de produção. Um projeto VBA assinado digitalmente deixa de estar validamente assinado no momento em que o seu código-fonte muda, uma vez que a assinatura cobre o conteúdo do projeto; o HotXLS não tem forma de voltar a assinar um projeto em seu nome, e o Excel descarta ou assinala a assinatura na próxima vez que o ficheiro abre, pelo que um projeto de macro assinado precisa de um passo de nova assinatura a jusante, se essa assinatura for algo que o seu fluxo de trabalho efetivamente verifique. O segundo modo de falha pertence a quem seja tentado a reimplementar este formato de compressão de raiz em vez de usar uma biblioteca que já o trata: um único bit errado num cabeçalho de bloco, na semiparte de assinatura, no campo de tamanho, ou na flag de compressão, produz um ficheiro que o Excel se recusa a abrir, normalmente por trás de um aviso genérico de corrupção que não dá qualquer pista sobre qual byte estava errado — precisamente a classe de bug que a estratégia de escrita de blocos em bruto descrita acima existe para evitar
Nada disto exige fazer engenharia inversa do formato para o usar. Os programadores Delphi e C++Builder obtêm acesso de leitura e escrita a SourceCode, recompressão em conformidade com o MS-OVBA, e os três destinos de gravação aqui descritos como parte do componente HotXLS standard, a par do resto da sua API de livros de cálculo XLS clássico e OOXML