フォルダやエクスプローラーのようないわゆる「ファイラー」を作製してしまおうというのが今度のシリーズです。この講座を終えると、結構しっかりとしたファイラーが作れてしまう……らしいです、はい。
その第1回目に当たる今回は、ファイルの列挙という、言ってみれば最も基礎的な部分の実現方法を紹介しましょう。 ちなみに今回はMSDNのUsing PIDLs and Display Namesというページを参考にしています。 |
APIとインターフェイス
通常、ファイルを列挙する場合にはFindFirstFile()とFindNextFile()というふたつのAPIを使用します。このふたつの関数には「使いやすい」「ワイルドカードを使用できる」「同時にファイルデータを取得できる」といったメリットがあります。 が、このAPIではデスクトップやマイコンピュータといった特殊なフォルダのファイルを取得できません。これらのファイルの取得にはインターフェイスを使用しなければならないのです。 と言っても、この方法は扱いが面倒だというデメリットがあるので、もしFindFirstFile()等で十分なのであれば、そちらの使用をお奨めします。
さて、ファイル操作を目的としてインターフェイスを使用する場合、中心となるのがITEMIDLIST構造体と、IShellFolderインターフェイスです。
このふたつは、次のような形で使用します。
1:あるフォルダを示すアイテムIDを取得する
列挙されたファイルはアイテムIDとして取得するので、再びIShellFolderにバインドすればどんどん深いフォルダへと進むことができるというわけです。 |
とりあえず下準備
今回のプロジェクトは、単純なSDIアプリケーションとします。で、作業の中心となるクラスはビュークラスとします。
さて、まずは必要なファイルのインクルードです。インクルードするファイルはshlobj.hです。このファイルをインクルードすれば、インターフェイス関係はすべてOKです。StdAfx.hの中でインクルードしてください。
次にIMallocインターフェイスの取得です。フォルダを選択するダイアログ(前編)で説明しましたが、インターフェイスのほとんどは動的にメモリを確保し、返ってきたポインタを使用して操作するという形を取ります。その確保や解放を行うのがIMallocです。このインターフェイスは何度となく使用するので、メンバ変数として取っておきましょう。
なんでまたLPMALLOCってポインタなの、と思われるかもしれません。インターフェイスを使用する場合、そのほとんどをポインタを介して操作します。実際のオブジェクトを操作することは滅多にないと考えていいでしょう。ウィンドウやGDIオブジェクトを操作するときにハンドルを介するのと同じようなものです。 |
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とします。 |
アイテムIDを取得する
フォルダにバインドするアイテムIDの取得には、いくつか方法があります。
まず「デスクトップフォルダ」ですが、これは必要ありません。当然ですね。SHGetDesktopFolder()で、アイテムIDどころかIShellFolderが取得できてしまうのですから。
通常のフォルダはどうでしょう。パスをフォルダに変換する場合には、実はIShellFolderのParseDisplayName()というメソッドを使用します。だったらなんで直接バインドできないんだとか思いますが、それは置いといて、次の関数でアイテム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を使用して文字列を変換します。
アイテム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引数に受け取るためのポインタを渡します。こういう形はインターフェイスでは結構出てくるので見慣れておきましょう。こんな風な形で、いろんなインターフェイスを取得することができるというわけです。 |
IEnumIDListの取得
バインドが成功したら、バインドしたIShellFolderからIEnumIDListインターフェイスを取得します。このインターフェイスから、ファイルのアイテムIDを取得します。 「また新しいインターフェイスが出てきたぁ!!」と思うかもしれませんが、IEnumIDListは別に難しいこともないので気にすることはないでしょう。
IEnumIDListの取得にはIShellFolder::EnumObjects()というメソッドを使用します。第2引数には取得するファイルのタイプを指定できますが、フォルダ、それ以外、隠しファイルのみっつしか選べないのであんまり気にしなくていいです(例ではすべて設定しています)。IEnumIDListは第3引数に返ってきます。 |
アイテムIDリストの取得
ファイルを示すアイテムIDはIEnumIDList::Next()メソッドを使用して取得します。IEnumIDListはCListクラスに似たインターフェイスで、アイテムIDを次々と取得していくことができます。 第1引数には一度に取得するアイテムIDの数を入れます。さらに第3引数には受け取った数が入ります。今回はひとつずつ取得するので1とNULLを入れています。で、第2引数でアイテムIDを受け取ります。全部アイテムIDを取得するとS_FALSEが返ってくるんで、それまでループで取得していけばOKです。 これまで通り、ここで取得するアイテムIDも後で解放しなければなりません。アイテムIDの解放にはIMalloc::Free()メソッドを使用します。
ちなみに、IEnumIDList::Next()メソッドのヘルプを見てみると、引数が4つあることに気付くと思います。これは注意が必要です。 |
ファイルパスの取得
さて、アイテム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とか)。
そこで、次回は「ITEMIDLISTとはなんぞね!?」と題してアイテムIDのその人となりに迫ってみましょう。 |
(C)KAB-studio 1998 ALL RIGHTS RESERVED. |