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

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

IFileOperation::NewItem()の使用例

確認環境: Windows 7 SP1 64bit, Microsoft Visual Studio Community 2017(VisualStudio/15.0.0+26228.9)
IFileOperation関係の前の記事: IFileOperationでZIPファイルを展開する方法
IFileOperation関係の次の記事: FOFX_RECYCLEONDELETEやFOFX_ADDUNDORECORDの振る舞いの実験

この記事は IFileOperation::NewItem method の使用例や特徴について記述しています。

2017/03/26(日): 「実際に作成されたファイル/ディレクトリの名前を知る方法」節のサンプルコードの実装を Windows-classic-samples/ProgressSinkSampleApp.cpp at master · Microsoft/Windows-classic-samples を参考に修正。
2017/03/29(水): main()の実装を、~CComPtr()の後にCoUninitialize()を呼び出すよう修正。

空ファイルの作成例

NewItem()のdwFileAttributes引数に、FILE_ATTRIBUTE_DIRECTORYフラグを与えなければファイル作成になります。
PerformOperations()を呼び出すタイミングで実際に作成されます。
なおExplorerの新規作成では".config"などドット始まりのファイルは作成できませんが、NewItem()では問題なく作成できます。

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

void NewItemTest(::IFileOperation *pFileOperation, ::IShellItem *pTargetFolderShellItem) {
    HRESULT hr = S_OK;

    hr = pFileOperation->NewItem(
        pTargetFolderShellItem,
        FILE_ATTRIBUTE_NORMAL,
        L"test.txt",
        nullptr,
        nullptr);
    if (FAILED(hr)) { return; }

    hr = pFileOperation->PerformOperations();
    if (FAILED(hr)) { return; }
}

int main() {
    HRESULT hr = S_OK;
    hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
    if (SUCCEEDED(hr)) {
        {
            ::ATL::CComPtr<::IFileOperation> pFileOperation;
            hr = pFileOperation.CoCreateInstance(CLSID_FileOperation);
            if (SUCCEEDED(hr)) {
                ::ATL::CComPtr<::IShellItem> pTargetFolderShellItem;
                hr = ::SHCreateItemFromParsingName(LR"(c:\test\)", nullptr, IID_PPV_ARGS(&pTargetFolderShellItem));
                if (SUCCEEDED(hr)) {
                    NewItemTest(pFileOperation, pTargetFolderShellItem);
                }
            }
        }
        CoUninitialize();
    }
}

空ディレクトリの作成例

NewItem()のdwFileAttributes引数に、FILE_ATTRIBUTE_DIRECTORYフラグを与えるとディレクトリ作成になります。
(この事にしばらく気付かず、ファイル作成専用関数だと思っていました。)

// #includeの内容やmain()の中身は上のサンプルコードと同じ、以降のサンプルコードでも同様
void NewItemTest(::IFileOperation *pFileOperation, ::IShellItem *pTargetFolderShellItem) {
    HRESULT hr = S_OK;

    hr = pFileOperation->NewItem(
        pTargetFolderShellItem,
        FILE_ATTRIBUTE_DIRECTORY,
        L"test",
        nullptr,
        nullptr);
    if (FAILED(hr)) { return; }

    hr = pFileOperation->PerformOperations();
    if (FAILED(hr)) { return; }
}

管理者権限昇格ダイアログ

"C:\Program Files"以下など、権限が必要な場所をpsiDestinationFolder引数で指定した場合は次のダイアログが表示されます:

キャンセルを選択すると、PerformOperations()がCOPYENGINE_E_USER_CANCELLED(0x80270000)を返します。
スキップを選択すると、次の内容を処理します。
続行を選択すると、今度はUACのダイアログが表示されます:

いいえを選択すると、PerformOperations()がERROR_CANCELLED(0x800704C7)を返します。
はいを選択すると、晴れて実際に作成されます。

同名のファイル/ディレクトリが既に存在したときの挙動

pszName引数に指定した名前のファイルまたはディレクトリが存在した場合、末尾に(2)などが付与された名前で作成されます。
この挙動はSetOperationFlags()でFOF_RENAMEONCOLLISIONフラグを立てていてもいなくても同じです。確認ダイアログも表示されません。

