ファイルを列挙する

 フォルダやエクスプローラーのようないわゆる「ファイラー」を作製してしまおうというのが今度のシリーズです。この講座を終えると、結構しっかりとしたファイラーが作れてしまう……らしいです、はい。
 その第1回目に当たる今回は、ファイルの列挙という、言ってみれば最も基礎的な部分の実現方法を紹介しましょう。
 ちなみに今回はMSDNのUsing PIDLs and Display Namesというページを参考にしています。

APIとインターフェイス
 通常、ファイルを列挙する場合にはFindFirstFile()FindNextFile()というふたつのAPIを使用します。このふたつの関数には「使いやすい」「ワイルドカードを使用できる」「同時にファイルデータを取得できる」といったメリットがあります。
 が、このAPIではデスクトップやマイコンピュータといった特殊なフォルダのファイルを取得できません。これらのファイルの取得にはインターフェイスを使用しなければならないのです。
 と言っても、この方法は扱いが面倒だというデメリットがあるので、もしFindFirstFile()等で十分なのであれば、そちらの使用をお奨めします。

 さて、ファイル操作を目的としてインターフェイスを使用する場合、中心となるのがITEMIDLIST構造体と、IShellFolderインターフェイスです。
 ITEMIDLIST構造体はファイルやフォルダを示す構造体です。パスという文字列ではないので、デスクトップやマイコンピュータなども示すことができます。前回までに何度か出てきているので知っているかもしれませんね。
 IShellFolderインターフェイスはフォルダを意味するインターフェイスです。このインターフェイスを使用することで、ファイルを列挙したり、そのファイルのファイル名やアイコンを取得することができます。

 このふたつは、次のような形で使用します。

1:あるフォルダを示すアイテムIDを取得する
2:そのアイテムIDをIShellFolderにバインドする
3:IShellFolder::EnumObjects()を使ってファイルを列挙する

 列挙されたファイルはアイテムIDとして取得するので、再びIShellFolderにバインドすればどんどん深いフォルダへと進むことができるというわけです。

とりあえず下準備
 今回のプロジェクトは、単純なSDIアプリケーションとします。で、作業の中心となるクラスはビュークラスとします。

 さて、まずは必要なファイルのインクルードです。インクルードするファイルはshlobj.hです。このファイルをインクルードすれば、インターフェイス関係はすべてOKです。StdAfx.hの中でインクルードしてください。

 次にIMallocインターフェイスの取得です。フォルダを選択するダイアログ(前編)で説明しましたが、インターフェイスのほとんどは動的にメモリを確保し、返ってきたポインタを使用して操作するという形を取ります。その確保や解放を行うのがIMallocです。このインターフェイスは何度となく使用するので、メンバ変数として取っておきましょう。
 まずはメンバ変数の宣言です。型はLPMALLOC、変数名はm_pMallocとしましょう(特に書きませんが、アクセス指定はすべてprivateとします)。

 なんでまたLPMALLOCってポインタなの、と思われるかもしれません。インターフェイスを使用する場合、そのほとんどをポインタを介して操作します。実際のオブジェクトを操作することは滅多にないと考えていいでしょう。ウィンドウやGDIオブジェクトを操作するときにハンドルを介するのと同じようなものです。
 そして、このハンドルの場合と同じように、構築と消滅が必要です。
 構築、つまりIMallocへのポインタの取得にはSHGetMalloc()というAPIを使用します。今回のファイル操作を行うクラスのコンストラクタに次のコードを書き込んでください。


CT_Filer2View::CT_Filer2View()
{
	HRESULT	hRes;

	hRes = ::SHGetMalloc( &m_pMalloc );
	ASSERT( hRes == NOERROR );

	// この下は後で説明。
	hRes = ::SHGetDesktopFolder( &m_pDTFolder );
	ASSERT( hRes == NOERROR );
}
	

 たったこれだけでインターフェイスを取得できます。簡単でしょ? まー、ポインタのポインタを渡すとゆーかなり変な状況ではあるんですけどね……。
 インターフェイスの破棄は、そのインターフェイスのRelease()メソッド(クラスの「メンバ関数」とほぼ同じです)を使用すればOK。デストラクタで次のように破棄します。


