アイテムIDリストとは?

 これまではかなり曖昧に扱ってきた「アイテムID」ですが、今回はその正体を見極めてみましょう。
 ちなみに参考文献はほとんどありませんが、一応MSDNのItem Identifiers and Pointers to Item Identifier Listsというページに書いてあります。また、いくつかのDelphi用コンポーネントのソースコードを参考にさせてもらいました(汗)。

アイテムIDとは
Fig.1  「ファイル操作を目的としてインターフェイスを使用する場合、中心となるのがITEMIDLIST構造体」だと何度か説明してきました。ではいったい、どのようなものなのでしょうか。
 まず、ITEMIDLISTSHITEMID構造体ひとつだけをメンバとして持っています。ということは、ITEMIDLIST=SHITEMIDと考えてしまっていいでしょう。

 ではSHITEMID構造体はどうかというと、こちらはふたつのメンバを持っています。
 USHORT cbSHITEMID(つまりITEMIDLIST)のサイズを格納します。
 そしてBYTE abID[1]こそが「ファイルを表すデータ」です。が、このデータを直接操作することはないので、「ここにデータがあるんだ」ということだけ憶えておけばいいでしょう。

 そして最も重要なことは、この「ファイルデータ」の部分は可変長だということです。妙な配列の形になっていますが、その配列のサイズがどの程度かはファイルごとにまったく違います。
 そのサイズを格納するのがcbです。このもうひとつのメンバに構造体全体のサイズが格納されていて、どれだけの大きさなのかを知ることができるというわけです。これはヒジョーに重要なことなので、しっかり憶えておいてください。

 さて、このアイテムID、実は「単一ファイル(フォルダ)名」のみを示します。例えばC:\Windows\System\MSVCRT.DLLというファイルを示すアイテムIDがあった場合、このアイテムIDはMSVCRT.DLLのみを示します。
 これは少しイメージを掴みにくいかもしれません。文字列の場合には、ファイルだけも、フルパスも、途中のフォルダふたつなんていうのも、どんな形でも示すことができますが、アイテムIDの場合には必ずひとつのファイルかフォルダだけを示します(一部を除く)。
 では、フルパスをアイテムIDで示すにはどうすればいいのでしょうか。ここで登場するのがアイテムIDリストです!!

アイテムIDリスト
Fig.2  そのまんまですが、アイテムIDリストとはアイテムIDが連なっているものです。アイテムIDが連なり、最後に16ビットサイズの領域にゼロが入っていることでリストの最後だということが分かります。文字列での終端(ターミネーター)の\0を、16ビットサイズの0に置き換えてみれば分かりやすいと思います。

 例えば前述のC:\Windows\System\MSVCRT.DLLを、ファイルパスからアイテムIDを取得するで紹介したGetItemIDList()という関数を使用してアイテムIDを取得したとします。
 このアイテムID、実はアイテムIDリストです。アイテムIDもアイテムIDリストもLPITEMIDLISTというポインタの形で操作するため分かりませんが、実際にはアイテムIDリスト、つまりアイテムIDがいっぱい連なった物なのです。
 ってゆーか、実はアイテムIDそのものを取得することはありません。常にアイテムIDリストとして取得します。ごっちゃになっちゃった方、ごめんなさい。

 さて、このアイテムIDリストはどのような中身になっているのでしょうか。その関数を紹介する前に、補助的なふたつの関数を説明しましょう。まずは領域を確保する関数です。

メモリを確保する関数


////////////////////////////////////////////////////////////////////
// アイテムIDリストを格納するためのメモリを確保します。

LPITEMIDLIST CT_Filer2View::CreateItemID(UINT p_uiSize)
{
	LPITEMIDLIST	pRetIDList;

	// メモリをアロケートします。
	pRetIDList = (LPITEMIDLIST)m_pMalloc->Alloc( p_uiSize );

	if(pRetIDList != NULL )
		memset( pRetIDList , 0, p_uiSize );	//初期化します。

	return pRetIDList;
}
	

 アイテムIDリストはアイテムIDが連なっています。アイテムIDひとつひとつを処理するためには、それぞれを抜き出し、新しいアイテムIDリストとして作製する必要があります。そのために動的にメモリを確保するのが、この関数です。引数として渡されるサイズ分だけ、メモリを確保します。
 メモリの確保にはIMalloc::Alloc()というメソッドを使用します。この中で使っているm_pMallocは、前回に紹介したものなのでそちらをご覧ください。このメソッドを使用してメモリを確保することで、IMalloc::Free()メソッドでの解放が可能になるというわけです。

次のアイテムIDを取得する関数


////////////////////////////////////////////////////////////////////
// 次のアイテムIDを示す位置へとポインタを進めます。

