Programming Field - プログラミング Tips

仮想フォルダを既存フォルダのようにしてしまう(Shell Namespaceの簡易利用)

ここでは、シェル名前空間(Shell Namespace)に仮想フォルダを作成し、それをあたかも既存フォルダのようにしてしまう、という手順を紹介します。これは本サイトで配布している「MyFolder」に使用しています。また、Windows 上では「マイ ドキュメント」がこれを行っています。

シェル名前空間に仮想フォルダを置くには、最低限 IShellFolder インターフェイスIPersistFolder インターフェイスを実装する必要があります。詳しい実装方法やレジストリの登録方法は省略します。

ここで既存フォルダと同じような動作をさせるのに手っ取り早い方法は、『既存フォルダの IShellFolder インターフェイスを取得しておき、仮想フォルダで実装している IShellFolder インターフェイスの各メソッドが呼び出されたときに、対応する既存フォルダのメソッドを呼び出してやる(カバーする)』という方法です。IShellView インターフェイスなどの知識や実装も必要無くなるので楽です。

ただこれでは仮想フォルダの「個性」がなくなってしまうので、「個性」をつけるために GetDisplayNameOf メソッドは独自の実装をつけておくべきです。独自の実装は、第一引数の LPCITEMIDLIST 型データが「仮想フォルダ自身」を指しているかどうかを判定して、そうであれば仮想フォルダの名前を返す、という感じです。仮想フォルダ自身を指しているかどうかの判定は、LPCITEMIDLIST がリストとして何も要素を持っていない (mkid.cb == 0) かどうかで十分だと思われます。

これだけすれば、大抵の処理は既存フォルダ(の IShellFolder)がやってくれます。

ところが1つだけ問題があります。「エクスプローラ」のようにフォルダツリーを表示して仮想フォルダを開き、仮想フォルダ内のサブフォルダを開くと、ツリー上では「仮想フォルダ内のサブフォルダ」ではなく、「元のフォルダのサブフォルダ」が開いてしまいます。一方、「マイ ドキュメント」は仮想フォルダでありながら、感覚的には普通のフォルダそのものの扱い方ができ、当然「元のフォルダのサブフォルダ」が開く問題も起こりません。

そこで、上記で挙げた問題を回避するために IPersistFolder3 インターフェイスを使います(※)。どう使用するかというと、既存フォルダの IShellFolder インターフェイスから IPersistFolder3 インターフェイスを取得し、初期化を行います。この初期化のときに使用する PERSIST_FOLDER_TARGET_INFO 構造体の pidlTargetFolder メンバに元のフォルダの LPITEMIDLIST、初期化メソッド InitializeEx の第二引数に仮想フォルダの LPITEMIDLIST を渡してあげることで、「既存フォルダのサブフォルダの親」=「仮想フォルダ」と認識させることが出来るようになり、フォルダツリーも上手く動くようになります。

IPersistFolder3 はおそらく Windows 2000/Me 以降でのみ使用可能です。

ただし、ここでもう1つ問題があります。SHGetDesktopFolder 関数から子フォルダを探していって目的となる既存フォルダの IShellFolder インターフェイスを取得した場合、このインターフェイスは既に初期化済みであるため、IPersistFolder3 インターフェイスを取得して初期化しようとしてもエラーが起きます。

そこで、初期化をしていない IShellFolder インターフェイスを取得する手段として、SHCoCreateInstance 関数を使用します。この関数は Windows 95 以降で使用できます。(名前が公開されたのは Windows 2000 あたり。)

この辺は実際のコードを見てもらうと分かりやすいと思うので、以下のコードをご覧ください。MyCreateFolderObject 関数を用いることで、細工を施した既存フォルダの IShellFolder インターフェイスが取得できます。

// MyCreateFolderObject - フォルダオブジェクトを作成
// szPath: 元フォルダのパス
// lpidlFolder: 元フォルダの ITEMIDLIST
// lpidlRootFolder: 仮想フォルダの ITEMIDLIST
IShellFolder* MyCreateFolderObject(LPCWSTR szPath,
    LPITEMIDLIST lpidlFolder, LPITEMIDLIST lpidlRootFolder)
{
    IUnknown* lpUnknown;
    IShellFolder* lpSFolder;
    IPersistFolder* lpPFolder;
    IPersistFolder3* lpPFolder3;
    // CLSID_ShellFSFolder はエクスプローラで使う
    // フォルダオブジェクトの CLSID
    // (いきなり IID_IShellFolder を使ってインターフェイスを
    // 取得しても問題ないはずだけど、念のため IUnknown で)
    // CLSID は一番目か二番目の引数どちらかで指定すればOK
    // (一番目で指定する場合は "{XXXXXXXX-...}" の形式)
    hr = SHCoCreateInstance(NULL, &CLSID_ShellFSFolder, NULL,
        IID_IUnknown, (void**) &lpUnknown);
    if (FAILED(hr))
        return NULL;
    // IPersistFolder3 が使えるかどうかチェックする
    // (Windows 98 などでは使えないはず)
    hr = lpUnknown->QueryInterface(IID_IPersistFolder3,
        (void**) &lpFolder3);
    if (SUCCEEDED(hr))
    {
        PERSIST_FOLDER_TARGET_INFO pfti;
        wcsncpy(pfti.szTargetParsingName, szPath, MAX_PATH);
        pfti.szNetworkProvider[0] = 0;
        pfti.dwAttributes = FILE_ATTRIBUTE_DIRECTORY;
        pfti.csidl = -1;
        pfti.pidlTargetFolder = lpidlFolder;
        // 2番目の引数に、仮想フォルダ自身に
        // 当たる ITEMIDLIST を指定する
        hr = lpPFolder3->InitializeEx(NULL,
            lpidlRootFolder, &pfti);
        lpPFolder3->Release();
    }
    else // バージョンなどの問題で IPersistFolder3 が未実装
    {
        // 通常の方法で初期化する (元のフォルダが開く問題は起こる)
        hr = pFolder->QueryInterface(IID_IPersistFolder,
            (void**) &lpPFolder);
        if (SUCCEEDED(hr))
        {
            hr = lpPFolder->Initialize(lpidlFolder);
            lpPFolder->Release();
        }
        else
        {
            lpUnknown->Release();
            return lpSFolder;
        }
    }
    // IShellFolder を返す
    hr = lpUnknown->QueryInterface(IID_IShellFolder,
        (void**) &lpSFolder);
    lpUnknown->Release();
    return (FAILED(hr) ? NULL : lpSFolder);
}

これにより、「元のフォルダのサブフォルダ」が開く問題は解消されます。あとは ICopyHook インターフェイスなどを実装して、より一般のフォルダに近づけたりしてみてください。

最終更新日: 2007/11/03