EternalWindows
シェル名前空間 / ShellBrowserの位置づけ

これまで作成してきたファイラーは、フォルダやファイルをグラフィカルに表示しましたが、 これだけではファイラーとして十分な機能を備えているとは言えません。 たとえば、リストビューのアイテムを違う場所へD&Dする方法は取り上げていませんでしたから、 完璧なファイラーを開発する場合はこうした機能もサポートするべきと言えます。 しかし、こうした必要な機能を追い求めていくと、要はエクスプローラーの作成に近づくということであり、 それならばエクスプローラーが使用している機能を通常のアプリケーションでも利用できないものかと考えてしまうものです。 今回から取り上げるShellBrowserとフォルダビューの関係を理解すれば、これは可能になります。

フォルダビューとは、エクスプローラーのフォルダに表示されているリストビューのようなウインドウの事を指します。

このようなウインドウは、一見するとエクスプローラーによって作成されているように思えますが、 実際にはshell32.dllの関数で作成された既定のフォルダビューです。 つまり、エクスプローラー独自の機能ではないので、通常のアプリケーションからもフォルダビューを表示することが可能です。 ただし、フォルダビュー側としては、そのアプリケーションが何らかの既知のインターフェースを実装していなければ、 フォルダビューを扱う上での重要な通知を送ることができませんから、 アプリケーションはIShellBrowserというインターフェースを実装することになっています。 ShellBrowserとは、このIShellBrowserを実装したCOMオブジェクトのことであり、 フォルダビューの表示や管理を目的としています。

エクスプローラーのフォルダがフォルダビューを表示しているということは、 エクスプローラーのフォルダはShellBrowserが作成しているということになります。 今回はこれを証明するために、フォルダのウインドウからIShellBrowserを取得し、 それを使用してフォルダを制御する方法を取り上げます。 エクスプローラーはフォルダを作成する際に、そのウインドウとインターフェースをShellWindows(後述)に登録しているため、 IShellWindows::FindWindowSWを呼び出すことで、関連するウインドウハンドルとインターフェースを取得することができます。

HRESULT IShellWindows::FindWindowSW(
  VARIANT *pvarLoc,
  VARIANT *pvarLocRoot,
  int swClass,
  long *phwnd,
  int swfwOptions,
  IDispatch **ppdispOut
);

pvarLocは、フォルダのPIDLを格納したVARIANT構造体のアドレスを指定します。 pvarLocRootは、VT_EMPTYで初期化されたVARIANT構造体のアドレスを指定します。 swClassは、検索したいウインドウのタイプを表す定数を指定します。 phwndは、ウインドウハンドルを受け取る変数のアドレスを指定します。 ppdispOutは、IDispatchインターフェースを受け取る変数のアドレスを指定します。

IShellWindows::FindWindowSWで初期化されるppdispOutは、IShellBrowserを実装したCOMオブジェクトを識別しているわけではありません。 よって、ppdispOutのQueryInterfaceからIShellBrowserを取得することはできません。 ただし、ppdispOutによって識別されるオブジェクトはIServiceProviderを実装しており、 このインターフェースのQueryServiceを呼び出せば、 IShellBrowserを実装するオブジェクトを取得できるようになっています。

IShellBrowser    *pShellBrowser;
IServiceProvider *pServiceProvider;

pDispatch->QueryInterface(IID_PPV_ARGS(&pServiceProvider));
pServiceProvider->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&pShellBrowser));
pServiceProvider->Release();

IDispatch::QueryInterfaceでIServiceProviderを取得できれば、 IServiceProvider::QueryServiceにSID_STopLevelBrowserを指定することで、 IShellBrowserを取得することができます。 IShellBrowserを取得したら、IServiceProviderやIDispatchは開放しても構いません。

ShellBrowserはフォルダビューを管理することが目的ですから、 当然ながらIShellBrowserにはそのようなメソッドが含まれています。 たとえば、QueryActiveShellViewを呼び出せば、 現在表示されているフォルダビューを表すIShellViewを取得することができます。 また、BrowseObjectを呼び出せば、現在表示されているフォルダを切り替えることができます。

HRESULT IShellBrowser::BrowseObject(
  PCUIDLIST_RELATIVE pidl,
  UINT wFlags
);

pidlは、新しく表示するフォルダのPIDLを指定します。 wFlagsは、定義されている定数を指定します。

今回のプログラムは、デスクトップのフォルダウインドウを表示します。 メッセージボックスに応答するとドキュメントフォルダのウインドウに切り替わります。

#include <windows.h>
#include <shlobj.h>
#include <shlwapi.h>

