EternalWindows
シェル拡張 / IShellFolderの実装

仮想フォルダを実装するために最低限必要となるインターフェースは、IShellFolderとIPersistFolderです。 IShellFolderは、アイテムの情報を取得する場合やフォルダビューを作成する場合などに使用され、 IPersistFolderは仮想フォルダにPIDLを設定する場合に使用されます。 今回は、これらを継承したCShellFolderというクラスを定義することにします。

class CShellFolder : public IShellFolder2, public IPersistFolder2
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();

	STDMETHODIMP ParseDisplayName(HWND hwnd, IBindCtx *pbc, LPWSTR pszDisplayName, ULONG *pchEaten, PIDLIST_RELATIVE *ppidl, ULONG *pdwAttributes);
	STDMETHODIMP EnumObjects(HWND hwndOwner, SHCONTF grfFlags, IEnumIDList **ppenumIDList);
	STDMETHODIMP BindToObject(PCUIDLIST_RELATIVE pidl, IBindCtx *pbc, REFIID riid, void **ppvOut);
	STDMETHODIMP BindToStorage(PCUIDLIST_RELATIVE pidl, IBindCtx *pbc, REFIID riid, void **ppvOut);
	STDMETHODIMP CompareIDs(LPARAM lParam, PCUIDLIST_RELATIVE pidl1, PCUIDLIST_RELATIVE pidl2);
	STDMETHODIMP CreateViewObject(HWND hwndOwner, REFIID riid, void **ppv);
	STDMETHODIMP GetAttributesOf(UINT cidl, PCUITEMID_CHILD_ARRAY apidl, SFGAOF *rgfInOut);
	STDMETHODIMP GetUIObjectOf(HWND hwndOwner, UINT cidl, PCUITEMID_CHILD_ARRAY apidl, REFIID riid, UINT *rgfReserved, void **ppv);
	STDMETHODIMP GetDisplayNameOf(PCUITEMID_CHILD pidl, SHGDNF uFlags, STRRET *pName);
	STDMETHODIMP SetNameOf(HWND hwndOwner, PCUITEMID_CHILD pidl, LPCWSTR pszName, SHGDNF uFlags, PITEMID_CHILD *ppidlOut);

	STDMETHODIMP GetDefaultSearchGUID(GUID *pguid);
	STDMETHODIMP EnumSearches(IEnumExtraSearch **ppEnum);
	STDMETHODIMP GetDefaultColumn(DWORD dwReserved, ULONG *pSort, ULONG *pDisplay);
	STDMETHODIMP GetDefaultColumnState(UINT iColumn, SHCOLSTATEF *pcsFlags);
	STDMETHODIMP GetDetailsEx(PCUITEMID_CHILD pidl, const SHCOLUMNID *pscid, VARIANT *pv);
	STDMETHODIMP GetDetailsOf(PCUITEMID_CHILD pidl, UINT iColumn, SHELLDETAILS *psd);
	STDMETHODIMP MapColumnToSCID(UINT iColumn, SHCOLUMNID *pscid);

	STDMETHODIMP GetClassID(CLSID *pClassID);
	STDMETHODIMP Initialize(LPCITEMIDLIST pidl);
	STDMETHODIMP GetCurFolder(LPITEMIDLIST *ppidl);

	CShellFolder();
	~CShellFolder();
	void SetLevel(BYTE level);

private:
	LONG              m_cRef;
	BYTE              m_level;
	PIDLIST_ABSOLUTE  m_pidl;
};

この定義をよく見ると、IShellFolderではなくIShellFolder2を継承し、 さらにIPersistFolderではなくIPersistFolder2を継承していますが、これは問題ありません。 理由は、上位(数字が2)のインターフェースが下位のインターフェースを継承しているからです。 独自のフォルダビューを作成する場合は、下位のインターフェースを実装するだけで問題ないのですが、 デフォルトのフォルダビューを使用する場合は上位のインターフェースも実装することになります。

それでは、IShellFolderのメソッドから順に見ていくことにします。 EnumObjectsとGetUIObjectOfについては、後の節で取り上げます。

