フォルダを選択するダイアログ(後編)

 さて、::SHBrowseForFolder()の後編です。この関数は近年まれに見る便利なAPIなので、骨の髄までしゃぶってみましょう。あ、もちろん前編を読んでおいてください。そして、前回の「ファイルのパスからアイテムIDを取得する」関数を今回使用するので、そちらも読んでおいてくださいね。

とりあえず関数だけ
 とりあえず関数だけ紹介します。前回、前々回と同じクラスに作製していると考えてください。


CString CTestView::BrowseForFolder2( HWND p_hWnd, CString p_cSetStr
	, CString p_cRootStr, CString p_cCaptionStr, UINT p_uiFlags )
{
	LPMALLOC pMalloc;

	// IMallocインターフェイスへのポインタを取得します。
	if( ::SHGetMalloc( &pMalloc ) != NOERROR )
		return _T( "" );

	LPITEMIDLIST pIDL, pSetIDL, pRootIDL;
	BROWSEINFO stBInfo;
	char	chReturn[MAX_PATH];
	CString	cRetStr;

	pRootIDL = GetItemIDList( p_cRootStr );
	pSetIDL = GetItemIDList( p_cSetStr );

	// 構造体を初期化します。
	stBInfo.pidlRoot = pRootIDL;//ルートフォルダです。
	stBInfo.hwndOwner = p_hWnd;	//このダイアログの親ウィンドウのハンドルです。
	stBInfo.pszDisplayName = chReturn;	//選択されているフォルダを返すためのポインタです。
	stBInfo.lpszTitle = p_cCaptionStr;	//説明の文字列です。
	stBInfo.ulFlags = p_uiFlags;	//フォルダだけにします。
	stBInfo.lpfn = (BFFCALLBACK)BrowseCallbackProc;	//プロシージャへのポインタです。
	stBInfo.lParam = (LPARAM)pSetIDL;	//選択するフォルダへのIDです。

	// ダイアログボックスを表示します。
	pIDL = ::SHBrowseForFolder( &stBInfo );
	// pidlにはそのフォルダのネームスペースに関連づけられたポインタが
	// 入っています。この時点ではchReturnには選択されたフォルダ名だけ
	// しか入っていないので、このポインタを利用します。

	if( pIDL != NULL )
	{
		// フルパスを取得します。
		if( ::SHGetPathFromIDList( pIDL, chReturn ) )
			cRetStr = chReturn;

		pMalloc->Free( pIDL );
	}

	// リリースします。
	if( pRootIDL != NULL )
		pMalloc->Free( pRootIDL );

	if( pSetIDL != NULL )
		pMalloc->Free( pSetIDL );

	pMalloc->Release();

	// 文字列を返します。
	return cRetStr;
}
	

 関数の雛形は前々回のものと同じですが、途中で前回作製したGetItemIDList()という関数を使用している部分や、この関数から返ってくるアイテムIDをあとでリリースしている部分、そしてBROWSEINFO構造体へと入れているデータがいくつか違います。その辺を見ていきましょう。
 あ、この関数を作製しただけじゃ、ビルドできません。ビルドはもう少し待ってくださいね。

アイテムIDの取得
 前回作製した関数で、ファイルのパスからアイテムIDを取得しています。このアイテムIDをあとで使用します。使用したあとはIMalloc::Free()を使って削除しておきます。それにしても、ポインタの戻り値ってなんか変……。

ルートフォルダ
 BROWSEINFO構造体のメンバpidlRootには、ルートフォルダに指定するフォルダのアイテムIDへのポインタ(長いな)を入れます。
 ルートフォルダとは、ツリーコントロールの一番根本になるフォルダのことです。通常はデスクトップがルートですが、このメンバに指定したフォルダのアイテムIDを入れることで、好きなフォルダをルートにすることができます。
 例えばインストーラで、アプリケーションへのショートカットを作製する場所をこのダイアログで選んでもらう時にC:\Windows\スタート メニュー(もちろんホントは半角カタカナ)をルートに指定すれば、スタートボタンから現れるツリーの中のフォルダを必ず指定してくれるはずです。
 あとで紹介する方法を使えば特定のフォルダを選択できないようにすることができます。でも、この方法は簡単で、ユーザーにも解りやすい方法と言えるでしょう。
 ちなみにあまり例はないのですが、例えば「タスクバーのプロパティ」ダイアログの「スタートメニューの設定」ページで「削除」ボタンを押すと現れるダイアログや、インターネットエクスプローラーで「お気に入りの追加」を選んだ時に現れるダイアログなどが、ルートフォルダを変更している例です。

BrowseCallbackProc
 さて、BROWSEINFO構造体lpfnメンバには、なにやら怪しい値が入っています。BrowseCallbackProcとは何なのでしょう? これ、実は関数です。といっても、これから作るんですが。
 ここで指定している関数は、言ってみればダイアログプロシージャのようなものです。ダイアログから送られてくるメッセージを処理するためのプロシージャ関数を、ここで指定するわけです。必要がなければNULLで構わないのですが、これを使うのと使わないのとではえらい差があります。というわけで、ここでは使ってみましょう。

 では実際に関数を作製します。「どういう関数を作ればいいのか」ということは::BrowseCallbackProc()のリファレンスを見てください(つまりAPIとしてどういう関数にしなければならないのか、決められています)。具体的には、次のようなコードを、これまでと同じファイル(例ではTestView.cpp)に書き込んでください。