#pragma comment (lib, "shlwapi.lib")

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT          hr;
	HWND             hwnd;
	VARIANT          var;
	VARIANT          varRoot;
	IDispatch        *pDispatch;
	IShellBrowser    *pShellBrowser;
	IShellWindows    *pShellWindows;
	PIDLIST_ABSOLUTE pidlPersonal;
	PIDLIST_ABSOLUTE pidlDesktop;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pShellWindows));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	SHGetSpecialFolderLocation(NULL, CSIDL_DESKTOP, &pidlDesktop);
	SHOpenFolderAndSelectItems(pidlDesktop, 0, NULL, 0);

	var.vt = VT_BYREF | VT_VARIANT;
	var.pvarVal = (VARIANT *)pidlDesktop;
	VariantInit(&varRoot);

	if (pShellWindows->FindWindowSW(&var, &varRoot, SWC_BROWSER, (LONG *)&hwnd, SWFO_NEEDDISPATCH, &pDispatch) != S_OK) {
		CoTaskMemFree(pidlDesktop);
		pShellWindows->Release();
		CoUninitialize();
		return 0;
	}

	if (IUnknown_QueryService(pDispatch, SID_STopLevelBrowser, IID_PPV_ARGS(&pShellBrowser)) != S_OK) {
		CoTaskMemFree(pidlDesktop);
		pDispatch->Release();
		pShellWindows->Release();
		CoUninitialize();
		return 0;
	}

	MessageBox(NULL, TEXT("フォルダを切り替えます。"), TEXT("OK"), MB_OK);

	SHGetSpecialFolderLocation(NULL, CSIDL_PERSONAL, &pidlPersonal);
	pShellBrowser->BrowseObject(pidlPersonal, SBSP_SAMEBROWSER | SBSP_ABSOLUTE);

	CoTaskMemFree(pidlDesktop);
	CoTaskMemFree(pidlPersonal);
	pDispatch->Release();
	pShellBrowser->Release();
	pShellWindows->Release();
	CoUninitialize();

	return 0;
}

まず、IShellWindowsを取得するためにCLSID_ShellWindowsを指定してCoCreateInstanceを呼び出します。 次に、SHGetSpecialFolderLocationでデスクトップのPIDLを取得し、 SHOpenFolderAndSelectItemsでデスクトップのフォルダを表示します。 フォルダを表示することでIShellBrowserを実装するオブジェクトを作成されるわけですから、この処理は必要になります。 IShellWindows::FindWindowSWの呼び出しは、次のようになっています。

if (pShellWindows->FindWindowSW(&var, &varRoot, SWC_DESKTOP, (LONG *)&hwnd, SWFO_NEEDDISPATCH, &pDispatch) != S_OK) {
	CoTaskMemFree(pidlDesktop);
	pShellWindows->Release();
	CoUninitialize();
	return 0;
}

FindWindowSWは、完全PIDLから関連するウインドウとIDispatchを取得するようになっているため、 第1引数のVARIANT構造体は完全PIDLを格納している必要があります。 具体的には、vtにVT_BYREFとVT_VARIANTを指定し、pvarValにPIDLを指定します。 第2引数のVARIANT構造体は、VariantInitで初期化しておくだけで構いません。 第3引数は検索するウインドウのタイプであり、SHOpenFolderAndSelectItemsで表示したウインドウを検索する場合はSWC_BROWSERを指定します。 第4引数は見つかったウインドウのハンドルが返ります。 第5引数はSWFO_NEEDDISPATCHを指定することになります。 これにより、第6引数にIDispatchが返ることになります。

既に述べたように、FindWindowSWで取得したIDispatchからIShellBrowserを取得するためには、 IServiceProviderを取得してQueryServiceを呼び出す必要があります。 IUnknown_QueryServiceを呼び出せば、この2つの処理をまとめて行うことができます。

if (IUnknown_QueryService(pDispatch, SID_STopLevelBrowser, IID_PPV_ARGS(&pShellBrowser)) != S_OK) {
	CoTaskMemFree(pidlDesktop);
	pDispatch->Release();
	pShellWindows->Release();
	CoUninitialize();
	return 0;
}

IUnknown_QueryServiceは、第1引数のインターフェースからIServiceProviderを取得し、QueryServiceを呼び出します。 つまり、アプリケーションはIUnknown_QueryServiceを呼び出すことによって、 IServiceProviderを明示的に取得する必要がなくなることになります。 IUnknown_QueryServiceの第2引数から第4引数は、QueryServiceの第1引数から第3引数に相当します。

フォルダを切り替える旨のメッセージボックスが表示されたら、それに応答します。 そうすると、IShellWindows::BrowseObjectによって、表示するフォルダが変更されます。