STDMETHODIMP CShellFolder::ParseDisplayName(HWND hwnd, IBindCtx *pbc, LPWSTR pszDisplayName, ULONG *pchEaten, PIDLIST_RELATIVE *ppidl, ULONG *pdwAttributes)
{
	return E_NOTIMPL;
}

ParseDisplayNameは、アイテムの名前からアイテムのPIDLを取得する際に呼ばれるとされていますが、 今回のサンプルでは呼び出しを確認することができませんでした。 よって、実装しないことを示すE_NOTIMPLを返しています。

STDMETHODIMP CShellFolder::BindToObject(PCUIDLIST_RELATIVE pidl, IBindCtx *pbc, REFIID riid, void **ppvOut)
{
	CShellFolder *p;
	HRESULT      hr;

	p = new CShellFolder();
	
	hr = p->QueryInterface(riid, ppvOut);
	if (SUCCEEDED(hr)) {
		PIDLIST_ABSOLUTE pidlNew = ILCombine(m_pidl, pidl);
		p->Initialize(pidlNew);
		p->SetLevel(m_level + 1);
	}

	p->Release();

	return hr;
}

BindToObjectは、主に表示されたフォルダを切り替える際に呼ばれます。 まず、新しく表示されるフォルダのためにCShellFolderを作成し、 要求されたインターフェースを実装しているかを確認するためにQueryInterfaceを呼び出します。 ここで失敗した場合は、オブジェクトの参照カウントが1のままですから、 Releaseの実行によってオブジェクトは開放されることになります。 成功した場合は、新しいオブジェクトにPIDLを設定するためにInitializeを呼び出すことになります。 ここで指定するPIDLは、現在のフォルダのPIDL(m_pidl)とバインド先の新しいフォルダのPIDLを連結したものでなければなりません。 m_levelはフォルダの階層を表すメンバであり、 新しく作成されたフォルダのm_levelは現在の階層より1つ大きくなっていなければなりません。 自作メソッドであるSetLevelを呼び出すと、第1引数の値がm_levelに設定されます。 なお、BindToObjectは前のフォルダに戻った場合にも呼ばれますが、 その際に指定されるインターフェースは今回実装していないため、 オブジェクトは破棄されることになります。

STDMETHODIMP CShellFolder::BindToStorage(PCUIDLIST_RELATIVE pidl, IBindCtx *pbc, REFIID riid, void **ppvOut)
{
	return BindToObject(pidl, pbc, riid, ppvOut);
}

BindToStorageは、BindToObjectを呼び出すだけで問題ありません。 ただし、このメソッドが呼ばれることはないと思われます。

STDMETHODIMP CShellFolder::CompareIDs(LPARAM lParam, PCUIDLIST_RELATIVE pidl1, PCUIDLIST_RELATIVE pidl2)
{
	LPITEMDATA lpData1 = (LPITEMDATA)pidl1;
	LPITEMDATA lpData2 = (LPITEMDATA)pidl2;
	USHORT     nCode;

	nCode = StrCmpW(lpData1->szName, lpData2->szName);

	return MAKE_HRESULT(0, 0, nCode);
}

CompareIDsは、2つのPIDLを比較する際に呼ばれます。 たとえば、デフォルトのフォルダビューのポップアップメニューには「並び替え」という項目がありますが、 この中の「昇順」や「降順」を選択するとCompareIDsが呼ばれます。 CompareIDsでは、pidl1 > pidl2の場合に0より大きい値を返し、 pidl1 < pidl2の場合に0より小さい値を返し、 pidl1 = pidl2の場合は0を返すことになっていますが、 この調べ方についてはPIDLの中身によって異なるでしょう。 次節で取り上げるように、今回のサンプルはPIDLの中身がITEMDATA構造体になっており、 この構造体のszNameにはアイテムの名前が格納されています。 2つの文字列を比較するStrCmpにアイテムの名前を指定すれば、 CompareIDsの望む形で戻り値を得ることができます。

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);
	}

	return hr;
}

CreateViewObjectは、フォルダビューを作成する際に呼ばれます。 デフォルトのフォルダビューを使用する場合は、SHCreateShellFolderViewを呼び出すだけで構いません。

