EternalWindows
シェル拡張 / 仮想フォルダサンプル(カテゴリ)
サンプルはこちら

Windows XP以降のフォルダビューでは、フォルダ内のアイテムをグループ化できるようになっています。 グループ化を行うには、コンテキストメニューの「グループで表示」からカラム名を選択します。

フォルダビューがグループ化を独自に実装していない場合は、 上図のように既定のメカニズムでグループ化が行われることになります。 ただし、今回のようにグループ化を独自に実装している場合は、 次のように大文字と小文字でグループ化することができます。

各アイテムが大文字と小文字どちらのグループに属するかは、アイテム名の5番目の文字で決定することにしています。 5番目の文字というのは"アイテムA"の"A"であり、これは大文字ですから大文字のグループに属しています。 また、"アイテムB"の"B"も大文字ですから、このアイテムも大文字のグループに属しています。 しかし、たとたば"アイテムB"の名前を"アイテムb"としてコンパイルすると次のようになります。

"アイテムb"の5番目の文字は小文字になっているため、このアイテムは小文字というグループに属することになります。 フォルダビューが一覧表示である場合はグループ化できないことに注意してください。

アイテムをグループ化するには、ICategoryProviderを実装したオブジェクトが必要になります。 このオブジェクトはフォルダ上で右クリックされた場合に、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_ICategoryProvider)) {
		CCategoryProvider *p;

		p = new CCategoryProvider(static_cast<IShellFolder *>(this));
		hr = p->QueryInterface(riid, ppv);
		p->Release();
	}
	else
		;

	return hr;
}

riidがIID_ICategoryProviderである場合に、CCategoryProviderを型としたオブジェクトを作成しています。 CCategoryProviderはICategoryProviderを実装しているのでこれは問題ありません。

CCategoryProviderの実装を見ていく前に、CShellFolderの重要な点を説明します。 シェルはカラムのインデックスからそのカラムのプロパティキーを取得しようとするため、 これをMapColumnToSCIDで返すようにしておきます。

STDMETHODIMP CShellFolder::MapColumnToSCID(UINT iColumn, SHCOLUMNID *pscid)
{
	if (iColumn == 0) {
		*pscid = PKEY_ItemNameDisplay;
		return S_OK;
	}

	return E_FAIL;
}

今回は作成しているカラムが1つだけであるため、iColumnが0である場合のみ処理を行います。 0番目のカラム名は「名前」になっていたため、これを表すPKEY_ItemNameDisplayをpscidに格納すればよいでしょう。 SHCOLUMNID構造体は現在PROPERTYKEY構造体に置き換えられているため、 プロパティキーに関する定数を問題なく指定することができます。

MapColumnToSCIDでプロパティキーを返すようにすると、ICategoryProvider::CanCategorizeOnSCIDが呼ばれるようになります。 このメソッドでは、指定されたプロパティキーをサポートするかどうかを返します。

STDMETHODIMP CCategoryProvider::CanCategorizeOnSCID(const SHCOLUMNID *pscid)
{
	if (IsEqualPropertyKey(*pscid, PKEY_ItemNameDisplay))
		return S_OK;
	
	return S_FALSE;
}

今回サポートするプロパティキーはPKEY_ItemNameDisplayですから、これがpscidと一致する場合はS_OKを返すようにします。

メニューからカラムの名前が選択された場合は、GetCategoryForSCIDが呼ばれます。 ここでは、指定されたプロパティキーに関連するカテゴリオブジェクトのGUIDを返します。 カテゴリオブジェクトとは、ICategorizerを実装するオブジェクトのことです。

STDMETHODIMP CCategoryProvider::GetCategoryForSCID(const SHCOLUMNID *pscid, GUID *pguid)
{
	if (IsEqualPropertyKey(*pscid, PKEY_ItemNameDisplay)) {
		*pguid = GUID_Name;
		return S_OK;
	}

	return E_INVALIDARG;
}

カテゴリオブジェクトはカラムの数だけ存在しなければならないため、 個々のカテゴリオブジェクトを識別するためのGUIDが必要になります。 このGUIDはシェル拡張内で独自に定義されなければなりません。 PKEY_ItemNameDisplayのカラムに関連するカテゴリオブジェクトは、GUID_NameというGUIDで識別されるものとします。

GetCategoryForSCIDでS_OKを返すとCreateCategoryが呼ばれることになります。 ここでは実際にカテゴリオブジェクトを作成します。

STDMETHODIMP CCategoryProvider::CreateCategory(const GUID *pguid, REFIID riid, void **ppv)
{
	HRESULT hr;

	if (IsEqualIID(riid, IID_ICategorizer)) {
		if (IsEqualGUID(*pguid, GUID_Name)) {
			*ppv = new CCategorizerName(m_pShellFolder);
			hr = S_OK;
		}
		else
			hr = E_INVALIDARG;
	}
	else
		hr = E_NOINTERFACE;

	return hr;
}

riidがIID_ICategorizerである場合は、カテゴリオブジェクトのアドレスを返さなければなりませんが、 その前にGUIDの確認を行っておく必要があります。 今回作成するカテゴリオブジェクトはGUID_Nameで識別されるものだけですから、 この場合のみカテゴリオブジェクトを作成するようにします。

ICategorizerを取得したシェルは、フォルダに存在するアイテムの数だけGetCategoryを呼び出します。 ここでは、そのアイテムがどのグループ(カテゴリ)に属するかを返すことになります。

STDMETHODIMP CCategorizerName::GetCategory(UINT cidl, PCUITEMID_CHILD_ARRAY apidl, DWORD *rgCategoryIds)
{
	STRRET strret;

	m_pShellFolder->GetDisplayNameOf(apidl[0], SHGDN_NORMAL, &strret);
	if (IsCharUpper(strret.pOleStr[4]))
		rgCategoryIds[0] = CATEGORY_UPPER;
	else
		rgCategoryIds[0] = CATEGORY_LOWER;

	CoTaskMemFree(strret.pOleStr);

	return S_OK;
}

今回は、グループを決定するための要素としてアイテムの名前を使用しています。 具体的には、IShellFolder::GetDisplayNameOfでアイテムの名前を取得し、 この5番目の文字が大文字であるかをIsCharUpperで調べます。 そして結果が真である場合は大文字を表すカテゴリIDを返し、 真でない場合は小文字を表すカテゴリIDを返します。 CATEGORY_UPPERなどのカテゴリIDはシェル拡張内で独自に定義することになります。

カテゴリIDに関連する名前は、GetCategoryInfoを通じて取得されることになります。

STDMETHODIMP CCategorizerName::GetCategoryInfo(DWORD dwCategoryId, CATEGORY_INFO *pci)
{
	if (dwCategoryId == CATEGORY_UPPER)
		StrCpyW(pci->wszName, L"大文字");
	else
		StrCpyW(pci->wszName, L"小文字");

	return S_OK;
}

CATEGORY_INFO構造体のwszNameにカテゴリの名前を格納するようにします。 これで、フォルダビューにカテゴリの名前が表示されることになります。


戻る