Language: Deutsch English















Last Update: 2017 - 11 - 14





Wie man ZIP-Archive mit VBA und der Shell32-Bibliothek erstellt

von Philipp Stiefel, ursprünglich veröffentlicht am 14. März 2016


Article header image, Bündel von Briefen

Foto von Joanna Kosinska hier verwendet unter CC0 Lizensierung

Ich kann mir einige Szenarien vorstellen, in denen Du in deiner Access/Office-Anwendung ein komprimiertes ZIP-Archiv mit VBA erstellen möchtest. Kürzlich musste ich mehrere Dateien aus einer Anwendung per Email versenden. Natürlich kann man mehrere Dateien an eine Email anhängen. In meinem Fall allerdings waren die Dateien in einer (Unter-)Ordnerstruktur gespeichert. Diese Struktur sollte auch zum Empfänger übermittelt werden. Die einzige Möglichkeit dies zu erreichen, ist es, anstelle einzelner Dateien, die Dateien, samt Ordnerstruktur, in einem ZIP-Archiv zu versenden. Die geringere Dateigröße durch die Komprimierung ist natürlich ein weiterer Vorteil.

Möglichkeiten ein ZIP-Archiv zu erstellen

Es gibt viele verschiedene Anwendungen, um ein ZIP-Archiv zu erstellen. Einige davon sind sogar kostenlos. Allerdings ist es bei den meisten meiner Kunden nicht zulässig Fremdanwendungen oder -Bibliotheken zu installieren. Daher versuche ich eher alle Anforderungen mit Komponenten zu lösen, die im Lieferumfang von Windows enthalten sind. Seit Windows 2000 gibt es das „Senden an komprimierten Ordner“-Feature im Windows Explorer um Dateien in ein ZIP-Archiv zu kopieren.

Die meisten Anwendungen zur ZIP-Erstellung können über die Befehlszeile gesteuert werden. Zwar ist ein Befehlszeilen-Interface nicht die ideale „API“ um eine Anwendung zu steuern, aber es ist definitiv geeignet, um einen einfachen Prozess wie das Hinzufügen von Dateien zu einem ZIP-Archiv zu starten. – Aber mit dem „Senden an komprimierten Ordner“-Feature im Windows Explorer gibt es ein Problem. - Es hat kein Befehlszeilen-Interface.

Ein ZIP-Archiv mit der Windows Shell erzeugen

Da eine Fremdanwendung keine Option für mich ist, muss ich das verwenden, was mit Windows „out-of-the-box“ direkt verfügbar ist. Also konzentriere ich mich auf die Shell32-Bibliothek. – Direkt vom Start weg ist das ein etwas steiniger Weg.

Das „Senden an komprimierten Ordner“-Feature ist in den Windows Explorer integriert, daher sollte es mit der Shell32-Bibliothek automatisierbar sein. Allerdings gibt es kein Objekt und keine Methode in der Bibliothek, mit der man ein neues ZIP-Archiv erstellen kann.

Aber ein leeres Archiv zu erzeugen kann ja nicht so schwierig sein, oder?

Das ZIP-Dateiformat ist so populär, dass es sogar auf Wikipedia dokumentiert ist, wenn auch nur auf Englisch. Die Daten, die in jeder ZIP-Datei mindestens vorhanden sein müssen, stellen den End of central directory record (EOCD) dar. Dies ist ein 22 Byte großer Bereich in der Datei, der beschreibt, was in dem ZIP-Archiv enthalten ist. Da wir ja eine leere ZIP-Datei erstellen wollen, sind keine Dateien darin enthalten. Also müssen wir nur eine neue Datei erzeugen und die Header-Daten des EOCD dort hineinschreiben. Der Header beginnt mit vier Bytes, die die End of central directory Signatur darstellen; diese sind 0x06054b50. In einem leeren ZIP-Archiv sind die verbleibenden 18 Bytes dieser Datenstruktur mit Nullen gefüllt.

Wir können also ein leeres ZIP-Archiv erstellen, indem wir einfach nur diese 22 Bytes in eine neue Datei schreiben.

Public Sub createZipFile(ByVal fileName As String) Dim fileNo As Integer Dim ZIPFileEOCD(22) As Byte 'Signature of the EOCD: &H06054b50 ZIPFileEOCD(0) = Val("&H50") ZIPFileEOCD(1) = Val("&H4b") ZIPFileEOCD(2) = Val("&H05") ZIPFileEOCD(3) = Val("&H06") fileNo = FreeFile Open fileName For Binary Access Write As #fileNo Put #fileNo, , ZIPFileEOCD Close #fileNo End Sub

Mit diesen wenigen und einfachen Codezeilen erzeugen wir also eine neue, leere ZIP-Datei.

Dateien zum ZIP-Archiv hinzufügen

