Technischer Artikel

VBA-Quellcode umschreiben und MS-OVBA neu komprimieren in Delphi

Eine fest kodierte Arbeitsblattreferenz über tausend makrofähige Report-Vorlagen umzubenennen schließt aus, jede Datei von Hand im VBA-Editor zu öffnen. HotXLS, die native Delphi- und C++Builder-Excel-Komponente, behandelt diesen Fall, indem es den Quellcode eines VBA-Moduls als bearbeitbare SourceCode-Eigenschaft freilegt und jede Änderung mit dem MS-OVBA-Kompressionsalgorithmus neu komprimiert, den Microsoft für VBA-Speicherung definiert, und das Ergebnis zurück in den klassischen XLS-VBA-Speicher, eine eigenständige VBA-Projektdatei oder eine makrofähige XLSM-Arbeitsmappe schreibt. Keine Excel-Instanz, kein VBA-Editor und kein Makrorekorder ist irgendwo in diesem Pfad beteiligt

Warum ein VBA-Modul-Stream keine Textdatei ist

Ein VBA-Modul innerhalb einer XLS-Arbeitsmappe oder einer eigenständigen VBA-Projektdatei ist kein Quelltext, der in einem Stream darauf wartet, gelesen zu werden — es ist ein kleiner binärer Container. Zuerst kommt ein kompilierter Performance-Cache, die Bytes, die Office verwendet, um das Neu-Kompilieren des Moduls beim Laden zu überspringen, wenn der Cache noch zur Host-Version passt, und der eigentliche Quelltext folgt, durch ein proprietäres Kompressionsschema geschickt, das MS-OVBA speziell für VBA-Speicherung definiert. Dieses Schema ist kein zip, kein deflate, und nichts, das die Windows-Kompressions-APIs nativ produzieren, weshalb genau die meisten Drittanbieter-Excel-Bibliotheken den Quellcode eines Moduls lesen können — Dekompression ist die leichtere Hälfte des Problems — aber davor stoppen, ihn zurückzuschreiben, da Rekompression der Punkt ist, an dem ein subtil falsches Bit eine Datei erzeugt, die Excel nicht öffnen will. Öffentliche Abhandlungen zur Leseseite gibt es; schreibseitige Implementierungen, die tatsächlich Rekompression ausüben statt nur ein bestehendes Modul zur Inspektion auszupacken, sind selten genug, dass dies eine der am wenigsten dokumentierten Ecken der Excel-Dateiformate bleibt

Was ändert HotXLSs SourceCode-Eigenschaft tatsächlich?

HotXLS repräsentiert jedes VBA-Modul als ein TXLSVBAModule-Objekt mit einer schlichten SourceCode: WideString-Eigenschaft, und ihr einen neuen Wert zuzuweisen ist genau so einfach, wie es aussieht: Das Modul wird im Speicher als geändert markiert, und nichts fasst den zugrunde liegenden OLE-Stream an, bis das Projekt gespeichert wird. Das Projekt selbst kommt von IXLSWorkbook.VBAProject auf der klassischen XLS-Engine oder TXLSXWorkbook.ParsedVBAProject auf der makrofähigen OOXML-Engine, beide geben ein TXLSVBAProject zurück, dessen Module hinter einem 1-basierten Item[]-Indexer und einer Count-Eigenschaft sitzen, sodass eine Batch-Bearbeitung über jedes Modul einer Arbeitsmappe nur eine Schleife über einen Integer-Bereich ist

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;

Diese Schleife ist auch die Form eines Audit-Durchlaufs. Bevor tausend Vorlagen angefasst werden, wollen die meisten Teams zuerst wissen, wie viele von ihnen tatsächlich Makros tragen und worauf diese Makros verweisen, das Szenario hinter der Arbeitsmappen-Audit- und Konvertierungswerkbank — dasselbe Project.Count, das hier eine Umschreibeschleife antreibt, wird dort zu einer Makro-Zählung pro Datei

Innerhalb des MS-OVBA-Kompressionscontainers

MS-OVBAs Kompressionsformat verpackt Quellbytes in das, was die Spezifikation einen CompressedContainer nennt: ein einzelnes Signaturbyte, das gleich 0x01 sein muss, gefolgt von einer Folge von CompressedChunk-Blöcken, jeder deckt bis zu 4096 Bytes dekomprimierter Daten ab. Ein 16-Bit-Chunk-Header trägt drei Felder — eine 3-Bit-Signatur, die gleich 3 sein muss, ein 12-Bit-Größenfeld und ein CompressedChunkFlag-Bit, das markiert, ob die Nutzlast des Chunks literale Bytes oder eine token-komprimierte Sequenz ist. Wenn das Flag gesetzt ist, ist die Nutzlast eine Folge von Flag-Byte-präfixierten Gruppen von jeweils acht Tokens, und jedes Token ist entweder ein einzelnes literales Byte oder ein CopyToken: eine Offset/Länge-Rückreferenz zu bereits früher im selben Chunk dekomprimierten Bytes, wobei sich die Bitbreiten-Aufteilung zwischen Offset und Länge danach richtet, wie weit der Dekompressor gerade im Chunk steht. Dieser Teil von MS-OVBA (§2.4.1, Compression and Decompression) ist die Stelle, an der eine handgestrickte Implementierung am häufigsten einen Tag an einem Off-by-one in dieser Bitbreiten-Berechnung verliert

Warum HotXLS rohe Chunks schreibt statt Tokens abzugleichen

