プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

IFileOperationでZIPファイルを展開する方法

確認環境: Windows 7 SP1 64bit, Visual Studio Community 2015 Update 3
IFileOperation関係の前の記事: IFileOperationで異なる種類の操作を登録した時のUI表示の話 (以前はC#のCOM相互運用を使っていました)
IFileOperation関係の次の記事: IFileOperation::NewItem()の使用例

背景

ExplorerでZIPファイルを展開出来るのならIFileOperationでも出来るのでは?
{展開, 解凍, decompress, extract, unzip}などでググっても全くヒットしないけど出来るのでは?
といろいろ試していたら出来ました。

注意点

IShellItem::BindToHandler method 中のBHID_EnumItemsの説明には"If the item is a folder"とフォルダの場合だけ記述されており、ZIPファイルの場合に取得できるかどうかは記述されていません。
そのため環境によっては上手くいかない可能性があります。

実際の処理

COMを扱うならC++から使う方が楽だと思ったので、今回からC++です。CComPtrによりリソースが解放されます。
ZIPファイルのパスからIShellItemを作成し、IShellItem::BindToHandlerでIEnumShellItemsを取得しているのがポイントです。

なお、BHID_StorageEnumを使った場合に違いがあるかは分かりませんでした。隠しフォルダーはどちらの場合でも展開されました。

#include<winsdkver.h>
#define WINVER _WIN32_WINNT_WIN7
#define _WIN32_WINNT _WIN32_WINNT_WIN7

#include<ShObjIdl.h>
#include<ShlGuid.h>
#include<atlbase.h>

void ExtractZip(LPCWSTR zipPath, LPCWSTR destFolderPath) {
    int hr;

    CComPtr<IShellItem> pShellItemZip;
    hr = SHCreateItemFromParsingName(zipPath, nullptr, IID_PPV_ARGS(&pShellItemZip));
    if (FAILED(hr)) { return; }

    CComPtr<IEnumShellItems> pEnumShellItems;
    hr = pShellItemZip->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(&pEnumShellItems));
    if (FAILED(hr)) { return; }

    CComPtr<IShellItem> pShellItemDestFolder;
    hr = SHCreateItemFromParsingName(destFolderPath, nullptr, IID_PPV_ARGS(&pShellItemDestFolder));
    if (FAILED(hr)) { return; }

    CComPtr<IFileOperation> pFileOperation;
    hr = pFileOperation.CoCreateInstance(CLSID_FileOperation);
    if (FAILED(hr)) { return; }

    hr = pFileOperation->CopyItems(pEnumShellItems, pShellItemDestFolder);
    if (FAILED(hr)) { return; }

    pFileOperation->PerformOperations();
}

int main() {
    if (SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) {
        ExtractZip(L"c:\\test\\test.zip", L"c:\\test\\target\\");
        CoUninitialize();
    }
}

展開例

例1: フォルダ1つを圧縮したZIPファイル

before:
c:\test
├test.zip
│└foo
│  ├bar
│  └baz
└target

after:
c:\test
├test.zip(内容は同じなので子要素省略)
└target
  └foo
    ├bar
    └baz

例2: 直下に複数のエントリーを含むZIPファイル

before:
c:\test
├test.zip
│├[Content_Types].xml
│├customXml(子要素略)
│├docProps(子要素略)
│├word(子要素略)
│└_rels(子要素略)
└target

after:
c:\test
├test.zip(内容は同じなので子要素省略)
└target
  ├[Content_Types].xml
  ├customXml(子要素略)
  ├docProps(子要素略)
  ├word(子要素略)
  └_rels(子要素略)

パスワードがかかったZIPを展開する場合 (2017/01/14 追記)

ZIPにパスワードがかかってる場合、自動的にプロンプトが表示されます。

正しいパスワードを入力した場合は正常に展開されます。キャンセルした場合はコピー全体がキャンセルされます。

ただしスキップした場合は次のダイアログが表示されます。

再試行を押すと同一ファイルのパスワード入力へ戻り、スキップを押すと次のファイルのパスワード入力に進みます。
ここで表示されているHRESULTの 0x800704DE は ERROR_CONTINUE の数値です。
邪魔でしか無いこのダイアログは IFileOperation::SetOperationFlags method で FOF_NOERRORUI を指定すると表示されなくなります。