int AFXAPI BrowseCallbackProc( HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData )
{
	// 初期化時にフォルダを選択させます。
	if( uMsg == BFFM_INITIALIZED )
	{
		::SetWindowText( hwnd, _T( "テストのダイアログ" ) );
		::SendMessage( hwnd, BFFM_SETSELECTION, FALSE, lpData );
	}
	else
	{
		char chText[MAX_PATH];
		if( ::SHGetPathFromIDList( (LPITEMIDLIST)lParam, chText ) )
			::SendMessage( hwnd, BFFM_SETSTATUSTEXT, TRUE, (LPARAM)chText );
	}

	return 0;
}
	

 この関数は、Viewクラスのメンバとして作っているわけではないので、クラスウィザード等を使わずにそのまま書き込んでください。
 さらに、この関数のプロトタイプ宣言をヘッダーファイルに書き込んでください。

static int AFXAPI BrowseCallbackProc( HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData );

 ここまで書き込めばビルドできます。では、::BrowseCallbackProc()の中身について見ていきましょう。

::BrowseCallbackProc()
 まず、この関数の引数を見てみましょう。

 第1引数のhwndにはダイアログのウィンドウハンドルが入っています。ウィンドウハンドルが入っている……いい響きです。
 例えば、上の例では::SetWindowText()を使ってダイアログのタイトルを変更しています。もちろん、ダイアログの位置やサイズの変更とか、ボタンのタイトルの変更とか、その他色々好き勝手にできるというわけです。この機能を使わない手はないでしょう。

 第2引数について見てみましょう。この関数は、ダイアログでイベントが起きる度にウィンドウズから呼び出されます。と言ってもその「イベント」はたった2種類だけです。
 ひとつはダイアログが最初に表示される直前で、このとき、第2引数のuMsgの中にBFFM_INITIALIZEDが入っています。このとき、第3引数のlParamには0が入っています。
 もうひとつはフォルダの選択が変更されたときで、このときにはuMsgの中にBFFM_SELCHANGEDが入っています。このときには第3引数のlParamには、選択されたフォルダのアイテムIDへのポインタが入っています。
(注:筆者が持っているMSDNの::BrowseCallbackProc()の解説には、uMsgの欄でlParamがlpDataに書き代わっています。ここはミスプリントなので注意してください)

 第4引数のlpDataには、::SHBrowseForFolder()で渡したBROWSEINFO構造体lParamメンバに入っていたものがそのまま入っています。今回は「ダイアログが開いたときに選択しておくフォルダのアイテムIDへのポインタ」を入れてあります。

 さて、上の説明通り、この関数は「ダイアログが開くとき」と「フォルダが選択されたとき」のふたつの場合しかないので、このふたつについて処理を行えば十分ということです。上のコードはそうなっています。
 で、実際の操作ですが、hwndの説明に書いたように、このウィンドウハンドルを使用してダイアログを操作することができます。ですが、それとは別に、次の3つの便利なメッセージを送ることができます。

 BFFM_ENABLEOKメッセージを送ると、「OK」ボタンを押せるようにしたり押せないようにしたりできます。::SendMessage()wParam(つまり第3引数)に0以外を入れると押せるように、0を入れると押せないようにします。::EnableWindow()CWnd::EnableWindow()とフラグは同じですね。

 BFFM_SETSELECTIONメッセージを送ると、特定のフォルダを選択状態にできます。この機能を使うと、フォルダが選択された状態でダイアログが開かせることができます。上の例ではこれを行っています。
 このメッセージを送るとき、(::SendMessage()の)wParamの値によってlParamの意味が変わります。
 wParamTRUEを入れると、lParamには選択するフォルダが入った文字列へのポインタを入れなければなりません。
 wParamFALSEを入れると、lParamには選択するフォルダのアイテムIDへのポインタを入れなければなりません。
 つまり、普通の文字列も、アイテムIDも使うことができるということです(上の例ではアイテムIDを使用しています)。

 BFFM_SETSTATUSTEXTメッセージを送ると、ツリーコントロールの上のスタティックボックスに文字列を書き込むことができます。書き込む文字列へのポインタをlParamに入れる必要があります。
 ただし、このメッセージは、::SHBrowseForFolder()で渡したBROWSEINFO構造体ulFlagsメンバBIF_STATUSTEXTを入れておく必要があります。
 上の例では、選択したフォルダのフルパスを書き込みます。

まとめ
 今回は、インターフェイスというよりも、::SHBrowseForFolder()::BrowseCallbackProc()の解説に終始しました。ですが、それだけこの関数が「使える」ということです。ところが残念なことに、サンプルが非常に少なく、しかも::BrowseCallbackProc()を使っているものに至っては皆無であり、その上インターフェイスというハードルがあるという、取っつきにくさではトップクラスのAPIでもあるのです。
 とはいえ、筆者だって特別な情報を得たわけではありません。サンプルをコピーして、英文をよく読んで、デバッグを丹念にして、色々な状況を試してみる。それだけのことです。
 「実現したい機能があるけど、難しそう」と思うことは多々あるでしょう。でも、色々試してみればなんとかなるものです。当たって砕けろ、失敗の許されるプログラミングならではの言葉ではないでしょうか。

 もっとも、適当半分に理解したAPIをフリーウェアやシェアウェアに使うのはちょっちやばめですが……。

(C)KAB-studio 1997 ALL RIGHTS RESERVED.