LPITEMIDLIST CT_Filer2View::GetNextItemID(LPITEMIDLIST p_pItemIDList)
{ 
	// アイテムIDのサイズを取得します。
	int iSize = p_pItemIDList->mkid.cb;

	// ゼロなら最後です。
	if( iSize == 0 )
		return NULL;

	// ポインタをサイズ分増やします。
	p_pItemIDList = (LPITEMIDLIST)( ( (LPBYTE)p_pItemIDList ) + iSize );

	// 最後ならNULLを、そうでないならアイテムIDへのポインタを返します。
	if( p_pItemIDList->mkid.cb == 0 )
		return NULL;
	else
		return p_pItemIDList;
} 
	

Fig.3  アイテムIDリストはアイテムIDが連なってできているものです。が、取得したLPITEMIDLISTは、このリストの先頭アドレスを示しているのに過ぎません。これでは、最初のひとつだけのアイテムIDしか取得できないことになります。そこで、のアイテムIDを取得することが必要になってくるわけです。
 と言っても、実際には単にポインタをサイズ分増やすだけです。型キャストを行うのは、正確にサイズ分だけ増やすためです。ポインタの演算の場合、通常の整数演算とは違って型によって増え方や減り方が変わります。cbメンバに入っているのは構造体のサイズ(=バイト数)なので、バイト単位で増えてもらわないと困るわけです。キャストを行わなかった場合には、おそらくITEMIDLIST構造体の単位で増えるので、不正に大きな数が返ってくることでしょう。

 増やす前と増やした後に、cbメンバが0でないかチェックします。このメンバにはITEMIDLISTのサイズが入るはずなので、当然このメンバ自身のサイズも含まれるわけで、ゼロが入らないように見えます。が、実際にはゼロが入り、ゼロが入っていたときに、リストの終端まで来ているということが分かるようになっています。
 アイテムIDリストは、終端に16ビットの領域を確保し、そこを0で埋めてあることになっています。そして、cbメンバの型であるUSHORTも16ビットなのです。
 つまり、ポインタを進めて終端にたどり着いたとき、終端の16ビット領域がちょうどcbメンバに当てはまるというわけです。で、そこに0が入っていれば終端だということになるわけです。
 この仕組みは後ほど再び出てくるので憶えておいてください。

アイテムIDのコピー
 上の関数でアイテムIDひとつひとつを示すことができたら、そのアイテムIDをコピーして、そのアイテムIDひとつだけが入ったアイテムIDリストを作製します。


////////////////////////////////////////////////////////////////////
// アイテムIDリストからアイテムIDをコピーします。

LPITEMIDLIST CT_Filer2View::CopyItemID(LPITEMIDLIST p_pItemIDList) 
{ 
	// アイテムIDのサイズを取得します。
	int iSize = p_pItemIDList->mkid.cb;

	// 新しく領域を確保します。アイテムIDはゼロの入った16ビットサイズの
	//領域で閉じなければならないので、その分だけ多めに確保します。
	LPITEMIDLIST pNewIDList = CreateItemID( iSize + sizeof(USHORT) );
	if( pNewIDList == NULL )
		return NULL;

	// アイテムIDをコピーします。
	memcpy( pNewIDList, p_pItemIDList, iSize );

	// ゼロで閉じておきます。
	memset( (LPBYTE)pNewIDList + iSize, 0, sizeof(USHORT) );

	return pNewIDList;
}
	

 CreateItemID()は先ほど作製した関数です。USHORTの分だけサイズを増やすのは、終端を示す16ビットサイズの領域を作らなければならないからです。必要とするのはひとつのアイテムIDですが、それを操作するためにはアイテムIDリストの形にする必要があるのです。

 memcpy()はランタイム関数で、メモリの中身をそのままコピーしてくれます。またmemset()は同じくランタイム関数で、特定の値でメモリを埋めてくれます。今回は、最後の16ビットに0を埋めています。型キャストは先ほどの説明と同じ意味です。

リストを渡ってみる
 では、アイテムIDリストの中身を見る関数を紹介しましょう。


////////////////////////////////////////////////////////////////////
// アイテムIDリストの中を見ていきます。

