EternalWindows
シェル拡張 / 仮想フォルダサンプル(コマンドバー)
サンプルはこちら

Windows Vistaからのフォルダには、コマンドバーと呼ばれるバーが表示されています。 このようなバーは既定で仮想フォルダに表示されませんが、 しかるべき処理を行うようにすれば次のように表示することが可能になります。

シェル拡張がコマンドの表示をサポートするようになると、「整理」と「表示」というコマンドが既定で表示されます。 独自に実装したコマンドはこれらの横に表示され、ポップアップメニューのようにサブコマンドを表示することもできます。

シェルはコマンドバーを表示する段階になると、IExplorerCommandProviderを実装したオブジェクトを取得しようとします。 これは、IShellFolder::CreateViewObjectを通じて行われます。

STDMETHODIMP CShellFolder::CreateViewObject(HWND hwndOwner, REFIID riid, void **ppv)
{
	HRESULT hr = E_NOINTERFACE;
	
	if (IsEqualIID(riid, IID_IShellView)) {
		SFV_CREATE sfvCreate;

		sfvCreate.cbSize   = sizeof(SFV_CREATE);
		sfvCreate.pshf     = static_cast<IShellFolder *>(this);
		sfvCreate.psvOuter = NULL;
		sfvCreate.psfvcb   = NULL;
		
		hr = SHCreateShellFolderView(&sfvCreate, (IShellView **)ppv);
	}
	else if (IsEqualIID(riid, IID_IExplorerCommandProvider)) {
		CExplorerCommandProvider *p;

		p = new CExplorerCommandProvider();
		hr = p->QueryInterface(riid, ppv);
		p->Release();
	}
	else
		;

	return hr;
}

riidがIID_IExplorerCommandProviderである場合は、 CExplorerCommandProviderというクラスのオブジェクトを作成しています。 このオブジェクトはIExplorerCommandProviderを実装している必要があります。

コマンドバーにおける1つのコマンドはIExplorerCommandで識別され、 これはIEnumExplorerCommandで列挙されることになります。 シェルはIExplorerCommandProvider::GetCommandsを通じて、IEnumExplorerCommandを取得します。

STDMETHODIMP CExplorerCommandProvider::GetCommands(IUnknown *punkSite, REFIID riid, void **ppv)
{
	HRESULT              hr;
	CEnumExplorerCommand *p;

	p = new CEnumExplorerCommand((LPCOMMAND)g_command, 2);
	hr = p->QueryInterface(riid, ppv);
	p->Release();

	return hr;
}

上記のように、IEnumExplorerCommandを実装したCEnumExplorerCommandを作成します。 第1引数にはg_commandという変数を指定していますが、これは独自に定義したCOMMAND構造体の配列です。 COMMAND構造体は1つのコマンドの情報を格納しており、後述するCExplorerCommandと関連付けられることになります。 第2引数はコマンドの数であり、今回は「コマンド1」と「コマンド2」という2つのコマンドを作成しますから2を指定しています。 punkSiteで表されるオブジェクトはIServiceProviderを実装しており、 QueryServiceからIShellBrowserを取得することができます。

COMMAND構造体は次のように定義されています。

struct COMMAND {
	const GUID    *lpguid;
	WCHAR         szTitle[10];
	EXPCMDFLAGS   flags;
	const COMMAND *lpSubCommandArray;
	ULONG         uSubCommandCount;
};

lpguidは、コマンドを識別するGUIDのアドレスを指定します。 szTitleは、コマンドの名前を指定します。 flagsは、コマンドに関する定数を指定します。 lpSubCommandArrayは、一連のサブコマンドを表すCOMMAND構造体の配列を指定します。 コマンドがサブコマンドを持たない場合はNULLを指定します。 uSubCommandCountは、サブコマンドの数を指定します。 lpSubCommandArrayがNULLの場合は0を指定します。 コマンドにツールチップやアイコンを表示したい場合は、そのためのメンバをさらに追加することになります。

g_commandの定義は次のようになっています。

const COMMAND g_subCommand[] = {
	{&GUID_SubCommand1, L"サブコマンド1", 0, NULL, 0},
	{&GUID_SubCommand2, L"サブコマンド2", 0, NULL, 0}
};

const COMMAND g_command[] = {
	{&GUID_Command1, L"コマンド1", ECF_HASSUBCOMMANDS, g_subCommand, 2},
	{&GUID_Command2, L"コマンド2", 0, NULL, 0}
};