void NewItemTest(::IFileOperation *pFileOperation, ::IShellItem *pTargetFolderShellItem) {
    HRESULT hr = S_OK;

    for (int i = 0; i < 6; ++i) {
        hr = pFileOperation->NewItem(
            pTargetFolderShellItem,
            i < 3 ? FILE_ATTRIBUTE_NORMAL : FILE_ATTRIBUTE_DIRECTORY,
            L"test.txt",
            nullptr,
            nullptr);
        if (FAILED(hr)) { return; }
    }

    hr = pFileOperation->PerformOperations();
    if (FAILED(hr)) { return; }
}

上記のサンプルコードを実行すると、次の名前で生成されます:

test.txt ファイル
test (2).txt ファイル
test (3).txt ファイル
test.txt (2) ディレクトリ
test.txt (3) ディレクトリ
test.txt (4) ディレクトリ

試しにpszNameに"NUL"(Windowsの予約デバイス名)を指定すると、実際は"NUL (2)"という名前で作成されました。これも同じ理由でしょう。

実際に作成されたファイル/ディレクトリの名前を知る方法

前述したように、同名のファイル/ディレクトリが存在した場合はpszName引数に指定したものとは異なる名前で作成されます。
IFileOperationの前身であるSHFileOperationでは SHFILEOPSTRUCT structure のfFlagsにFOF_WANTMAPPINGHANDLEフラグがあり SHNAMEMAPPING structure から変更後の名前を取得できるようです。
IFileOperation::SetOperationFlags()のドキュメントにはFOF_WANTMAPPINGHANDLEフラグはありませんが、代わりにAdvise()やNewItem()などに IFileOperationProgressSink interface を渡せて、それを通じて通知を受け取ることが出来ます。
今回の目的の場合は IFileOperationProgressSink::PostNewItem method のpszNewName引数から、実際に作成された名前を取得できます

長いサンプルコードになっていますが、内容はIUnknownの実装とIFileOperationProgressSinkのputs付き実装、それにPostNewItem()でpszNewNameを保持しているだけです。
NewItem()のpfopsItem引数にIFileOperationProgressSinkを渡す場合は、IFileOperationProgressSinkのPreNewItem(), PostNewItem()だけが呼ばれるようです。

#include<cstdio>
#include<string>

class MyFileOperationProgressSink :
    public ::IFileOperationProgressSink {
public:
    MyFileOperationProgressSink() = default;
    virtual ~MyFileOperationProgressSink() = default;
    MyFileOperationProgressSink(const MyFileOperationProgressSink&) = delete;
    MyFileOperationProgressSink& operator = (const MyFileOperationProgressSink&) = delete;

    STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject) override {
        // ::QITAB::dwOffsetはintなのに、マクロがDWORDを格納しようとして警告出るのどうにかならないのか……
        static const ::QITAB qit[] = {
            QITABENT(MyFileOperationProgressSink, IFileOperationProgressSink),
            {},
        };
        return ::QISearch(this, qit, riid, ppvObject);
    }
    STDMETHODIMP_(ULONG) AddRef() override {
        return ::InterlockedIncrement(&this->_refCount);
    }
    STDMETHODIMP_(ULONG) Release() override {
        auto result = ::InterlockedDecrement(&this->_refCount);
        if (result == 0) {
            delete this;
        }
        return result;
    }
    STDMETHOD(StartOperations)() override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(FinishOperations) (HRESULT hrResult) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PreRenameItem)(DWORD dwFlags, IShellItem * psiItem, LPCWSTR pszNewName) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PostRenameItem)(DWORD dwFlags, IShellItem * psiItem, LPCWSTR pszNewName, HRESULT hrRename, IShellItem * psiNewlyCreated) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PreMoveItem)(DWORD dwFlags, IShellItem * psiItem, IShellItem * psiDestinationFolder, LPCWSTR pszNewName) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PostMoveItem)(DWORD dwFlags, IShellItem * psiItem, IShellItem * psiDestinationFolder, LPCWSTR pszNewName, HRESULT hrMove, IShellItem * psiNewlyCreated) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PreCopyItem)(DWORD dwFlags, IShellItem * psiItem, IShellItem * psiDestinationFolder, LPCWSTR pszNewName) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PostCopyItem)(DWORD dwFlags, IShellItem * psiItem, IShellItem * psiDestinationFolder, LPCWSTR pszNewName, HRESULT hrCopy, IShellItem * psiNewlyCreated) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PreDeleteItem)(DWORD dwFlags, IShellItem * psiItem) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PostDeleteItem)(DWORD dwFlags, IShellItem * psiItem, HRESULT hrDelete, IShellItem * psiNewlyCreated) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PreNewItem)(DWORD dwFlags, IShellItem * psiDestinationFolder, LPCWSTR pszNewName) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PostNewItem)(DWORD dwFlags, IShellItem * psiDestinationFolder, LPCWSTR pszNewName, LPCWSTR pszTemplateName, DWORD dwFileAttributes, HRESULT hrNew, IShellItem * psiNewItem) override {
        puts(__func__);
        this->_newName = pszNewName;
        return S_OK;
    }
    STDMETHOD(UpdateProgress)(UINT iWorkTotal, UINT iWorkSoFar) override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(ResetTimer)() override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(PauseTimer)() override {
        puts(__func__);
        return S_OK;
    }
    STDMETHOD(ResumeTimer)() override {
        puts(__func__);
        return S_OK;
    }

    std::wstring GetNewName() { return this->_newName; }