SHGetSpecialFolderLocation(NULL, CSIDL_PERSONAL, &pidlPersonal);
pShellBrowser->BrowseObject(pidlPersonal, SBSP_SAMEBROWSER | SBSP_ABSOLUTE);

BrowseObjectの第1引数には、新しく表示するフォルダのPIDLを指定します。 SHGetSpecialFolderLocationにCSIDL_PERSONALを指定しているため、 ドキュメントフォルダが表示されることになります。 第2引数のSBSP_ABSOLUTEは、第1引数が完全PIDLである場合に指定します。 SBSP_SAMEBROWSERは、現在のウインドウで新しいフォルダを表示することを意味しますが、 SBSP_NEWBROWSERを指定すると新しいウインドウでフォルダを表示することができます。

IShellWindows::FindWindowSWの第3引数について少し補足します。 今回は、SHOpenFolderAndSelectItemsで表示したデスクトップを検索するためSWC_BROWSERを指定していますが、 Windowsキー + Eキーでデスクトップを表示した場合は、SWC_BROWSERではなくSWC_EXPLORERを指定することになります。 また、Windows VistaからはSWC_DESKTOPを指定することが可能になっており、 この定数を指定する場合は予めフォルダを表示しておく必要はありません。 さらに、第1引数のVARIANT構造体はVariantInitで初期化するだけで構いません。 SWC_DESKTOPを指定した場合は、取得できるウインドウハンドルがGetShellWindowの戻り値と一致し、 BrowseObjectでは常に新しいフォルダが作成されることになります。

IShellWindowsについて

アプリケーション内で作成したオブジェクトがIDispatchを実装し、さらにウインドウハンドルを持っているような場合は、 これらをShellWindowsと呼ばれるオブジェクトに登録することができます。 これはいわば、自分が現在実行されていることをShellWindowsを通じて示すということであり、 他のアプリケーションはIShellWindowsを通じて、登録されたオブジェクトやウインドウの情報を取得できるようになります。 次のコードは、登録された各種ウインドウのタイトルを表示することで、 現在どれだけのウインドウが登録されているかを調べています。

#include <windows.h>
#include <shlobj.h>
#include <shlwapi.h>

#pragma comment (lib, "shlwapi.lib")

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	LONG          i, lCount;
	TCHAR         szBuf[256];
	HRESULT       hr;
	HWND          hwnd;
	VARIANT       var;
	IDispatch     *pDispatch;
	IShellBrowser *pShellBrowser;
	IShellWindows *pShellWindows;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pShellWindows));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pShellWindows->get_Count(&lCount);

	for (i = 0; i < lCount; i++) {
		var.vt = VT_I4;
		var.lVal = i;
		if (pShellWindows->Item(var, &pDispatch) == S_OK) {
			if (IUnknown_QueryService(pDispatch, SID_STopLevelBrowser, IID_PPV_ARGS(&pShellBrowser)) == S_OK) {
				if (IUnknown_GetWindow(pShellBrowser, &hwnd) == S_OK) {
					GetWindowText(hwnd, szBuf, sizeof(szBuf) / sizeof(TCHAR));
					MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
				}
				pShellBrowser->Release();
			}
			pDispatch->Release();
		}
	}

	pShellWindows->Release();
	CoUninitialize();

	return 0;
}

get_Countは登録されたウインドウの数を返すため、この数だけループを行うことになります。 Itemの第1引数はVARIANT構造体であり、取得したいウインドウのインデックスを格納している必要があります。 lValメンバにインデックスを指定するため、vtメンバにはVT_I4を指定することになります。 Itemが成功してIDispatchを取得したら、IUnknown_QueryServiceでIShellBrowserを取得します。 通常、登録されたウインドウはこれをサポートしているはずです。 IShellBrowserを取得したら、ウインドウハンドルを取得するためにIUnknown_GetWindowを呼び出します。 本来ならば、QueryInterfaceでIOleWindowを取得してGetWindowを呼び出すことになるのですが、 IUnknown_GetWindowを呼び出せばそれらをまとめて行うことができます。

さて、表示されるウインドウのタイトルですが、これは現在作成されているフォルダとInternet Explorerのタブを反映しています。 1つのフォルダが作成されていれば、そのフォルダのタイトルが表示されることになりますし、 1つのInternet Explorerウインドウで3つのタブを開いていれば、 その3つのタブのタイトルが表示されることになります。 タイトルが表示されるということは、そのウインドウのIDispatchを取得できたことを意味するため、 タイトルの取得に限らずその他の操作も実行できることになります。 たとえば、Internet ExplorerはIWebBrowserを実装していますから、これをQueryInterfaceで取得するのもよいでしょう。



戻る