g_commandの1番目の要素は、「コマンド1」というボタンの情報を格納しています。 第3引数にECF_HASSUBCOMMANDSを指定した場合は、コマンドの横に矢印が表示されてサブコマンドを表示できますから、 第4引数にg_subCommandというサブコマンドの配列を指定しています。 g_commandの2番目の要素は、「コマンド2」というボタンの情報を格納しています。 このコマンドはサブコマンドを持たないため、第4引数にはNULLを指定しています。

COMMAND構造体というのはシェル拡張内でコマンドを表すためのものであり、 シェルがコマンドを識別するために使用するのはあくまでIExplorerCommandです。 シェルはIEnumExplorerCommand::Nextでこれを取得しようとするため、 IExplorerCommandを実装したCExplorerCommandを返すようにしています。

STDMETHODIMP CEnumExplorerCommand::Next(ULONG celt, IExplorerCommand **pUICommand, ULONG *pceltFetched)
{
	if (m_uCount >= m_uMaxCount)
		return E_FAIL;
	
	*pUICommand = new CExplorerCommand((LPCOMMAND)&m_lpCommandArray[m_uCount]);
		
	if (pceltFetched != NULL)
		*pceltFetched = 1;
	
	m_uCount++;

	return S_OK;
}

CExplorerCommandの第1引数は、このコマンドに関連付けたいCOMMAND構造体のアドレスを指定します。 この関連付けによってCExplorerCommandの実装は、COMMAND構造体のメンバを返すだけで済むようになります。 m_lpCommandArrayは列挙の対象となるCOMMAND構造体の配列であり、 今回の場合はg_commandかg_subCommandを識別することになります。 m_uCountは列挙したコマンドの数であり、m_uMaxCountは列挙できる最大のコマンド数です。

IExplorerCommandを取得したシェルは、様々なメソッドを呼び出してコマンドの情報を取得することになります。 まずは、次に示すメソッド群を確認します。

STDMETHODIMP CExplorerCommand::GetTitle(IShellItemArray *psiItemArray, LPWSTR *ppszName)
{
	SHStrDup(m_lpCommand->szTitle, ppszName);

	return S_OK;
}

STDMETHODIMP CExplorerCommand::GetIcon(IShellItemArray *psiItemArray, LPWSTR *ppszIcon)
{
	return E_NOTIMPL;
}

STDMETHODIMP CExplorerCommand::GetToolTip(IShellItemArray *psiItemArray, LPWSTR *ppszInfotip)
{
	return E_NOTIMPL;
}

STDMETHODIMP CExplorerCommand::GetCanonicalName(GUID *pguidCommandName)
{
	*pguidCommandName = *m_lpCommand->lpguid;

	return S_OK;
}

STDMETHODIMP CExplorerCommand::GetState(IShellItemArray *psiItemArray, BOOL fOkToBeSlow, EXPCMDSTATE *pCmdState)
{
	*pCmdState = ECS_ENABLED;

	return S_OK;
}

STDMETHODIMP CExplorerCommand::GetFlags(EXPCMDFLAGS *pFlags)
{
	*pFlags = m_lpCommand->flags;

	return S_OK;
}

GetTitleは、コマンドの名前を取得する際に呼ばれます。 コマンドの名前はCOMMAND構造体のszTitleに格納しているため、これを参照することになります。 SHStrDupを呼び出せば第1引数の文字列を格納した新たなメモリが確保され、そのアドレスが第2引数に格納されます。 GetIconは、アイコンを格納したモジュールのパスを取得する際に呼ばれます。 今回はコマンドの横にアイコンを表示するつもりがないので、E_NOTIMPLを返すようにしています。 GetToolTipは、コマンドのツールチップ文字列を取得する際に呼ばれます。 これも今回は表示しないためE_NOTIMPLを返しています。 GetCanonicalNameは、コマンドを識別するGUIDを取得する際に呼ばれます。 このメソッドは必ず実装するようにします。 GetStateは、コマンドの状態を取得する際に呼ばれます。 今回のコマンドは常に有効になっているため、常にECS_ENABLEDを返すようにしています。 GetFlagsは、コマンドのフラグを取得する際に呼ばれます。 フラグはCOMMAND構造体のflagsに格納しているため、これを返せば問題ありません。 各メソッドの第1引数はIShellItemArrayになっているため、 GetItemAtを呼び出すことで現在選択されているアイテムのIShellItemを取得することができます。