Jetzt, nachdem wir ein neues ZIP-Archiv erstellt haben, ist es tatsächlich ziemlich einfach, Dateien zu dem Archiv hinzuzufügen. Wir verwenden die Namespace-Methode des Shell.Application-Objektes um zum einen eine Referenz auf die ZIP-Datei und zum anderen eine Referenz auf die Datei oder den Ordner den wir hinzufügen wollen, zu erhalten. Danach können wir die CopyHere-Methode des Shell.Folder-Objektes verwenden, um Dateien zu unserem ZIP-Archiv hinzuzufügen.

Public Sub AddToZip(ByVal zipArchivePath As String, ByVal addPath As String) Dim sh As Object Dim fSource As Object Dim fTarget As Object Dim iSource As Object Dim sourceItem As Object Dim i As Long Set sh = CreateObject("Shell.Application") Set fTarget = sh.NameSpace((zipArchivePath)) If fTarget Is Nothing Then createZipFile zipArchivePath Set fTarget = sh.NameSpace((zipArchivePath)) End If Dim containingFolder As String Dim itemToZip As String containingFolder = Left(addPath, InStrRev(addPath, "\")) itemToZip = Mid(addPath, InStrRev(addPath, "\") + 1) Set fSource = sh.NameSpace((containingFolder)) For i = 0 To fSource.Items.Count - 1 If fSource.Items.Item((i)).Name = itemToZip Then Set sourceItem = fSource.Items.Item((i)) Exit For End If Next i fTarget.CopyHere sourceItem End Sub

Das Argument addPath in meiner Implementation kann entweder der Pfad eines Ordners oder einer Datei sein. Wenn das ZIP-Archiv noch nicht existiert, dann wird es erstellt.

Diese Codezeilen fügen also eine Datei (oder einen Ordner) zu unserem ZIP-Archiv hinzu. Wenn du dir den Code aufmerksam ansiehst, fällt dir wahrscheinlich auf, dass er etwas merkwürdig aussieht.

Ich hätte die Item-Elemente des Quellordners direkt mit ihrem (Datei-)Name ansprechen können, anstelle alle Elemente mit dem Index zu durchlaufen und dann den Elementnamen mit dem der gewünschten Datei zu vergleichen. – Nun ja, die direkte Ansprache über den Dateinamen funktioniert manchmal, aber scheint mit langen Dateinamen immer fehlzuschlagen. – Möglicherweise ist dieser Zugriff auf die altertümliche 8+3-Syntax für Dateinamen limitiert. Da meine Variante stabil und zuverlässig zu funktionieren scheint, habe ich mir nicht die Mühe gemacht, das genau zu erforschen.

Die andere eigenartige Sache ist, dass ich alle Variablen, die ich an die Methoden der Shell32-Bibliothek übergebe, in extra Klammern einschließe. Indem ich das tue, erzwinge ich, dass diese Argumente ByVal an die Methode übergeben werden. Durch die Klammern werden sie zu einem Ausdruck, der vor der Übergabe an die Methode evaluiert werden muss und dessen Ergebnis in einer neuen, internen Variable (Speicherbereich) zwischengespeichert wird. Aus Gründen die mein Verständnis überfordern, akzeptieren die Methoden der Shell32-API die Argumente nur dann, wenn sie entweder als hartkodierte Konstanten oder ausdrücklich ByVal, wie ich es in meinem Code mache, übergeben werden.

Jetzt wird der Code ausgeführt und tut was wir möchten. Lass uns jetzt einem Moment damit beschäftigen, wie er das tut. Du wirst dabei zwei Dinge feststellen. Zum einem, dass ein Fortschrittsdialog eingeblendet wird, der es dem Benutzer ermöglicht die Operation abzubrechen. – Unschön, aber damit kann ich leben.

Fortschrittsdialog der ZIP-Operation

Du siehst diesen Fortschrittsdialog nur, wenn größere Dateien zu dem Archiv hinzugefügt werden. Andernfalls ist die Operation schon abgeschlossen, bevor der Dialog überhaupt eingeblendet werden kann.

Das andere Problem, dass du bei der Ausführung bemerken könntest, ist, dass die CopyHere-Methode asynchron arbeitet. Die Methode kehrt sofort zum Aufrufer zurück, aber die Dateioperation ist dann noch gar nicht abgeschlossen. Also kann dein Code nicht wissen, wann alle gewünschten Dateien in das ZIP-Archiv aufgenommen wurden und die Operation wirklich fertig ist.

Aktualisieren/Bearbeiten eines ZIP-Archivs

Es gibt keine wirklich brauchbaren Methoden in der API der Shell32-Bibilothek um ein ZIP-Archiv zu aktualisieren oder zu bearbeiten. Wenn wir versuchen eine Datei zu einem Archiv hinzuzufügen, die bereits mit dem gleichen Dateinamen darin enthalten ist, dann löst das einfach einen Laufzeitfehler aus. Es gibt keine Option, mit der man festlegen kann die bestehende Datei zu überschreiben. Die rohe API der Shell32.dll kennt zwar die Option FOF_RENAMEONCOLLISION, die eigentlich Dateien, die bereits im Zielverzeichnis existieren, mit einem geänderten Namen dennoch hinzufügt, aber dies funktioniert nicht mit Dateien in einem ZIP-Archiv.

Meine Idee diese Einschränkung zu umgehen war, zu prüfen ob die Datei bereits im Archiv existiert und sie dann zu löschen, bevor sie erneut hinzugefügt wird. Das Überprüfen funktioniert zwar einfach über die FolderItems im Archiv, aber es ist nicht möglich die Datei aus dem Archiv zu löschen. Jede mir bekannte Methode um Dateien zu löschen, funktioniert nicht für eine Datei innerhalb eines ZIP-Archivs.

Die einzige Möglichkeit um eine Datei in einem ZIP-Archiv zu löschen ist es, das Delete-Verb des Shell.FolderItems aufzurufen. Das „funktioniert“, aber nur in so weit, dass es das Löschen der Datei durch den Benutzer simuliert, was natürlich eine Bestätigung der Löschoperation durch den Benutzer über einen Dialog anfordert. - Dies kann also nicht in einem Szenario ohne Benutzerinteraktion verwendet werden. Und selbst wenn ein Benutzer die gesamte ZIP-Operation gestartet hat, könnte er irritiert sein, warum er einen Löschvorgang bestätigen soll, den er selbst gar nicht direkt vorgenommen hat und daher nicht erwartet.

Public Sub DeleteFileWithInvokeVerb(ByVal zipArchivePath As String, ByVal deleteFileName As String) Dim sh As Object Dim fTarget As Object Dim iSource As Object Dim targetItem As Object Dim i As Long Set sh = CreateObject("Shell.Application") Set fTarget = sh.NameSpace((zipArchivePath)) For i = 0 To fTarget.Items.Count - 1 If fTarget.Items.Item((i)).Name = deleteFileName Then Set targetItem = fTarget.Items.Item((i)) Exit For End If Next i If Not targetItem Is Nothing Then targetItem.InvokeVerb "Delete" End If End Sub

Confirm delete dialog on InvokeVerb (delete)

Dateien aus einem ZIP-Archiv extrahieren

Dateien aus einem Archiv zu extrahieren ist einfach. Du setzt eine Shell32.Folder-Referenz auf das ZIP-Archiv und eine weitere auf einen Zielordner im Dateisystem und rufst dann wieder die CopyHere-Methode auf, um die Dateien des ZIP-Archivs in den Zielordner zu extrahieren.

Public Sub UnZip(ByVal zipArchivePath As String, ByVal extractToFolder As String) Dim sh As Object Dim fSource As Object Dim fTarget As Object Set sh = CreateObject("Shell.Application") Set fSource = sh.NameSpace((zipArchivePath)) Set fTarget = sh.NameSpace((extractToFolder)) fTarget.CopyHere fSource.Items End Sub

Diese Implementierung extrahiert alle Dateien aus dem ZIP-Archiv. Du kannst das einfach ändern indem du nur ein einzelnes FolderItem aus dem Archiv an die Methode übergibst, anstelle der kompletten Auflistung.

Andere Entwickler haben beobachtet, dass während der Extraktion ein temporärer Ordner angelegt wurde, der anschließend von der Shell32-Komponente nicht wieder gelöscht wurde. Allerdings konnte ich dieses Verhalten nicht reproduzieren und daher wird es in meiner Implementierung der UnZip-Funktionalität auch nicht behandelt. Du musst dich selbst darum kümmern, wenn du dies behandeln willst.

Einschränkungen der Shell32-API für ZIP-Archive

Lass uns hier dieses Thema mit einer Zusammenfassung der Dinge die funktionieren und derjenigen, die nicht (gut) funktionieren, abschließen.

Funktioniert

  • Ein neues ZIP-Archiv erstellen
  • Dateien zum ZIP-Archiv hinzufügen
  • Dateien aus dem ZIP-Archiv extrahieren

Funktioniert nicht (gut)

  • Es ist nicht möglich den Fortschrittsdialog beim Hinzufügen von Dateien zu unterbinden
  • Die Komprimierungsoperation ist asynchron und es lässt sich nicht sicher feststellen, wann sie fertig ist.
  • Es ist nicht möglich eine bestehende Datei in einem Archiv zu aktualisieren
  • Es ist nicht möglich eine Datei aus einem Archiv zu löschen, ohne dass der Benutzer dies bestätigt.
  • Du kannst kein Passwort für dein ZIP-Archiv definieren und es ist nicht möglich Dateien aus einem passwortgeschütztem Archiv zu extrahieren.
Share this article: Share on Facebook Tweet Share on LinkedIn Share on XING

Abonniere meinen Newsletter

*

Ich werde Deine Email-Addresse niemals weitergeben. Du kannst den Newsletter jederzeit abbestellen.



© 1999 - 2017 by Philipp Stiefel