EternalWindows
シェル拡張 / UIオブジェクトの実装

前節ではCEnumIDListを実装し、このオブジェクトをCShellFolder::EnumObjectsで返すことに成功しました。 これにより、呼び出し側はCEnumIDList::Nextを呼び出すことによって、フォルダ内のアイテムを列挙できるようになりますが、 そのアイテムがフォルダビューで正しく機能するかどうかはまた別の話になります。 たとえば、アイテムに関連するアイコンを返さなければアイテムにアイコンが設定されませんし、 ポップアップメニューを返さなければ右クリック時にポップアップメニューが表示されませんから、 CShellFolderはこうしたUI情報についても考慮する必要があります。

UI情報を取得する際に呼ばれるメソッドは、IShellFolder::GetUIObjectOfです。 このメソッドには、取得したいUIを示すインターフェースのIIDが指定され、 そこにはIContextMenuやIExtractIconなどのIIDも含まれます。 IContextMenuはポップアップメニューを初期化するインターフェースであり、 IExtractIconはアイコンのハンドルを返すインターフェースですから、 これらのインターフェースは可能な限り返すようにしたほうがよいでしょう。 インターフェースを返すというのは、それを実装したオブジェクトのアドレスを返すということですから、 例によってオブジェクトのためのクラスを定義することになります。

class CUIObject : public IContextMenu, public IExtractIcon, public IObjectWithSite
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();

	STDMETHODIMP QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags);
	STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici);
	STDMETHODIMP GetCommandString(UINT_PTR idCmd, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax);
	
	STDMETHODIMP GetIconLocation(UINT uFlags, LPTSTR szIconFile, UINT cchMax, int *piIndex, UINT *pwFlags);
	STDMETHODIMP Extract(LPCTSTR pszFile, UINT nIconIndex, HICON *phiconLarge, HICON *phiconSmall, UINT nIconSize);
	
	STDMETHODIMP SetSite(IUnknown *pUnkSite);
	STDMETHODIMP GetSite(REFIID riid, void **ppvSite);

	CUIObject(PITEMID_CHILD pidl);
	~CUIObject();

private:
	LONG          m_cRef;
	PITEMID_CHILD m_pidl;
	IUnknown      *m_pUnkSite;
};

IContextMenuとIExtractIconを継承したCUIObjectというクラスを定義しています。 インターフェース毎にクラスを定義することもできますが、 そうするとGetUIObjectOfで返すインターフェースが増えるにつれてクラスの数も増えることになるため、 1つのクラスでまとめて継承するようにしています。 IObjectWithSiteについては後述します。

CUIObjectのオブジェクトは、CShellFolder::GetUIObjectOfで作成します。

STDMETHODIMP CShellFolder::GetUIObjectOf(HWND hwndOwner, UINT cidl, PCUITEMID_CHILD_ARRAY apidl, REFIID riid, UINT *rgfReserved, void **ppv)
{
	HRESULT hr = E_NOINTERFACE;

	if (IsEqualIID(riid, IID_IContextMenu)) {
		CUIObject *p;

		p = new CUIObject((LPITEMIDLIST)apidl[0]);
		hr = p->QueryInterface(riid, ppv);
		p->Release();
	}
	else if (IsEqualIID(riid, IID_IExtractIcon)) {
		CUIObject *p;

		p = new CUIObject((LPITEMIDLIST)apidl[0]);
		hr = p->QueryInterface(riid, ppv);
		p->Release();
	}
	else
		;

	return hr;
}

CUIObjectのコンストラクタの第1引数は、アイテムのPIDLを指定することになります。 対象となるアイテムが分からなければアイコンを取得したりできませんから、 これは必要な情報になります。 GetUIObjectOfに指定されるその他のインターフェースとしてはIDataObjectなどがありますが、 実装しなくても特に問題はありません。

CUIObjectはIContextMenuを継承していますから、IContextMenuのメソッドを実装することになります。 次に示すQueryContextMenuは、アイテム上で右クリックが押された場合や、 ダブルクリックされた場合に呼ばれます。