HotXLSs Schreibpfad umgeht die Token-Abgleich-Hälfte dieses Algorithmus vollständig. Wenn es ein bearbeitetes Modul neu komprimiert, geht jeder Chunk mit gelöschtem CompressedChunkFlag hinaus, was bedeutet, dass der Chunk literale Bytes statt Rückreferenz-Tokens enthält — legal unter MS-OVBA, da ein komprimierter Container vollständig aus unkomprimierten Chunks bestehen darf, und es entfernt genau den Teil des Algorithmus, der am schwersten von Hand richtig zu bekommen ist: gültige Rückreferenzen zu finden und ein Offset/Länge-Paar in eine Bitbreite zu packen, die von der aktuellen Position innerhalb des Chunks abhängt. Der Kompromiss zeigt sich in der Dateigröße, nicht in der Korrektheit — ein umgeschriebener Modul-Stream landet nahe an der Größe seines Quelltexts plus einem Zwei-Byte-Header pro 4096-Byte-Block, nicht kleiner, wie es ein vollständig tokenkomprimierter Chunk wäre. Jeder Reader, der die Dekompressionsseite der Spezifikation implementiert, Excel eingeschlossen, öffnet das Ergebnis weiterhin korrekt, weil ein roher Chunk genauso gültig ein CompressedChunk ist wie ein tokenkomprimierter

Was HotXLS unangetastet lässt, wenn es ein Modul umschreibt

Rekompression ersetzt immer nur einen Teil des Modul-Streams. Jeder Modul-Stream speichert zuerst seinen Performance-Cache und zweitens seinen komprimierten Quellcode, und der dir-Stream des Projekts protokolliert genau, wo diese Teilung für jedes Modul in einem MODULEOFFSET-Eintrag liegt; HotXLS liest diesen Offset, behält jedes Byte davor exakt so, wie es sie vorgefunden hat, und baut nur den komprimierten Container ab dem Offset an neu auf

Der Quelltext selbst durchläuft die eigene Codepage des VBA-Projekts statt UTF-8 — dieselbe Legacy-Codepage, mit der Office das Projekt ursprünglich geschrieben hat. Eine SourceCode-Bearbeitung, die Zeichen außerhalb des Repertoires dieser Codepage einführt, wird still durch Best-Fit-Ersatzzeichen ersetzt, wenn HotXLS den String zurück in Bytes kodiert, nicht abgelehnt, sodass ein ungewöhnliches regionales Zeichen, das in einen Kommentar oder ein String-Literal fällt, der wahrscheinlichste Ort ist, den Verlust zu bemerken. Externe Referenzen und Bibliotheksbindungen innerhalb desselben Projekts folgen einem verwandten, aber separaten Erhaltungspfad, behandelt in dem begleitenden Artikel zur Erhaltung externer VBA-Links, und es lohnt sich, ihn zu lesen, bevor ein Umschreibe-Durchlauf ein Projekt anfasst, das auf andere Arbeitsmappen oder Typenbibliotheken verlinkt

Wie bekommt man die umgeschriebenen Makros zurück in eine Arbeitsmappe?

Nichts ruft den Rekompressionsschritt ausdrücklich auf — er läuft automatisch in dem Moment, in dem eine Arbeitsmappe oder ein eigenständiges VBA-Projekt gespeichert wird. TXLSVBAProject.ApplyChanges durchläuft jedes Modul, komprimiert diejenigen neu, deren SourceCode sich seit dem letzten Speichern geändert hat, und schreibt nur den Stream dieses Moduls um; sowohl das klassische TXLSWorkbook.SaveAs, wenn das Speicherziel das ursprüngliche Format der Datei beibehält, als auch das OOXML-TXLSXWorkbook.SaveAs für ein makrofähiges XLSM-Paket rufen es intern auf, bevor irgendetwas auf die Festplatte geschrieben wird, und SaveVBAProjectToFile ruft dieselbe Methode auf, wenn das Ziel eine losgelöste VBA-Projektdatei statt einer vollständigen Arbeitsmappe ist

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;

Alle drei Ziele teilen sich darunter dieselbe SourceCode- und ApplyChanges-Mechanik; der einzige echte Unterschied zwischen ihnen ist, welcher Speicheraufruf am Ende die Rekompression auslöst

Wo das immer noch scheitert

Zwei Fehlermodi sind häufig genug, um sich darauf vorzubereiten, bevor ein Umschreibe-Durchlauf gegen Produktionsdateien läuft. Ein digital signiertes VBA-Projekt hört auf, gültig signiert zu sein, in dem Moment, in dem sich sein Quellcode ändert, da die Signatur den Inhalt des Projekts abdeckt; HotXLS hat keine Möglichkeit, ein Projekt in Ihrem Namen erneut zu signieren, und Excel verwirft oder markiert die Signatur, wenn die Datei das nächste Mal öffnet, sodass ein signiertes Makro-Projekt einen nachgelagerten Neu-Signierungsschritt braucht, falls diese Signatur etwas ist, das Ihr Workflow tatsächlich prüft. Der zweite Fehlermodus gehört jedem, der versucht ist, dieses Kompressionsformat von Grund auf neu zu implementieren, statt eine Bibliothek zu verwenden, die es bereits handhabt: ein einziges falsches Bit in einem Chunk-Header, im Signatur-Nibble, im Größenfeld oder im Kompressions-Flag, erzeugt eine Datei, die Excel nicht öffnen will, meist hinter einer generischen Beschädigungswarnung, die keinen Hinweis gibt, welches Byte falsch war — genau die Fehlerklasse, die die zuvor beschriebene Roh-Chunk-Schreibstrategie vermeiden soll

Nichts davon erfordert, das Format zurückzuentwickeln, um es zu nutzen. Delphi- und C++Builder-Entwickler bekommen Lese- und Schreibzugriff auf SourceCode, MS-OVBA-konforme Rekompression und alle drei hier beschriebenen Rückschreibeziele als Teil der Standard-HotXLS-Komponente, zusammen mit dem Rest ihrer klassischen XLS- und OOXML-Arbeitsmappen-API