Teknisk artikel

Skriva om VBA-källkod och återkomprimera MS-OVBA i Delphi

Att döpa om en hårdkodad kalkylbladsreferens genomgående i tusen makroaktiverade rapportmallar utesluter att öppna varje fil i VBA-redigeraren för hand. HotXLS, den nativa Excel-komponenten för Delphi och C++Builder, hanterar det fallet genom att exponera en VBA-moduls källkod som en redigerbar SourceCode-egenskap och återkomprimera varje ändring med MS-OVBA-komprimeringsalgoritmen som Microsoft definierar för VBA-lagring, och skriver resultatet tillbaka till klassisk XLS VBA-lagring, en fristående VBA-projektfil, eller en makroaktiverad XLSM-arbetsbok. Ingen Excel-instans, ingen VBA-redigerare, och ingen makroinspelare är inblandad någonstans i den vägen

Varför en VBA-modulström inte är en textfil

En VBA-modul inuti en XLS-arbetsbok eller en fristående VBA-projektfil är inte källtext som ligger i en ström och väntar på att läsas — det är en liten binär behållare. Ett kompilerat prestandacache kommer först, byten som Office använder för att slippa kompilera om modulen vid inläsning när cachet fortfarande matchar värdversionen, och den faktiska källtexten följer, körd genom ett proprietärt komprimeringsschema som MS-OVBA definierar specifikt för VBA-lagring. Det schemat är inte zip, inte deflate, och inte något Windows komprimerings-API:er producerar naturligt, vilket är exakt varför de flesta tredjepartsbibliotek för Excel kan läsa en moduls källkod — dekomprimering är den enklare halvan av problemet — men stannar innan de kan skriva tillbaka den, eftersom återkomprimering är där en subtilt fel bit producerar en fil Excel vägrar öppna. Publika genomgångar av läsdelen finns; skrivdelsimplementationer som faktiskt utövar återkomprimering, snarare än att bara packa upp en befintlig modul för inspektion, är tillräckligt sällsynta att detta förblir ett av de minst dokumenterade hörnen av Excel-filformaten

Vad ändrar HotXLS SourceCode-egenskap egentligen?

HotXLS representerar varje VBA-modul som ett TXLSVBAModule-objekt med en enkel SourceCode: WideString-egenskap, och att tilldela den ett nytt värde är exakt så enkelt som det ser ut: modulen markeras smutsig i minnet, och inget rör den underliggande OLE-strömmen förrän projektet sparas. Projektet självt kommer från IXLSWorkbook.VBAProject på den klassiska XLS-motorn eller TXLSXWorkbook.ParsedVBAProject på den OOXML-baserade makroaktiverade motorn, båda returnerar ett TXLSVBAProject vars moduler ligger bakom en 1-baserad Item[]-indexerare och en Count-egenskap, så en batchredigering över varje modul i en arbetsbok är bara en loop över ett heltalsintervall

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;

Den loopen är också formen på en granskningspassering. Innan tusen mallar rörs vill de flesta team först veta hur många av dem som faktiskt bär makron och vad de makrona refererar till, vilket är scenariot bakom arbetsboksgranskning och konverteringsverktyget — samma Project.Count som driver en omskrivningsloop här blir en per-fil-makroräkning där

Inuti MS-OVBA-komprimeringsbehållaren

MS-OVBA:s komprimeringsformat paketerar källbyten till vad specifikationen kallar en CompressedContainer: en enda signaturbyte, som måste vara lika med 0x01, följd av en sekvens av CompressedChunk-block, var och en täcker upp till 4096 byte dekomprimerad data. Ett 16-bitars chunk-huvud bär tre fält — en 3-bitars signatur som måste vara lika med 3, ett 12-bitars storleksfält, och en CompressedChunkFlag-bit som markerar om chunkens payload är bokstavliga byte eller en tokenkomprimerad sekvens. När flaggan är satt är payloaden en serie flaggbyte-prefixade grupper om åtta tokens, och varje token är antingen en enda bokstavlig byte eller en CopyToken: en offset/längd-bakåtreferens till byte som redan dekomprimerats tidigare i samma chunk, med bitbredden fördelad mellan offset och längd som skiftar beroende på hur långt in i chunken dekomprimeraren för närvarande befinner sig. Denna del av MS-OVBA (§2.4.1, Compression and Decompression) är där en handbyggd implementation oftast förlorar en dag på ett off-by-one-fel i den bitbreddsberäkningen

Varför HotXLS skriver råa chunkar istället för att matcha tokens