private:
    ULONG _refCount = 1;
    std::wstring _newName;
};

void NewItemTest(::IFileOperation *pFileOperation, ::IShellItem *pTargetFolderShellItem) {
    HRESULT hr = S_OK;

    ::ATL::CComPtr<::MyFileOperationProgressSink> pSink;
    pSink.Attach(new MyFileOperationProgressSink{});

    hr = pFileOperation->NewItem(
        pTargetFolderShellItem,
        FILE_ATTRIBUTE_NORMAL,
        L"test.txt",
        nullptr,
        pSink);
    if (FAILED(hr)) { return; }

    hr = pFileOperation->PerformOperations();
    if (FAILED(hr)) { return; }

    _putws(pSink->GetNewName().c_str()); // "test (2).txt"などが出力されます
}

template fileの指定

NewItem()にはpszTemplateNameという引数があります。
MSDNドキュメントによると、3種類のディレクトリのいずれかにファイルを置いてその名前を指定すると、それと同じ内容を持つファイルを作成できるとのことです。
ただし自分の環境では "%ALLUSERSPROFILE%\Templates"も"%USERPROFILE%\Templates"も、Explorerでは表示されず、コマンドプロンプトではcdで移動できるものの中にファイルを作ろうとするとアクセスが拒否されました。
"%SystemRoot%\shellnew"(=自分の環境では"C:\Windows\shellnew")は通常通りExplorerで作成、移動出来たので、そこに適当な内容を持つ"TemplateSample.txt"を設置しました。
その状態で次のサンプルコードを実行すると、"%SystemRoot%\shellnew\TemplateSample.txt"と同一の内容を持つファイルを作成できました。

void NewItemTest(::IFileOperation *pFileOperation, ::IShellItem *pTargetFolderShellItem) {
    HRESULT hr = S_OK;

    hr = pFileOperation->NewItem(
        pTargetFolderShellItem,
        FILE_ATTRIBUTE_NORMAL,
        L"test.txt",
        L"TemplateSample.txt",
        nullptr);
    if (FAILED(hr)) { return; }

    hr = pFileOperation->PerformOperations();
    if (FAILED(hr)) { return; }
}

pszTemplateNameに存在しない名前を指定した場合は、確認ダイアログやエラーダイアログもなく0byteのファイルが作成されてPerformOperations()はS_OKを返しました。nullptrを渡した場合と全く同じ動作です。
またpszNameとpszTemplateNameに異なる拡張子のファイルを指定した場合でも、問題なくtemplate fileの内容で作成されました。
"%SystemRoot%\shellnew"以下にディレクトリを作成してその下にファイルを置き、pszTemplateNameに相対パス形式で指定してもtemplate fileの内容で作成されました。
それどころか"..\\wininit.ini"と上位ディレクトリのファイルを指定してもtemplate fileの内容で作成されました。
"C:\\Windows\\wininit.ini"という絶対パス表記すら可能でした。
"%SystemRoot%\\wininit.ini"と環境変数を含んでいるものは0byteのファイルが作成されました。