STDMETHODIMP CUIObject::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
	MENUITEMINFO mii;
	
	mii.cbSize     = sizeof(MENUITEMINFO);
	mii.fMask      = MIIM_ID | MIIM_TYPE | MIIM_STATE;
	mii.fType      = MFT_STRING;
	mii.fState     = MFS_DEFAULT;
	mii.wID        = idCmdFirst;
	mii.dwTypeData = TEXT("開く");
	
	InsertMenuItem(hmenu, idCmdFirst, FALSE, &mii);

	return MAKE_SCODE(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

InsertMenuItemを呼び出して、第1引数のポップアップメニューに独自の項目を追加します。 項目のIDはidCmdFirstからidCmdLastの間でなければならないため、MENUITEMINFO構造体のwIDにはidCmdFirstを指定しています。 項目をさらに追加する場合は、idCmdFirstを1つカウントするようにしてください。 fStateにMFS_DEFAULTを指定するのは非常に重要です。 これにより、追加した項目はデフォルトアイテムとして認識され、 ダブルクリック時にInvokeCommandが呼ばれるようになります。 MAKE_SCODEの第3引数は追加した項目の数であるため、1を指定します。

QueryContextMenuでポップアップメニューを初期化した呼び出し側は、 TrackPopupMenu などでポップアップメニューを表示するだろうと思われます。 そして、項目が選択された場合はそれを実行するためにInvokeCommandが呼ばれます。

STDMETHODIMP CUIObject::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
{
	UINT idCmd = LOWORD(pici->lpVerb);

	if (HIWORD(pici->lpVerb) != 0)
		return E_INVALIDARG;

	if (idCmd == 0) {
		LPITEMDATA lpData = (LPITEMDATA)m_pidl;
		
		if (lpData->attribute & SFGAO_FOLDER) {
			IServiceProvider *pServiceProvider;
			IShellBrowser    *pShellBrowser;
			HRESULT          hr;

			hr = m_pUnkSite->QueryInterface(IID_PPV_ARGS(&pServiceProvider));
			if (SUCCEEDED(hr)) {
				hr = pServiceProvider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&pShellBrowser));
				if (SUCCEEDED(hr)) {
					pShellBrowser->BrowseObject(m_pidl, SBSP_SAMEBROWSER | SBSP_RELATIVE);
					pShellBrowser->Release();
				}
				pServiceProvider->Release();
			}
		}
		else
			MessageBox(NULL, lpData->szName, TEXT("OK"), MB_OK);
	}
	else
		;

	return S_OK;
}

pici->lpVerbの下位ワードには、選択された項目へのオフセットが格納されることになっています。 たとえば、これが0である場合は1番目の項目が選択されたと分かるため、 LOWORDマクロで取得することになります。 ただし、上位ワードに0でない値が格納されている場合は、処理を続行する必要はありません。 ポップアップメニューの1番目の項目は「開く」という項目であり、 選択されたアイテムがフォルダであるかどうかで実行する処理を分岐させます。 フォルダである場合は1つの下の階層に進む必要がありますから、 その事をエクスプローラに伝えるためにIShellBrowser::BrowseObjectを呼び出す必要があります。 このインターフェースを取得する手順としては、まずm_pUnkSite(後述)からIServiceProviderを取得し、 その後にIServiceProvider::QueryServiceにSID_STopLevelBrowserとIID_IShellBrowserを指定することで取得できます。 BrowseObjectの第1引数は、実行するアイテムのPIDLを指定します。 このPIDLは完全なPIDLではないため、第2引数にはSBSP_RELATIVEを指定するようにします。 SBSP_SAMEBROWSERを指定した場合はフォルダの中身が現在のフォルダの中に表示されることになりますが、 新しくフォルダを作成したい場合はSBSP_NEWBROWSERを指定するようにしてください。 選択したアイテムがフォルダでない場合は、そのアイテムに応じた動作を行うことになります。 今回はアイテムの名前をメッセージボックスで表示しているだけですが、 実際の開発ではダイアログなどを表示することになると思われます。 なお、InvokeCommandでIShellViewが取得したい場合は、 IShellBrowser::QueryActiveShellViewを呼び出してください。

続いて、IExtractIconのメソッドについて見ていきます。 IExtractIconには、ExtractとGetIconLocationというメソッドがありますが、 アイコンを取得する段階で先に呼ばれるのはGetIconLocationです。

STDMETHODIMP CUIObject::GetIconLocation(UINT uFlags, LPTSTR szIconFile, UINT cchMax, int *piIndex, UINT *pwFlags)
{
	*pwFlags = GIL_NOTFILENAME | GIL_DONTCACHE;

	return S_OK;
}

GetIconLocationでは、第2引数にアイコンを格納するファイルのパスを格納し、 第4引数に取得するアイコンのインデックスを格納することになっています。 しかし、今回はファイルからアイコンを取得しないことになっているので、 第5引数のフラグにGIL_NOTFILENAMEを指定しています。 これを指定した場合は、第2引数と第4引数を初期化する必要はなくなります。 GIL_DONTCACHEは、デバッグ段階では指定しておいたほうがよいでしょう。 これを指定しないと、Extractで返すアイコンを変化させてもキャッシュされたアイコンが依然として表示され、 新しいアイコンを確認できないことになります。

GetIconLocationが成功すれば、Extractが呼ばれます。