HotXLS skrivväg kringgår tokenmatchningshalvan av den algoritmen helt. När den återkomprimerar en redigerad modul går varje chunk ut med CompressedChunkFlag rensad, vilket betyder att chunken innehåller bokstavliga byte snarare än bakåtreferenstokens — lagligt enligt MS-OVBA, eftersom en komprimerad behållare får bestå helt av okomprimerade chunkar, och det tar bort exakt den del av algoritmen som är svårast att få rätt för hand: att hitta giltiga bakåtreferenser och packa ett offset/längd-par in i en bitbredd som beror på den aktuella positionen inuti chunken. Avvägningen visar sig i filstorlek, inte korrekthet — en omskriven modulström hamnar nära storleken på sin källtext plus ett tvåbyte-huvud per 4096-byte-block, inte mindre som en fullt tokenkomprimerad chunk skulle vara. Varje läsare som implementerar dekomprimeringssidan av specifikationen, Excel inräknat, öppnar fortfarande resultatet korrekt, eftersom en rå chunk är precis lika giltig som en CompressedChunk som en tokenkomprimerad

Vad HotXLS lämnar orört när den skriver om en modul

Återkomprimering ersätter bara en del av modulströmmen. Varje modulström lagrar sitt prestandacache först och sin komprimerade källkod därefter, och projektets dir-ström registrerar exakt var den delningen faller för varje modul i en MODULEOFFSET-post; HotXLS läser den offseten, behåller varje byte innan den exakt som den hittade dem, och bygger om bara den komprimerade behållaren från offseten och framåt

Källtexten själv går tur och retur genom VBA-projektets egen kodsida snarare än UTF-8 — samma äldre kodsida Office skrev projektet med från början. En SourceCode-redigering som introducerar tecken utanför den kodsidans repertoar ersätts tyst med bäst-passande ersättningstecken när HotXLS kodar om strängen tillbaka till byte, avvisas inte, så ett ovanligt regionalt tecken tappat i en kommentar eller strängliteral är den mest sannolika platsen att märka förlusten. Externa referenser och biblioteksbindningar inom samma projekt följer en relaterad men separat bevarandeväg, som täcks i följeartikeln om bevarande av externa VBA-länkar, och den är värd att läsa innan en omskrivningspassering rör ett projekt som länkar ut till andra arbetsböcker eller typbibliotek

Hur får man tillbaka de omskrivna makrona i en arbetsbok?

Inget anropar återkomprimeringssteget explicit — det körs automatiskt i det ögonblick en arbetsbok eller ett fristående VBA-projekt sparas. TXLSVBAProject.ApplyChanges går igenom varje modul, återkomprimerar de vars SourceCode ändrats sedan senaste sparningen, och skriver om bara den modulens ström; den klassiska TXLSWorkbook.SaveAs, när sparmålet behåller filens ursprungliga format, och OOXML-TXLSXWorkbook.SaveAs för ett makroaktiverat XLSM-paket anropar båda den internt innan något skrivs till disk, och SaveVBAProjectToFile anropar samma metod när målet är en frikopplad VBA-projektfil snarare än en full arbetsbok

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;

Alla tre destinationerna delar samma SourceCode- och ApplyChanges-mekanik under ytan; den enda verkliga skillnaden mellan dem är vilket sparanrop som slutar utlösa återkomprimeringen

Var det fortfarande går sönder

Två felmönster är vanliga nog att planera kring innan en omskrivningspassering körs mot produktionsfiler. Ett digitalt signerat VBA-projekt slutar vara giltigt signerat i samma ögonblick dess källkod ändras, eftersom signaturen täcker projektets innehåll; HotXLS har inget sätt att signera om ett projekt åt dig, och Excel tar bort eller flaggar signaturen nästa gång filen öppnas, så ett signerat makroprojekt behöver ett steg för omsignering nedströms om den signaturen är något ditt arbetsflöde faktiskt kontrollerar. Det andra felmönstret tillhör den som frestas att återimplementera det här komprimeringsformatet från grunden istället för att använda ett bibliotek som redan hanterar det: en enda fel bit i ett chunk-huvud, i signaturnibblen, storleksfältet, eller den komprimerade flaggan, producerar en fil Excel vägrar öppna, vanligtvis bakom en generisk korruptionsvarning som inte ger någon ledtråd om vilken byte som var fel — precis den felkategori den råa chunk-skrivstrategin som beskrevs tidigare finns för att undvika

Inget av detta kräver att man reverse-engineerar formatet för att använda det. Delphi- och C++Builder-utvecklare får läs- och skrivåtkomst till SourceCode, MS-OVBA-kompatibel återkomprimering, och alla tre skriv-tillbaka-destinationer som beskrivs här som en del av standard-HotXLS-komponenten, tillsammans med resten av dess klassiska XLS- och OOXML-arbetsboks-API