フラグとしてECF_HASSUBCOMMANDSを含むコマンドは、サブコマンドを表示することができます。 サブコマンドを表示すべきに段階になるとEnumSubCommandsが呼ばれます。

STDMETHODIMP CExplorerCommand::EnumSubCommands(IEnumExplorerCommand **ppEnum)
{
	if (m_lpCommand->lpSubCommandArray == NULL)
		return E_NOTIMPL;

	*ppEnum = new CEnumExplorerCommand((LPCOMMAND)m_lpCommand->lpSubCommandArray, m_lpCommand->uSubCommandCount);
	
	return S_OK;
}

まず、COMMAND構造体にサブコマンドの配列が格納されているかを確認します。 これが格納されていない場合は、そのコマンドはサブコマンドを持ちませんから処理を続行しないようにします。 サブコマンドの配列が格納されている場合は、それとサブコマンドの数をCEnumExplorerCommandに指定します。 これによりIEnumExplorerCommand::Nextでサブコマンドが列挙されるようになります。

最後にコマンドが選択された場合の処理を確認します。 コマンドが選択された場合はInvokeが呼ばれます。

STDMETHODIMP CExplorerCommand::Invoke(IShellItemArray *psiItemArray, IBindCtx *pbc)
{
	if (IsEqualGUID(*m_lpCommand->lpguid, GUID_Command1))
		MessageBox(NULL, TEXT("1番目のコマンドが選択されました。"), TEXT("OK"), MB_OK);
	else if (IsEqualGUID(*m_lpCommand->lpguid, GUID_Command2))
		MessageBox(NULL, TEXT("2番目のコマンドが選択されました。"), TEXT("OK"), MB_OK);
	else if (IsEqualGUID(*m_lpCommand->lpguid, GUID_SubCommand1))
		MessageBox(NULL, TEXT("1番目のサブコマンドが選択されました。"), TEXT("OK"), MB_OK);
	else if (IsEqualGUID(*m_lpCommand->lpguid, GUID_SubCommand2))
		MessageBox(NULL, TEXT("2番目のサブコマンドが選択されました。"), TEXT("OK"), MB_OK);
	else
		;

	return S_OK;
}

上記ではコマンドのGUID毎に処理を分けているわけですが、この方法はあまり好ましくないかもしれません。 CExplorerCommandは1つのコマンドを識別するためのものですから、 複数のコマンドの処理がここに含まれるのは本来なら矛盾していると言えます。 ところで、今回の実装では「コマンド1」の押下時にInvokeは呼ばれません。 理由は、このコマンドがECF_HASSUBCOMMANDSを含んでおり、 押下時にはEnumSubCommandsが呼ばれることになるかです。 サブコマンドを持ちながらそのコマンドの押下もサポートしたい場合は、フラグにECF_HASSPLITBUTTONを指定すればよいでしょう。

既定のコマンドについて

IExplorerCommandを実装したオブジェクトを返した場合、既定で「整理」や「表示」といったコマンドが表示されるのは既に述べた通りです。 この「整理」というコマンドには、「新しいフォルダ」や「名前の変更」といったサブコマンドが含まれていますが、 今回の場合はこれらのコマンドが無効になって選択できません。 これらを有効にして選択するためにはどのようにすればよいのでしょうか。 まず、「新しいフォルダ」というコマンドですが、これはIShellFolder::CreateViewObjectでITransferDestinationを実装したオブジェクトを返せば有効になります。 一方、「名前の変更」というコマンドを有効にするには、その操作を表す定数をアイテムの属性に指定します。 アイテムはITEMDATA構造体で識別していましたが、この構造体のattributeメンバにSFGAO_CANRENAMEを指定すれば、 アイテムの選択時に「名前の変更」が有効になります。

コマンドを有効にすること自体は上記のように簡単ですが、このコマンドが選択された際の通知を受け取る方法がよく分かりません。 ITransferDestinationにはCreateItemというメソッドがありますが、 「新しいフォルダ」というコマンドが選択された場合でもメソッドは呼ばれないようです。 「名前の変更」や「削除」といったコマンドについても同じく分かりませんが、 ITransferSourceというインターフェースには、RenameItemやRemoveItemというメソッドが含まれています。 よって、これを基に通知を受け取ることができるのではないかと考えたのですが、 IShellFolder::CreateViewObjectにはITransferSourceのIIDが指定されることはありませんでした。



戻る