BOOL CT_Filer2View::ShowList(LPITEMIDLIST p_pItemIDList)
{
	HRESULT		hRes;
	STRRET		stFileName;
	LPITEMIDLIST	pCurIDList;
	LPSHELLFOLDER	pCurFolder, pTempFolder;
	CString		cPrintStr;

	SHGetDesktopFolder( &pCurFolder );
	pCurIDList = p_pItemIDList;

	do
	{
		LPITEMIDLIST pCopyIDList;

		// アイテムIDをコピーします。
		pCopyIDList = CopyItemID( pCurIDList );
		if( pCopyIDList == NULL )
		{
			TRACE0( "Copy FALSE\n" );
			break;
		}

		// フォルダの取得。
		hRes = pCurFolder->GetDisplayNameOf( pCopyIDList
				, SHGDN_NORMAL, &stFileName );
		if( hRes == NOERROR )
			cPrintStr = TFileName( pCopyIDList, &stFileName );
		else
			cPrintStr = "";

		// フォルダを表示します。
		TRACE( "%s\\", (LPCTSTR)cPrintStr );

		// そのフォルダへとバインドします。
		hRes = pCurFolder->BindToObject( pCopyIDList, NULL, IID_IShellFolder
			, (LPVOID *)&pTempFolder );
		if( hRes != NOERROR )
		{
			TRACE0( "\nBind DTFolder FALSE\n" );
			pTempFolder = NULL;
		}
		// 現在のフォルダを入れ換えます。
		pCurFolder->Release();
		pCurFolder = pTempFolder;

		// コピーしたアイテムIDを解放します。
		m_pMalloc->Free( pCopyIDList );

		// 次のアイテムIDへとポインタを進めます。
		pCurIDList = GetNextItemID( pCurIDList );
	}while( pCurIDList != NULL );	//終端まで来たら抜け出ます。

	if( pCurFolder != NULL )
		pCurFolder->Release();

	return TRUE;
}
	

Fig.4  このコードを実際に試してみてください。おそらくマイ コンピュータ\Windows 95 (C:)\Windows\System\Msvcrt.dllみたいな感じになると思います。そうです、実はフルパスを示すアイテムIDリストは、パスのアイテムIDが連なってできているのです! これが、アイテムIDがファイルやフォルダひとつひとつのみを示す理由です。

 関数の中身を見ていきましょう。関数に渡されたアイテムIDリストにはフルパスが入っているとします。
 まずそのアイテムIDリストから、先ほど作製したCopyItemID()を使用して最初のアイテムIDをコピーしてきます。
 次にアイテムIDのファイル名を出力します。それにはIShellFolder::GetDisplayNameOf()を使用します。
 出力した後、このフォルダを新しいIShellFolderにバインドします。
 後始末をした後、アイテムIDリストのポインタを次のアイテムIDまで進めます。NULLが返ってこなかったら、初めに戻ります。

 さて、実は2巡目以降に注目点があります。
 このループの中で、IShellFolderが使われています。最初のループではデスクトップフォルダを使用していましたが、それ以降のループでは先ほどバインドしたフォルダを使用しています。
 ループをひとつ通過するごとに、IShellFolderのフォルダは1階層ずつ深くなっていきます。フォルダを示すアイテムIDを取得したら、それをIShellFolderに加えていくような感じでしょうか。
 もしこういったことをせず、すべてデスクトップフォルダで行った場合にはうまくいきませんIShellFolder::GetDisplayNameOf()はそこそこ機能しますが、ドライブ名は取得できないようです。また、IShellFolder::BindToObject()に至っては全く機能しません。そのため、このようなアイテムIDを取得次第IShellFolderに追加していくという方法が必要になってくるわけです。

 さて、これでアイテムIDリストがどのような存在かということが分かったと思います。では、最後の詰めです。

サブフォルダのバインド
 前回「列挙したサブフォルダはそのままではバインドできない」と言いました。最後に、この解決方法を説明することにしましょう。

 カンのいい方は分かっちゃってるかもしれませんが、IEnumIDList::Next()で取得するアイテムIDリストは、たったひとつのアイテムID、つまりそのファイルかフォルダだけしか持っていません。
 これは先ほどのIShellFolderにフォルダをくっつけていくというのとは逆の発想です。特定のフォルダを示すIShellFolderがすでにあるということは、すなわちそのフォルダを示すアイテムIDリストがすでにあるということでもあるのです。そのアイテムIDリストがあるのに、わざわざそれをくっつけて出力するのはメモリと時間の無駄だとIEnumIDList::Next()を設計した人は考えたのでしょう。
 と言っても、IShellFolder::BindToObject()を使用するにはサブフォルダのフルパスが必要になります。ではどうすればいいのでしょうか?

 答は簡単。ふたつのアイテムIDリストをくっつけてしまえばいいんです

 実際には次のふたつの関数で実現します。

アイテムIDリストのサイズの取得
 この関数は、アイテムIDリストのサイズを取得する関数です。


////////////////////////////////////////////////////////////////////
// アイテムIDリストのメモリサイズを取得します。

