Programming Field

ITEMIDLISTについての適当なまとめ

最近WindowsのShellに関するプログラミングをを行っていますが、このプログラミングをする際、必ずと言っていいほど「ITEMIDLIST」(中身はSHITEMIDなど。ポインタはLPITEMIDLIST。PIDLと省略されることも)を使う機会が出てきます。このITEMIDLISTはなかなかの曲者で、SHGetPathFromIDList関数などしか使ったことのない人であれば、ITEMIDLISTには「ファイルのパスの情報が含まれている」ものとしか認識していないかもしれません。

本ページではそのITEMIDLISTについて少しまとめてみました。

まず英語のMSDNライブラリのページを見てみます。すると「Introduction to the Shell Namespace」に「Item ID Lists」の項があり、ITEMIDLISTは可変長のデータであることが書かれています。

さらに「ITEMIDLIST」のページでは、ITEMIDLISTは具体的に以下の3種類があるということが書かれています。

  1. IDLIST_ABSOLUTE (ptr: PIDLIST_ABSOLUTE、const ptr: PCIDLIST_ABSOLUTE)
    • 「絶対位置」を表すITEMIDLISTです。つまり、これ1つに親となるNamespaceからターゲットとなる子要素までのすべてのITEMIDが含まれているため、このデータを解析するだけでこのITEMIDLISTが指す要素が何なのかを容易に知ることができます。
  2. IDLIST_RELATIVE (ptr: PIDLIST_RELATIVE、const ptr: PCIDLIST_RELATIVE)
    • 「相対位置」を表すITEMIDLISTです。あるフォルダからどのくらいの位置(深さ)にあるかを示すITEMIDLISTであるため、ほとんどの場合このITEMIDLISTは対応する親フォルダのみ解析することができます。ちなみに、Windows Shellの概念において最上位フォルダは「デスクトップ」であるため、SHGetDesktopFolder関数によって取得したIShellFolderインターフェイスから得られる子要素のITEMIDLISTは、必然的にIDLIST_ABSOLUTEとなります。
  3. ITEMID_CHILD (ptr: PITEMID_CHILD、const ptr: PCITEMID_CHILD、unaligned array: PCUITEMID_CHILD_ARRAY)
    • 「子要素」を表すITEMIDLISTです。IDLIST_RELATIVEの一種で1つしか値が含まれていないリストとなっています。主にある親フォルダに所属する子要素を表す(IEnumIDList::Nextメソッドなどで取得できる)ITEMIDLISTであるため、IDLIST_RELATIVE同様その親フォルダに解析してもらう必要があります。なお、ITEMID_CHILDはIDLIST_RELATIVEとして扱うことができます。

ちなみに、IDLIST_RELATIVEやITEMID_CHILDはバイト境界を揃えられていない(unaligned)ことを意味する「PUIDLIST_RELATIVE」「PCUIDLIST_RELATIVE」「PCUITEMID_CHILD」なども存在し、64ビットプログラミングをする際はこの違いにも注意する必要があります。(なおPUIDLIST_ABSOLUTEはヘッダーファイル内に定義が存在しません。その理由はIDLIST_ABSOLUTEの性質上、そのポインタはメモリを割り当てた際の先頭番地になることがほとんどであるため、バイト境界を揃える必要が無いためです。)

さて、ITEMIDLISTには以上の3種類がありますが、これを注意して使わないと、ITEMIDLISTが指す要素に関する正しい情報が得られなくなります。

例えば、SHGetPathFromIDList関数を使ってフォルダ/ファイルのパスを取得しようとする以下のコードは、多くの場合失敗します。

IShellFolder* pSomeFolder;  // どこかの一般フォルダのデータ
IEnumIDList* pEnum;
LPITEMIDLIST lpidl;
HRESULT hr;

hr = pSomeFolder->EnumObjects(NULL,
    SHCONTF_FOLDERS | SHCONTF_NONFOLDERS,
    &pEnum);
if (SUCCEEDED(hr)) {
    // アイテムが1つ以上あると仮定
    hr = pEnum->Next(1, &lpidl, NULL);
    if (SUCCEEDED(hr)) {
        TCHAR szBuffer[MAX_PATH];
        // 以下の箇所で多くの場合FALSEを返すため
        // メッセージが表示されない
        if (SHGetPathFromIDList(lpidl, szBuffer))
            MessageBox(NULL, szBuffer, _T("Item Path"), MB_OK);
        CoTaskMemFree(lpidl);
    }
    pEnum->Release();
}

このコードが失敗する理由は、IEnumIDList::NextメソッドはPIDLIST_RELATIVE(PITEMID_CHILD)のITEMIDLISTを返すのに対し、SHGetPathFromIDList関数はPIDLIST_ABSOLUTE(PCIDLIST_ABSOLUTE)のITEMIDLISTを要求するためです。普通IEnumIDListで取得したITEMIDLISTは、そのフォルダに対応するIShellFolder::GetDisplayNameOfメソッドを利用してパス名などの情報を取得する必要があります。または、得たPIDLIST_RELATIVEと、フォルダ自身を指すPIDLIST_ABSOLUTEを結合して新しいPIDLIST_ABSOLUTEのITEMIDLISTを作成する、という手もあります。(結合方法はILCombine関数がありますが、自前で作成することも可能です。)

最近のSDKのヘッダーファイルにおける定義やMSDNライブラリのドキュメントには、要求されている・戻り値となっているITEMIDLISTが3種類のうちどれに当たるかが明記されています。そこでITEMIDLISTを使ったプログラミングをする際は、ただ単に「LPITEMIDLIST」と変数を使うのではなく、3種類を使い分けで使う方がミスも減ると思われます。(さらにshlobj.hをインクルードする前に「#define STRICT_TYPED_ITEMIDS」を入れておくと使い分けを強制することができ、種類の指定が誤っている箇所をエラーにすることが出来るためおススメします。)

最後に上記のコードを、(敢えて)SHGetPathFromIDList関数を利用したまま書き換えた例を示します。

IShellFolder* pSomeFolder;  // どこかの一般フォルダのデータ
IEnumIDList* pEnum;
PIDLIST_ABSOLUTE lpidlSomeFolder;  // どこかの一般フォルダを表す絶対位置のITEMIDLIST
PITEMID_CHILD lpidl;
PIDLIST_ABSOLUTE lpidlChild;
HRESULT hr;

hr = pSomeFolder->EnumObjects(NULL,
    SHCONTF_FOLDERS | SHCONTF_NONFOLDERS,
    &pEnum);
if (SUCCEEDED(hr)) {
    // アイテムが1つ以上あると仮定
    hr = pEnum->Next(1, &lpidl, NULL);
    if (SUCCEEDED(hr)) {
        TCHAR szBuffer[MAX_PATH];
        // ITEMIDLISTを結合して絶対位置を指すものを作成
        // ※ ILCombineはWindows 2000以降で使用可能。
        //   それ以前を対象にする場合は自作関数を用意する
        lpidlChild = ILCombine(lpidlSomeFolder, lpidl);
        // 絶対位置のITEMIDLISTを指定しているため
        // 正しくフォルダ/ファイルのパスを取得できる
        if (SHGetPathFromIDList(lpidlChild, szBuffer))
            MessageBox(NULL, szBuffer, _T("Item Path"), MB_OK);
        CoTaskMemFree(lpidlChild);
        CoTaskMemFree(lpidl);
    }
    pEnum->Release();
}