CT_Filer2View::~CT_Filer2View()
{
	m_pDTFolder->Release();
	// 上のは後で説明。

	m_pMalloc->Release();
}
	

 正確に言えば、「破棄」というのは間違いなんですが、でもその辺の難しいところは今のところ必要ないと思います、多分……。
 さて、下準備はこれで終わり。同時に、インターフェイスの操作方法についてのおさらいもできたということで、さっそく本題に入っていくことにしましょう。

デスクトップフォルダを取得する
 さて、まず最初に取得するのはデスクトップフォルダを示すIShellFolderインターフェイスです。このIShellFolderは、言ってみればフォルダの親分みたいなものです。実際、すべてのファイルのルートに位置するわけですが、このインターフェイスを使用するとどこにあるファイルでも操作することができるのです。このインターフェイスがあれば、どんなに深い場所に位置するフォルダでも簡単にバインドできるというわけです。

 このインターフェイスも所々で何度も使用するので、メンバ変数として持っておきましょう。型はLPSHELLFOLDER、変数名はm_pDTFolderとします。
 IMallocと同じような形で、取得と解放を行います。取得にはSHGetDesktopFolder()というAPIを使用します。これはSHGetMalloc()と構文的にまったく同じなので説明する必要はないでしょう。解放もIMallocと同じです。このふたつは、すでにIMallocのコードと一緒に書いてあるので確認しておいてください。

アイテムIDを取得する
 フォルダにバインドするアイテムIDの取得には、いくつか方法があります。

 まず「デスクトップフォルダ」ですが、これは必要ありません。当然ですね。SHGetDesktopFolder()で、アイテムIDどころかIShellFolderが取得できてしまうのですから。

 通常のフォルダはどうでしょう。パスをフォルダに変換する場合には、実はIShellFolderParseDisplayName()というメソッドを使用します。だったらなんで直接バインドできないんだとか思いますが、それは置いといて、次の関数でアイテムIDへの変換を行えます。


LPITEMIDLIST CT_Filer2View::GetItemIDList( CString p_cFileStr )
{
	if( p_cFileStr.IsEmpty() )
		return NULL;

	HRESULT		hRes;
	ULONG		chEaten;	//文字列のサイズを受け取ります。
	ULONG		dwAttributes;	//属性を受け取ります。
	OLECHAR		ochPath[MAX_PATH];	//ワイドバイト文字列です。
	LPITEMIDLIST	pIDL;	//フォルダを示すアイテムIDです。

	// これをしないとインターフェイスはダメなのです。
	::MultiByteToWideChar( CP_ACP, MB_PRECOMPOSED, p_cFileStr, -1, ochPath, MAX_PATH );

	// 実際にITEMIDLISTを取得します。
	hRes = m_pDTFolder->ParseDisplayName( NULL, NULL, ochPath, &chEaten, &pIDL, &dwAttributes);
    
	if( hRes != NOERROR )
		pIDL = NULL;

	return pIDL;	//取得したアイテムIDを返します。
}
	

 と、実はこのコードはファイルパスからアイテムIDを取得するで紹介したものとほとんど同じです。違いは、デスクトップフォルダが最初に取得したメンバ変数の物を使用しているというくらいです。が、もう一度見直してみましょう。

 まず、インターフェイスではUnicodeを使用します。そのため、MultiByteToWideChar()というAPIを使用して文字列を変換します。
 変換したあと、IShellFolder::ParseDisplayName()メソッドを使用してアイテムIDを取得します。これで、パスからアイテムIDに変換できました。
 このように、IShellFolderが持つメソッドにはファイルの情報を取得するメソッドがいくつかあります。これらについてはまた後ほど紹介しましょう。

 アイテムIDの取得方法はもうひとつあります。それはSHGetSpecialFolderLocation()というAPIを使用する方法です。この関数を使用すると、ゴミ箱やコントロールパネルといった特殊なフォルダを示すアイテムIDを直接取得することができます。
 この関数は「フォルダを選択するダイアログ」にも利用できます。例えば「スタートメニュー」のフォルダを取得して、それをルートに指定すれば、インストーラに見られるようなダイアログが簡単に作製できるでしょう。

 さて、ここからの処理をまとめた関数を紹介します。ただ、まだ作ってない関数もあるので、このままではコンパイルできませんから。


////////////////////////////////////////////////////////////////////
// ファイルのリストを作製します。