UINT CT_Filer2View::GetItemIDSize(LPITEMIDLIST p_pIDList)
{
	UINT	uiRet = 0;

	if( p_pIDList == NULL )
		return 0;

	// アイテムIDリストは16ビットサイズのゼロで止められているので、
	//その分を加えておきます。
	uiRet = sizeof(USHORT);

	// アイテムIDを渡っていって、cbメンバを加えていきます。
	do
	{
		uiRet = uiRet + (int)p_pIDList->mkid.cb;
		p_pIDList = GetNextItemID( p_pIDList );
	}while( p_pIDList != NULL );	//終端まで来たら抜けます。

	return uiRet;
}
	

 まず、アイテムIDリストには必ず16ビットのターミネーターが存在するのでそれを加えておいちゃいます。
 で、そのあとで、先ほどアイテムIDひとつひとつの名前を取得したときとと同じ方法で、アイテムIDのcbメンバから、それぞれのサイズを取得してどんどん足していきます。終端まで行き着いたときにはすべてのサイズが取得できているという寸法です。

アイテムIDリストの取得
 では最後の例、ふたつのアイテムIDリストをくっつける関数です。


////////////////////////////////////////////////////////////////////
// ふたつのアイテムIDリストをくっつけます。

LPITEMIDLIST CT_Filer2View::ConcatItemID( LPITEMIDLIST p_pIDList1, LPITEMIDLIST p_pIDList2 )
{
	if( p_pIDList1 == NULL || p_pIDList2 == NULL )
		return NULL;

	UINT 	iSize1 iSize2;
	LPITEMIDLIST	pRetIDList, pCurIDList;

	// 各アイテムIDのサイズを取得します。
	iSize1 = GetItemIDSize( p_pIDList1 ) - sizeof(USHORT);
	iSize2 = GetItemIDSize( p_pIDList2 );

	// 空のアイテムIDを作製します。
	pRetIDList = CreateItemID( iSize1 + iSize2  );
	if( pRetIDList == NULL )
	  return NULL;

	// アイテムIDリスト1をコピーします。
	memcpy( pRetIDList, p_pIDList1, iSize1 );

	// 次にコピーする位置を進めます。
	pCurIDList = (LPITEMIDLIST)( (LPBYTE)pRetIDList + iSize1 );

	// アイテムIDリスト2をコピーします。
	memcpy( pCurIDList, p_pIDList2, iSize2 );

	// アイテムIDリストの先頭ポインタを返してできあがり。
	return pRetIDList;
}
	

 まず先ほどの関数を使用して、アイテムIDリストのサイズを取得します。ここで、リスト1のサイズを16ビット分少なくしています。こうすることで、最後のターミネーターをコピーしないようにしています。これをしておかないと、ふたつのアイテムIDリストの間にターミネーターが入り込んでしまいます。
 くっつけたアイテムIDリストの格納場所をCreateItemID()で確保したあと、memcpy()を使ってまずアイテムIDリスト1をコピーします。コピーしたらアイテムIDリスト1の最後までポインタを進めて、そこからアイテムIDリスト2をコピーします。このリスト2には初めから16ビット領域のターミネーターが付いているので、そのままコピーしてしまえばこの新しいアイテムIDリストにもターミネーターが付くというわけです。

 これでアイテムIDリストのくっつけ方が分かったと思います。前回の例と組み合わせる場合には、IShellFolder::BindToObject()を使うとファイルとフォルダの区別とかネストによる時間の問題とかあるので、簡単にSHGetPathFromIDList()で試してみてください。


	// くっつける前。
	SHGetPathFromIDList( pFileIDList, cPrintStr.GetBuffer( MAX_PATH ) );
	cPrintStr.ReleaseBuffer();

	TRACE( "Before: %s\n", (LPCTSTR)cPrintStr );

	// くっつけた後。
	pCurIDList = ConcatItemID( p_pFolderIDList, pFileIDList );
	SHGetPathFromIDList( pCurIDList, cPrintStr.GetBuffer( MAX_PATH ) );
	cPrintStr.ReleaseBuffer();

	TRACE( "After: %s\n", (LPCTSTR)cPrintStr );

	m_pMalloc->Free( pCurIDList );
	

 上のコードを、ファイル表示の直後に入れてみれば、どのように表示されるのか比べてみることができるでしょう。

今回のまとめ
 今回の例で難しい部分のひとつは「今アイテムIDリストがどのような状態か」ということを把握することでしょう。使用する関数によって時にはフルパスであったり時にはフォルダひとつであったりするわけで、その辺に抜け道ができないようコーディングすることが結構難しいと思います。
 それともうひとつあげれば、メモリ管理の難しさでしょうか。とはいえ、メモリ管理はC言語のおはことでも言えるものです。
 最初に触れましたが、このコードの大半はDelphiのものを流用しています。ほぼ同じことができるとはいえ、C言語とは扱いやすさは大きく違うでしょう。そのコードを書いてくれた方に感謝します。

 ってゆーか、なんでDelphIのがあって、VCのがないのかなぁ……。

(C)KAB-studio 1998 ALL RIGHTS RESERVED.