STDMETHODIMP CShellFolder::GetAttributesOf(UINT cidl, PCUITEMID_CHILD_ARRAY apidl, SFGAOF *rgfInOut)
{
	LPITEMDATA lpData = (LPITEMDATA)apidl[0];
	
	*rgfInOut &= lpData->attribute;

	return S_OK;
}

GetAttributesOfは、アイテムの属性を取得する際に呼ばれます。 rgfInOutには要求する属性が格納されており、 メソッド内ではこの属性をアイテムがサポートしているかどうかを返す必要があります。 アイテムの属性はITEMDATA構造体のattributeに格納しているため、 これとrgfInOutの論理積をとります。

STDMETHODIMP CShellFolder::GetDisplayNameOf(PCUITEMID_CHILD pidl, SHGDNF uFlags, STRRET *pName)
{
	LPITEMDATA lpData = (LPITEMDATA)pidl;

	pName->uType = STRRET_WSTR;
	SHStrDupW(lpData->szName, &pName->pOleStr);

	return S_OK;
}

GetDisplayNameOfは、アイテムに関連する名前を取得する際に呼ばれます。 STRRET構造体のuTypeは名前のフォーマットであり、 STRRET_WSTRを指定した場合はpOleStrメンバにUNICODE文字列へのポインタを格納することになります。 SHStrDupを呼び出せば、第1引数の文字列がコピーされた新しいメモリが確保され、 そのメモリへのアドレスが第2引数に格納されます。

STDMETHODIMP CShellFolder::SetNameOf(HWND hwndOwner, PCUITEMID_CHILD pidl, LPCWSTR pszName, SHGDNF uFlags, PITEMID_CHILD *ppidlOut)
{
	return E_NOTIMPL;
}

SetNameOfは、指定されたPIDLに対して名前を設定する際に呼ばれるとされていますが 今回のサンプルでは呼び出しを確認することができませんでした。 よって、実装しないことを示すE_NOTIMPLを返しています。

続いて、IShellFolder2のメソッドを順に見ていきます。

STDMETHODIMP CShellFolder::GetDefaultSearchGUID(GUID *pguid)
{
	return E_NOTIMPL;
}

STDMETHODIMP CShellFolder::EnumSearches(IEnumExtraSearch **ppEnum)
{
	return E_NOINTERFACE;
}

GetDefaultSearchGUIDとEnumSearchesは、呼ばれることがないようなのでE_NOTIMPLを返すだけで構いません。

STDMETHODIMP CShellFolder::GetDefaultColumn(DWORD dwReserved, ULONG *pSort, ULONG *pDisplay)
{
	*pSort = 0;
	*pDisplay = 0;

	return S_OK;
}

GetDefaultColumnは、デフォルト表示のカラムのインデックスと、ソート時に参照するカラムのインデックスを取得する際に呼ばれます。 今回はカラムを1つしか追加しないため、どちらの変数にも0を指定することになります。 ちなみに、ソート時に参照するカラムというのは、既に示したCompareIDsの第1引数に大きく関係します。 名前とサイズという2つのカラムが存在するとして、CompareIDsの第1引数に1が指定された場合、 CompareIDsは名前ではなくサイズを基にアイテムの比較を行うことになります。 つまり、複数のカラムが存在する場合は、その数だけ比較のアルゴリズムが必要になります。

STDMETHODIMP CShellFolder::GetDefaultColumnState(UINT iColumn, SHCOLSTATEF *pcsFlags)
{
	if (iColumn == 0) {
		*pcsFlags = SHCOLSTATE_ONBYDEFAULT | SHCOLSTATE_TYPE_STR;
		return S_OK;
	}

	return E_INVALIDARG;
}

GetDefaultColumnStateは、第1引数のカラムのデフォルト状態を取得する際に呼ばれます。 今回はカラムを1つしか追加していないため、 インデックスが0であるカラムの場合のみ処理を行います。 SHCOLSTATE_ONBYDEFAULTはこのカラムがデフォルト表示のカラムであることを意味し、 SHCOLSTATE_TYPE_STRはカラムに設定される値が文字列であることを意味します。