BOOL CT_Filer2View::MakeList( LPITEMIDLIST p_pFolderIDList)
{
	HRESULT		hRes;
	ULONG		ulRetNo;
	STRRET		stFileName;
	LPITEMIDLIST	pFileIDList;
	LPSHELLFOLDER	pCurFolder;
	LPENUMIDLIST	pEnumIDList;
	CString		cPrintStr;

	if( p_pFolderIDList != NULL )
	{
		// IShellFolderにバインドします。
		hRes = m_pDTFolder->BindToObject( p_pFolderIDList, NULL, IID_IShellFolder, (LPVOID *)&pCurFolder );
		if( hRes != NOERROR )
			return TRUE;
	}
	else
	{
		// デスクトップフォルダを指定します。
		hRes = ::SHGetDesktopFolder( &pCurFolder );
		if( hRes != NOERROR )
			return TRUE;
	}

	// IEnumIDListを取得します。
	hRes = pCurFolder->EnumObjects( GetSafeHwnd()
		, SHCONTF_NONFOLDERS |SHCONTF_INCLUDEHIDDEN | SHCONTF_FOLDERS, &pEnumIDList );
	if( hRes != NOERROR )
		return FALSE;

	// IEnumIDListからアイテムIDを取得していきます。
	while( pEnumIDList->Next( 1, &pFileIDList, &ulRetNo ) == NOERROR )
	{
		// ファイルパスの取得。
		hRes = pCurFolder->GetDisplayNameOf( pFileIDList
				, SHGDN_FORPARSING, &stFileName );
		if( hRes != NOERROR )
			break;

		// 文字列の変換。
		cPrintStr = TFileName( pFileIDList, &stFileName );

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

		m_pMalloc->Free( pFileIDList );
	}

	pCurFolder->Release();

	return TRUE;
}
	

 この関数は、フォルダを示すアイテムIDを引数に渡すと、そのフォルダ内のファイルがデバッグ用アウトプットにバンバン出力されるというものです。また、NULLを渡すとデスクトップのファイルを出力します。
 では、その内容を見ていきましょう。

フォルダのバインド
 フォルダのアイテムIDを取得したら、それをIShellFolderに結びつけます。結びつけたそのIShellFolderインターフェイスから、ファイルリストを取得することができます。
 結びつけるにはIShellFolder::BindToObject()メソッドを使用します。注意して欲しいのはBindToObject()メソッドを呼び出したインターフェイスにバインドされるわけではないということです。呼び出したインターフェイス(例ではデスクトップフォルダ)はあくまでバインドの手助けをするだけです。
 バインドされたIShellFolderは第4引数に返ってきます。このインターフェイスをこれから使うわけです。ちなみにこのインターフェイスも動的に作製された形なので、あとでRelease()メソッドを使って解放する必要があります。

 ちなみに、例を見ると奇妙な形だということに気付くと思います。第3引数に返すインターフェイスのタイプ、第4引数に受け取るためのポインタを渡します。こういう形はインターフェイスでは結構出てくるので見慣れておきましょう。こんな風な形で、いろんなインターフェイスを取得することができるというわけです。
 ちなみに、このメソッドの場合にはIShellFolderしか取得できません(汗)。

IEnumIDListの取得
 バインドが成功したら、バインドしたIShellFolderからIEnumIDListインターフェイスを取得します。このインターフェイスから、ファイルのアイテムIDを取得します。
 「また新しいインターフェイスが出てきたぁ!!」と思うかもしれませんが、IEnumIDListは別に難しいこともないので気にすることはないでしょう。

 IEnumIDListの取得にはIShellFolder::EnumObjects()というメソッドを使用します。第2引数には取得するファイルのタイプを指定できますが、フォルダ、それ以外、隠しファイルのみっつしか選べないのであんまり気にしなくていいです(例ではすべて設定しています)。IEnumIDListは第3引数に返ってきます。

