Last Update: 2018 - 02 - 27
How to create ZIP-Archives with VBA and the Shell32 library
by Philipp Stiefel, originally published March 14th, 2016
I can imagine several scenarios where you might need to create a compressed ZIP-Archive with VBA code in your application. Recently I needed to send several files via email from an application. Of course you can add multiple files as attachments to an email message. However, in my case these files were arranged in a sub-folder hierarchy. This hierarchy should be transferred to the email recipient. The only option to achieve this, is to send the files in a ZIP-Archive instead as individual files. The reduced file size of the zipped files is an additional benefit of course.
Ways to create an ZIP-Archive
There are many different applications to zip files manually, some of them are even free. However, with most of my clients, installing a third party library or application is a no-go. So I rather try to use components included with Windows to solve any problem. Since Windows 2000 there is the “Send to compressed (zipped) folder” feature available in Windows Explorer to store files in a ZIP-Archive.
Most of the third party applications can be automated via a command line interface. While a command line interface is not ideal as an API to automate an application, it definitely is usable to start a simple process like adding files to a ZIP folder. – But with the “Send to compressed folder” feature integrated in Windows Explorer, there is a problem though. It has no command line interface.
Creating a ZIP archive with the Windows Shell
So as using any third party application or library is not an option for me, I needed to use what is available with Windows out of the box. So I focus on the Shell32-Library. - Right from the beginning that’s a bit of rocky path.
“Send to compressed folder” is integrated into Windows Explorer. So it should be automatable with the Shell32.dll-Library. However, there is no object or method included in that library to create a new ZIP-Archive.
But to create an empty ZIP-Archive can’t be that difficult, can it?
The ZIP file format is so popular, it is even documented on Wikipedia. The minimal data that needs to be in every ZIP file is the End of central directory record (EOCD). This is a 22 bytes (at least) area in the file, which describes what is actually included in the ZIP Archive. As we want to create an empty ZIP- Archive, there are no files in there. So we only need to create a new file and write the header information of the EOCD. The header starts with four bytes, which are the End of central directory signature. Those are 0x06054b50. In an empty ZIP-file the remaining 18 bytes of that record are just zeroes.
So we can create an empty ZIP file by just writing those 22 bytes to a new file.
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
So with these few simple lines of code, we can create a new, empty ZIP file.
Adding files to an ZIP-Archive
Now, after we created a ZIP file, it’s actually pretty easy to add files to it. We use the Namespace method of the Shell.Application-Object to get a reference to the ZIP-Folder-Archive and to the normal files system folder containing the files or folders we want to add to the archive. After that, we can add files to our newly created ZIP archive by using the CopyHere-Method of the Shell.Folder referencing our ZIP-Archive.
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
The argument addPath in my implementation may be the full path to either a file or a folder. If the ZIP-Archive referenced by
So these lines of code add a file to our ZIP archive. If you pay attention, the code looks somewhat weird. I could have addressed the item within the source folders Items by name directly instead of using an index and comparing the name to the item I want to ZIP. – Well, that direct approach works sometimes, but it fails with long file names. – Maybe it’s limited to the ancient 8+3-Syntax for file names. I did not bother to find out, as my implementation seems to work reliably.
The other weird thing is me using extra brackets around the variables I pass to the methods of the Shell32-API. By doing that I force them to be passed ByVal to the API, as the expression in the brackets needs to be evaluated and the result stored in a new, internal variable (memory area). – For reasons beyond my comprehension, the Shell32-API accepts the arguments only either as hardcoded constants or explicitly passed ByVal in the way I used in the sample code.
Now, the code executes and does what we want it to. But let’s take a closer look on how it does it. You will notice two things then. First a progress dialog appears that allows the user to cancel the operation. - Not nice, but I can live with that.
You see the progress dialog only with larger files. Otherwise the operation will complete before the dialog becomes visible.
The other problem you will notice is that the CopyHere-method it is executed asynchronously. The method returns immediately, but the file copy operation to the ZIP-Archive is not finished then. So your code does not know when the file(s) has been completely written to the ZIP-Archive.
Updating/Editing a ZIP-Archive with the Shell32-Library
There are no usable methods for editing or updating a ZIP file available in the API. If we try to add a file to the archive that is already in there (any file with the same file name) we simply get an error and cannot add the file. There is no option to specify to overwrite the file. While the raw Shell-API explicitly has the option FOF_RENAMEONCOLLISION, which should add files already in the target folder with a new name, but this does not work for files within a ZIP-Archive.
My idea to circumvent this limitation was to check if that file exists already in the ZIP and the deleting it. The checking part works by checking the FolderItems inside, but it is not possible to delete the file. Any method I tried to delete a file, did not work for files inside a ZIP-Archive.
The only method to delete a file there is to invoke the Delete-verb of the Shell.FolderItem. This does “work”, but only as if the user deleted the file manually, which triggers a confirm delete dialog. – So this cannot be used in an unattended scenario. And even if the user started the whole operation, he might be confused by the unexpected question to confirm a file delete.
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
Extracting the files from the ZIP-Archive is straight forward and pretty simple. You just set a Shell32.Folder reference to the ZIP-Archive and another one to the target folder in the file system and finally call the CopyHere-Method of the target folder to extract the files.
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
This implementation extracts all files form the archive. You can easily change that to extract only specific files.
Other people observed this operation creating a temporary folder for the extraction that was not deleted automatically by the Shell32-Component. However, I wasn’t able to reproduce this behavior, so it is not dealt with in my Implementation. You might need to take care of that yourself if you want to.
Limitations of the Shell32 API for ZIP Archive
So let’s wrap up this topic with a summary of things that do work and those that do not work.
Does no work (well)
© 1999 - 2017 by Philipp Stiefel