STDMETHODIMP CShellFolder::GetDetailsEx(PCUITEMID_CHILD pidl, const SHCOLUMNID *pscid, VARIANT *pv)
{
	return E_NOTIMPL;
}

GetDetailsExは、第1引数のアイテムから第2引数のカラムに相当するデータを取得する際に呼ばれます。 GetDetailsOfを実装している場合は、特に実装する必要がないためE_NOTIMPLを返すようにしています。

STDMETHODIMP CShellFolder::GetDetailsOf(PCUITEMID_CHILD pidl, UINT iColumn, SHELLDETAILS *psd)
{
	WCHAR   szBuf[256];
	HRESULT hr = E_FAIL;

	if (pidl == NULL) {
		if (iColumn == 0) {
			psd->fmt = LVCFMT_LEFT;
			StrCpyW(szBuf, L"名前");
			hr = S_OK;
		}
	}
	else {
		if (iColumn == 0) {
			psd->fmt = LVCFMT_LEFT;
			StrCpyW(szBuf, ((LPITEMDATA)pidl)->szName);
			hr = S_OK;
		}
	}

	if (hr == S_OK) {
		psd->str.uType = STRRET_WSTR;
		hr = SHStrDupW(szBuf, &psd->str.pOleStr);
	}

	return hr;
}

GetDetailsOfは、第1引数のアイテムから第2引数のカラムに相当するデータを取得する際に呼ばれます。 GetDetailsExは、カラムをSHCOLUMNID構造体というキーで識別することになっていますが、 このメソッドはカラムをインデックスで識別するので比較的分かりやすいといえます。 pidlがNULLである場合は、カラムそのものの名前を返さなければならないため、 カラムのフォーマットを設定してバッファに名前をコピーします。 カラムを複数個追加したい場合は、iColumnが1以上であるときにも同じような処理を行うことになります。 pidlがNULLでない場合は、カラムに関連するアイテムのデータを返すことになります。 名前というカラムに関連するデータはアイテムの名前でなければならないため、 ITEMDATA構造体のszNameメンバをバッファにコピーしています。 SHStrDupによって、初期化されたバッファの内容が専用のメモリにコピーされ、 SHELLDETAILS構造体のstr.pOleStrがそのアドレスを受け取ることができます。

STDMETHODIMP CShellFolder::MapColumnToSCID(UINT iColumn, SHCOLUMNID *pscid)
{
	return E_NOTIMPL;
}

MapColumnToSCIDは、第1引数のインデックスで識別されるカラムから関連するキーを取得する際に呼ばれます。 GetDetailsExを実装していない場合は、E_NOTIMPLを返すだけで構いません。

CShellFolderが継承しているIPersistFolder2は、内部的にIPersistとIPersistFolderを継承しています。 よって、これら3つのインターフェースのメソッドを実装する必要があります。

STDMETHODIMP CShellFolder::GetClassID(CLSID *pClassID)
{
	*pClassID = CLSID_ShellFolder;

	return S_OK;
}

STDMETHODIMP CShellFolder::Initialize(LPCITEMIDLIST pidl)
{
	if (pidl == NULL)
		return E_FAIL;

	m_pidl = ILClone(pidl);

	return S_OK;
}

STDMETHODIMP CShellFolder::GetCurFolder(LPITEMIDLIST *ppidl)
{
	if (m_pidl == NULL || ppidl == NULL)
		return E_FAIL;

	*ppidl = ILClone(m_pidl);

	return S_OK;
}

GetClassIDはIPersistのメソッドであり、CLSIDを返すだけで問題ありません。 InitializeはIPersistFolderのメソッドであり、このフォルダのPIDLが与えられることになります。 このPIDLは後で使用することになるため、メンバ変数であるm_pidlに保存しておきます。 pidlはメソッドが制御を返した後に開放される可能性があるので、保存の際はILCloneで複製するようにします。 GetCurFolderはIPersistFolder2のメソッドであり、このフォルダのPIDLを返すことになります。 これは、保存しておいたm_pidlを複製して格納すれば問題ありません。


戻る