STDMETHODIMP CUIObject::Extract(LPCTSTR pszFile, UINT nIconIndex, HICON *phiconLarge, HICON *phiconSmall, UINT nIconSize)
{
	LPTSTR     nId[] = {IDI_EXCLAMATION, IDI_QUESTION, IDI_ERROR};
	LPITEMDATA lpData = (LPITEMDATA)m_pidl;

	if (phiconLarge != NULL)
		*phiconLarge = (HICON)LoadImage(NULL, nId[lpData->iconIndex], IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (phiconSmall != NULL)
		*phiconSmall = (HICON)LoadImage(NULL, nId[lpData->iconIndex], IMAGE_ICON, 0, 0, LR_SHARED);

	return S_OK;
}

Extractでは、第3引数に大きいアイコンのハンドルを返し、第4引数に小さいアイコンのハンドルを返すことになります。 通常は、ファイルからアイコンを取得するためにExtractIconExなどを呼び出すことになると思われますが、 今回は簡単のため、LoadImageで既定のアイコンのハンドルを取得しています。

さて、残すインターフェースはIObjectWithSiteですが、 これはIContextMenuを使用する側がオブジェクトに対して情報を与えるために必要です。 既に述べたように、IContextMenuではIShellBrowserが必要になるわけですが、 オブジェクトがIObjectWithSiteを実装していれば、 IObjectWithSite::SetSiteを通じてIShellBrowserに関連するインターフェースを取得できるようになります。

STDMETHODIMP CUIObject::SetSite(IUnknown *pUnkSite)
{
	m_pUnkSite = pUnkSite;

	return S_OK;
}

STDMETHODIMP CUIObject::GetSite(REFIID riid, void **ppvSite)
{
	if (m_pUnkSite == NULL)
		return E_NOINTERFACE;

	return m_pUnkSite->QueryInterface(riid, ppvSite);
}

SetSiteに指定されるインターフェースからはIShellBrowserを取得することができるため、 必ずメンバとして保存することになります。 GetSiteについては、SetSiteで保存したメンバのQueryInterfaceを呼び出します。

デフォルトのUIオブジェクトについて

IShellFolder::GetUIObjectOfでは、IContextMenuやIExtractIconを実装したオブジェクトを返すことになりますが、 これはシェル拡張内で自作されたものでなくても構いません。 たとえば、Windows Vistaから登場したSHCreateDefaultContextMenuを呼び出せば、 IContextMenuを実装したデフォルトのオブジェクトを取得できますから、 このアドレスを返しても問題ありません。

if (IsEqualIID(riid, IID_IContextMenu)) {
	DEFCONTEXTMENU dcm;

	dcm.hwnd                = hwndOwner;
	dcm.pcmcb               = NULL;
	dcm.pidlFolder          = NULL;
	dcm.psf                 = static_cast<IShellFolder *>(this);
	dcm.cidl                = cidl;
	dcm.apidl               = apidl;
	dcm.punkAssociationInfo = NULL;
	dcm.cKeys               = 0;
	dcm.aKeys               = NULL;
	
	hr = SHCreateDefaultContextMenu(&dcm, riid, ppv);
}

デフォルトメニューの欠点は、メニュー項目にアプリケーションの意思が反映されないことだと思われます。 アイテムが属性としてSFGAO_FOLDERを含む場合は、「開く」という項目から下位のフォルダに切り替えることができるのですが、 属性が0であるアイテムに対してはメニュー自体が表示されません。 これでは、フォルダの階層を切り替えることしかできませんから、 やはりIContextMenuを実装したオブジェクトを自作する必要があるといえます。

Windows Vistaから登場したSHCreateDefaultExtractIconを呼び出せば、 IExtractIconを実装したデフォルトのオブジェクトも取得することができます。 正確には、この関数が返すのはIDefaultExtractIconなのですが、 QueryInterfaceを呼び出すことでIExtractIconを取得することができます。

else if (IsEqualIID(riid, IID_IExtractIcon)) {
	IDefaultExtractIconInit *pDefaultExtractIconInit;
	
	hr = SHCreateDefaultExtractIcon(IID_PPV_ARGS(&pDefaultExtractIconInit));
	if (SUCCEEDED(hr)) {
		hr = pDefaultExtractIconInit->SetNormalIcon(L"shell32.dll", 1);
		if (SUCCEEDED(hr))
			hr = pDefaultExtractIconInit->QueryInterface(riid, ppv);

		pDefaultExtractIconInit->Release();
	}
}

QueryInterfaceを呼び出す前にSetNormalIconを呼び出している点が重要です。 これによりIExtractIconは、第1引数のファイルから第2引数のインデックスのアイコンを取得すればよいものと理解できます。



戻る