アイテムIDリストの取得
 ファイルを示すアイテムIDはIEnumIDList::Next()メソッドを使用して取得します。IEnumIDListCListクラスに似たインターフェイスで、アイテムIDを次々と取得していくことができます。
 第1引数には一度に取得するアイテムIDの数を入れます。さらに第3引数には受け取った数が入ります。今回はひとつずつ取得するので1とNULLを入れています。で、第2引数でアイテムIDを受け取ります。全部アイテムIDを取得するとS_FALSEが返ってくるんで、それまでループで取得していけばOKです。
 これまで通り、ここで取得するアイテムIDも後で解放しなければなりません。アイテムIDの解放にはIMalloc::Free()メソッドを使用します。

 ちなみに、IEnumIDList::Next()メソッドのヘルプを見てみると、引数が4つあることに気付くと思います。これは注意が必要です。
 引数が3つでいいのはC++だからです。C言語の場合には4つ必要で、第1引数に呼び出しているIEnumIDListインターフェイスを渡します。これはおそらく、C++ではインターフェイスをクラスで実現しているため呼び出し元のデータ(つまりプライベートなメンバ変数)を直接使用できるが、C言語では単に関数を呼び出しているだけなのでムリ、ということではないでしょうか(よく知らんですはい)。
 別にC++だけで作るんだからいいんですが、サンプルがC言語だったりすることがあるんでそういうときには注意しましょう。こういうのはメソッドによって違います。

ファイルパスの取得
 さて、アイテムIDは取得できました。このままでも色々と使えるんですが、やっぱりちゃんと文字として見れないとなんか落ち着きませんよね。というわけで、ファイルのパスを取得しましょう。
 パスの取得にはIShellFolder::GetDisplayNameOf()を使用します。IShellFolderのメソッドを使用することに注意してください。実際、アイテムIDに対して行いたいことがあっても、それがどのインターフェイスのどのメソッド(もしくはAPI)を使用すればいいのか分からないことが多々あります。今回の企画を読めばそれが分かるわけですねー、便利な世の中ですねー(汗)。
 第2引数には取得するファイル名のタイプを指定します。SHGDN_NORMALでファイル名だけ、SHGDN_FORPARSINGだとフルパスを取得することができます。が、実際にはそう単純じゃないみたいです。詳しくはSHGNOのリファレンスを参照してください。

 さて、このメソッドを使用して取得できたのはSTRRET構造体です。文字列ではないので、変換する必要があります。変換する関数は次のようなものです。


CString CT_Filer2View::TFileName(LPITEMIDLIST p_pIDlist, LPSTRRET p_pStrret)
{
	int		iLength;
	LPSTR	pchStr;
	CString	cRetStr;

	switch( p_pStrret->uType )
	{
	case STRRET_WSTR:
		iLength = ::WideCharToMultiByte(CP_OEMCP, WC_DEFAULTCHAR,
				p_pStrret->pOleStr, -1, NULL, 0, NULL, NULL);
		pchStr = (LPSTR)m_pMalloc->Alloc( iLength );

		if( pchStr != NULL )
		{
			::WideCharToMultiByte(CP_OEMCP, WC_DEFAULTCHAR,
					p_pStrret->pOleStr, -1, pchStr, iLength, NULL, NULL);
			cRetStr = (LPCTSTR)pchStr;
			m_pMalloc->Free( pchStr );
		}
		break;

	case STRRET_OFFSET:
		cRetStr = (LPCTSTR)( ( (char *)p_pIDlist ) + p_pStrret->uOffset );
		break;

	case STRRET_CSTR:
		cRetStr = (LPCTSTR)p_pStrret->cStr;
		break;
	}

	return cRetStr;
}
	

 なんでこれで変換できるのか、筆者には理解できないのでこれはまぁそのまま使ってください(汗)。

 で、ここまで来ればビルドしちゃってOKです。任意のフォルダを渡すようにすれば、そのフォルダの中にあるファイルを列挙できるはずです。

これだけじゃまだダメ!
 さて、実際に試してみるとそれなりに感動すると思いますが、実はこれだけではまだ不十分です!!

 例えば、パスの取得にIShellFolder::GetDisplayNameOf()ではなく、フォルダを選択するダイアログ(前編)の中で紹介したSHGetPathFromIDList()というAPIを使用してファイルパスを取得してみると、不正なパスが返ってくることでしょう(おそらく「システムフォルダ+ファイル名」でしょう。例えばC:\Windows\system\index.htmlとか)。
 また、ここからサブフォルダに移動しようとか考えてIShellFolder::BindToObject()でバインドしようとしても、見事に失敗します

 そこで、次回は「ITEMIDLISTとはなんぞね!?」と題してアイテムIDのその人となりに迫ってみましょう。
 さらにこれからの予定として「ファイルの実行」「コンテキストメニューの表示」「ゴミ箱への削除」「アイコンの取得」「ショートカットの操作」といったものを予定しています。期待しててねっ。

(C)KAB-studio 1998 ALL RIGHTS RESERVED.