EternalWindows
シェル名前空間 / フォルダの列挙

シェル名前空間を利用した開発を行うにあたって、 それがどういった状況下で必要になるかを考えることは重要な意味を持ちます。 たとえば、今回行うことになるフォルダの列挙という操作は、 ファイルシステム関数であるFindFirstFileで実現可能になっているため、 無理にシェル名前空間の関数を使用する必要はないと言えます。 しかし、こうしたファイルシステム関数では前節で述べた仮想フォルダを列挙することができませんし、 ファイルに関連付けられたコンテキストメニューを取得することもできませんから、 ファイラーとして振る舞うにあたっては機能が不十分ということになります。 こうしたことから、ファイラーのようなアプリケーションを開発する場合は、 シェル名前空間の機能を使用するほうがよいと言えます。

シェル名前空間では、1つのフォルダ(仮想フォルダ含む)をオブジェクトとして扱います。 このオブジェクトはIShellFolderというCOMインターフェースで表すことができ、 フォルダ内のアイテムを扱うメソッドが含まれています。 シェル名前空間のルートであるデスクトップのIShellFolderを取得するには、 SHGetDesktopFolderを呼び出します。

HRESULT SHGetDesktopFolder(
  IShellFolder **ppshf
);

ppshfは、IShellFolderへのポインタを受け取る変数のアドレスを指定します。

IShellFolderは1つのフォルダを表しますから、その下に存在するファイルやフォルダを列挙するためのメソッドを用意しています。 次に示すEnumObjectsを呼び出せば、列挙に使用するIEnumIDListを取得することができます。

HRESULT IShellFolder::EnumObjects(
  HWND hwndOwner,
  SHCONTF grfFlags,
  IEnumIDList **ppenumIDList
);

hwndOwnerは、表示されるダイアログの親ウインドウとするウインドウハンドルを指定します。 通常、列挙に伴ってダイアログが表示されることはありませんが、 管理者でないとアクセスできないフォルダ内のファイルを列挙しようとした場合は、 ダイアログが表示されることがあります。 このような際にNULLが指定されていた場合は、ダイアログは表示されずメソッドは失敗することになります。 grfFlagsは、どのようにアイテムを列挙するかを表す定数を指定します。 SHCONTF_FOLDERSを指定するとフォルダが列挙され、 SHCONTF_NONFOLDERSを指定すると非フォルダが列挙されることになります。 ppenumIDListは、IEnumIDListへのポインタを受け取る変数のアドレスを指定します。

IShellFolderとIEnumIDListを取得すれば、それだけでアイテムを列挙できるようになります。 次のコードは、デスクトップに存在するフォルダを列挙する例を示しています。

#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)
{
	TCHAR         szDisplayName[256];
	STRRET        strret;
	PITEMID_CHILD pidlChild;
	IShellFolder  *pDesktopFolder;
	IEnumIDList   *pEnumIdList;

	CoInitialize(NULL);
	
	SHGetDesktopFolder(&pDesktopFolder);

	if (pDesktopFolder->EnumObjects(NULL, SHCONTF_FOLDERS, &pEnumIdList) != S_OK) {
		pDesktopFolder->Release();
		return 0;
	}

	while (pEnumIdList->Next(1, &pidlChild, NULL) == S_OK) {
		pDesktopFolder->GetDisplayNameOf(pidlChild, SHGDN_NORMAL, &strret);
		StrRetToBuf(&strret, pidlChild, szDisplayName, sizeof(szDisplayName) / sizeof(TCHAR));
		MessageBox(NULL, szDisplayName, TEXT("OK"), MB_OK);
		CoTaskMemFree(pidlChild);
	}

	pEnumIdList->Release();
	pDesktopFolder->Release();
	
	CoUninitialize();

	return 0;
}

COMインターフェースを使用する関係上、プログラムの最初と最後にCoInitializeとCoUninitializeを呼び出します。 SHGetDesktopFolderでIShellFolderを取得したら、EnumObjectsを呼び出すことによってIEnumIDListを取得します。 第2引数にSHCONTF_FOLDERSを指定していることから、フォルダを列挙することになります。 関数が失敗したかどうかは戻り値がS_OKでないかで確認できますが、 代わりの方法としてFAILEDマクロは使用しないでください。 一部のアイテムでは、関数が失敗した際に0より大きい値(S_FALSEなど)を返すことがあるので、 0より低いかで判断するFAILEDマクロを使用するのは危険です。 IEnumIDListを取得したらNextを呼び出すことで、 フォルダ内のアイテムのPIDLを第2引数から取得することができます。 1つのPIDLを順に取得する場合は、第1引数は1で構いません。 PIDLを取得したら、IShellFolderのGetDisplayNameOfを呼び出してSTRRET構造体を取得することができます。 そして、この構造体をStrRetToBufに指定すれば、アイテムの名前を取得することができます。

PIDLには、完全PIDLと相対PIDLの2種類が存在します。 前者はデスクトップをルートとして含んだPIDLであり、 これ1つでアイテムを一意に識別することができます。 一方、後者は特定の親フォルダの下に存在するアイテムを識別するPIDLであり、 たとえば先のIEnumIDList::Nextで取得できるPIDLが該当します。 このようなPIDLからアイテムの名前を取得するには親フォルダに問い合わすしかないため、 先のように親フォルダのGetDisplayNameOfを呼び出すことになります。 PIDLに種類があるという関係上、LPITEMIDLIST型でPIDLを識別することはあまりありません。 理由は、どの種類のPIDLを識別しているのかが一目で分からないからです。 基本的には、完全PIDLをPIDLIST_ABSOLUTEで識別し、 相対PIDLをPIDLIST_RELATIVEかPITEMID_CHILDで識別します。

現在のフォルダの下に存在するフォルダ内のアイテムを列挙したい場合は、 そのフォルダに対してバインドを行うことになります。 バインドが成功すれば、そのフォルダのIShellFolderを取得することができます。

HRESULT IShellFolder::BindToObject(
  PCUIDLIST_RELATIVE pidl,
  IBindCtx *pbc,
  REFIID riid,
  void **ppvOut
);

pidlは、バインドするフォルダのPIDLを指定します。 pbcは、NULLで問題ありません。 riidは、取得したいインターフェースのIIDを指定します。 通常は、IID_IShellFolderを指定します。 ppvOutは、インターフェースを受け取る変数のアドレスを指定します。

今回のプログラムは、デスクトップ以下に存在するフォルダをツリービューで表示します。

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

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

HWND             g_hwndTreeView = NULL;
IShellFolder     *g_pDesktopFolder = NULL;
PIDLIST_ABSOLUTE g_pidlDesktop = NULL;

void EnumItem(PIDLIST_ABSOLUTE pidlAbsolute, HWND hwnd, HTREEITEM hitem);
void InsertTreeItem(PIDLIST_ABSOLUTE pidlAbsolute, HTREEITEM hitemParent, LPTSTR lpszName, SFGAOF attributes);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND hwndTreeView = NULL;
	
	switch (uMsg) {
		
	case WM_CREATE: {
		INITCOMMONCONTROLSEX ic;

		CoInitialize(NULL);
		
		ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
		ic.dwICC  = ICC_TREEVIEW_CLASSES;
		InitCommonControlsEx(&ic);

		hwndTreeView = CreateWindowEx(0, WC_TREEVIEW, TEXT(""), WS_CHILD | WS_VISIBLE | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT, 0, 0, 0, 0, hwnd, (HMENU)2, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		SHGetDesktopFolder(&g_pDesktopFolder);
		SHGetSpecialFolderLocation(hwnd, CSIDL_DESKTOP, &g_pidlDesktop);
		g_hwndTreeView = hwndTreeView;

		InsertTreeItem(g_pidlDesktop, TVI_ROOT, TEXT("デスクトップ"), SFGAO_HASSUBFOLDER);

		return 0;
	}

	case WM_NOTIFY: {
		LPNMHDR lpNmhdr = (LPNMHDR)lParam;
		if (lpNmhdr->code == TVN_ITEMEXPANDING) {
			LPTVITEM lp = &((LPNMTREEVIEW)lParam)->itemNew;
			if (!(lp->state & TVIS_EXPANDEDONCE))
				EnumItem((PIDLIST_ABSOLUTE)lp->lParam, hwnd, lp->hItem);
		}
		else if (lpNmhdr->code == TVN_DELETEITEM) {
			LPTVITEM lp = &((LPNMTREEVIEW)lParam)->itemOld;
			CoTaskMemFree((LPVOID)lp->lParam);
		}
		else
			;
		return 0;
	}
	
	case WM_SIZE:
		MoveWindow(hwndTreeView, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		if (g_pDesktopFolder != NULL)
			g_pDesktopFolder->Release();
		
		TreeView_DeleteAllItems(hwndTreeView);
	
		CoUninitialize();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void EnumItem(PIDLIST_ABSOLUTE pidlAbsolute, HWND hwnd, HTREEITEM hitem)
{
	BOOL             bBind;
	TCHAR            szDisplayName[256];
	SFGAOF           attributes = 0;
	STRRET           strret;
	PIDLIST_ABSOLUTE pidlNew;
	PITEMID_CHILD    pidlChild;
	IShellFolder     *pShellFolder;
	IEnumIDList      *pEnumIdList;

	if (g_pDesktopFolder->CompareIDs(0, pidlAbsolute, g_pidlDesktop) == 0) {
		pShellFolder = g_pDesktopFolder;
		bBind = FALSE;
	}
	else {
		g_pDesktopFolder->BindToObject(pidlAbsolute, NULL, IID_PPV_ARGS(&pShellFolder));
		bBind = TRUE;
	}

	if (pShellFolder->EnumObjects(hwnd, SHCONTF_FOLDERS, &pEnumIdList) != S_OK) {
		if (bBind)
			pShellFolder->Release();
		return;
	}

	while (pEnumIdList->Next(1, &pidlChild, NULL) == S_OK) {
		pShellFolder->GetDisplayNameOf(pidlChild, SHGDN_NORMAL, &strret);
		StrRetToBuf(&strret, pidlChild, szDisplayName, sizeof(szDisplayName) / sizeof(TCHAR));
		
		pidlNew = ILCombine(pidlAbsolute, pidlChild);
		
		attributes = SFGAO_HASSUBFOLDER;
		pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild, &attributes);
		InsertTreeItem(pidlNew, hitem, szDisplayName, attributes);
		
		CoTaskMemFree(pidlChild);
	}

	if (bBind)
		pShellFolder->Release();

	pEnumIdList->Release();
}

void InsertTreeItem(PIDLIST_ABSOLUTE pidlAbsolute, HTREEITEM hitemParent, LPTSTR lpszName, SFGAOF attributes)
{
	TVINSERTSTRUCT is;

	is.hParent      = hitemParent;
	is.item.mask    = TVIF_TEXT | TVIF_PARAM;
	is.item.pszText = lpszName;
	is.item.lParam  = (LPARAM)pidlAbsolute;

	if (attributes & SFGAO_HASSUBFOLDER) {
		is.item.mask     |= TVIF_CHILDREN;
		is.item.cChildren = 1;
	}
	
	TreeView_InsertItem(g_hwndTreeView, &is);
}

アイテムをツリービューによって列挙するのは、それほど容易なことではありません。 全てのアイテムを最初からツリービューに追加しようとすると、ツリービューの表示に時間が掛かってしまいますから、 フォルダが展開されると同時にアイテムを動的に追加する方法を考える必要があります。 また、ツリービューに追加されたアイテムには、そのアイテムを識別するための完全なPIDLを関連付けておく必要があります。 これを怠ると、アイテムが選択された際にそのアイテムの位置を取得することができず、問題といえます。 以上の事を踏まえて、WM_CREATEのコードから確認します。

case WM_CREATE: {
	INITCOMMONCONTROLSEX ic;

	CoInitialize(NULL);
	
	ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
	ic.dwICC  = ICC_TREEVIEW_CLASSES;
	InitCommonControlsEx(&ic);

	hwndTreeView = CreateWindowEx(0, WC_TREEVIEW, TEXT(""), WS_CHILD | WS_VISIBLE | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT, 0, 0, 0, 0, hwnd, (HMENU)2, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
	
	SHGetDesktopFolder(&g_pDesktopFolder);
	SHGetSpecialFolderLocation(hwnd, CSIDL_DESKTOP, &g_pidlDesktop);
	g_hwndTreeView = hwndTreeView;

	InsertTreeItem(g_pidlDesktop, TVI_ROOT, TEXT("デスクトップ"), SFGAO_HASSUBFOLDER);

	return 0;
}

CreateWindowExではWC_TREEVIEWを指定してツリービューを作成していますが、 このためにはInitCommonControlsExでICC_TREEVIEW_CLASSESを指定しておくべきです。 続いて、SHGetDesktopFolderでデスクトップのIShellFolderを取得し、 SHGetSpecialFolderLocationでデスクトップのPIDLを取得しています。 これらとツリービューのハンドルは他の関数でも必要になるため、グローバル変数に保存しています。 InsertTreeItemは、第2引数のアイテムの下に新しいアイテムを追加する自作関数です。 第3引数がそのアイテムの名前であり、第1引数のPIDLがアイテムに関連付けられることになります。

void InsertTreeItem(PIDLIST_ABSOLUTE pidlAbsolute, HTREEITEM hitemParent, LPTSTR lpszName, SFGAOF attributes)
{
	TVINSERTSTRUCT is;

	is.hParent      = hitemParent;
	is.item.mask    = TVIF_TEXT | TVIF_PARAM;
	is.item.pszText = lpszName;
	is.item.lParam  = (LPARAM)pidlAbsolute;

	if (attributes & SFGAO_HASSUBFOLDER) {
		is.item.mask     |= TVIF_CHILDREN;
		is.item.cChildren = 1;
	}
	
	TreeView_InsertItem(g_hwndTreeView, &is);
}

ツリービューにアイテムを追加するには、TreeView_InsertItemを呼び出します。 TVINSERTSTRUCT構造体のhParentには、追加するアイテムの親アイテムのハンドルを指定し、 item.maskには初期化するメンバを表す定数を指定します。 TVIF_TEXTとTVIF_PARAMを指定していることから、 item.pszTextとitem.lParamを初期化することになります。 lParamには独自のデータを指定することからできますから、PIDLを指定するようにしています。 attributesにSFGAO_HASSUBFOLDERが含まれる場合は、 item.maskにTVIF_CHILDRENを指定することによって、cChildrenメンバを初期化できるようにします。 ここに1以上の値を指定すれば、実際にそのアイテムの下にアイテムが存在しない場合でも、 アイテムを展開するための印が表示されることになります。

WM_CREATEで実行したInsertTreeItemにより、デスクトップという名前を持ったアイテムが1つ追加されます。 この時点ではまだ、下位に存在すべきアイテムが追加されていないため、 アイテムが展開されたタイミングを検出することにより、 デスクトップ以下のファイルやフォルダを追加する必要があります。 アイテムが展開された場合は、通知コードがTVN_ITEMEXPANDINGであるWM_NOTIFYが送られます。

if (lpNmhdr->code == TVN_ITEMEXPANDING) {
	LPTVITEM lp = &((LPNMTREEVIEW)lParam)->itemNew;
	if (!(lp->state & TVIS_EXPANDEDONCE))
		EnumItem((PIDLIST_ABSOLUTE)lp->lParam, hwnd, lp->hItem);
}

TVN_ITEMEXPANDINGの場合は、lParamをNMTREEVIEW構造体で表すことができます。 この構造体のstateにTVIS_EXPANDEDONCEが含まれない場合は、 アイテムがまだ一度も展開されていないことを意味するため、 このときにEnumItemという自作関数でアイテムを追加することになります。 第1引数は、展開しようとするアイテムのPIDLですが、 これはlParamから関連付けておいたため、そのままlParamを指定すれば問題ありません。 第2引数はウインドウハンドルであり、第3引数はツリービュー上におけるアイテムのハンドルを指定します。

EnumItemの重要な処理を順に見ていきます。

if (g_pDesktopFolder->CompareIDs(0, pidlAbsolute, g_pidlDesktop) == 0) {
	pShellFolder = g_pDesktopFolder;
	bBind = FALSE;
}
else {
	g_pDesktopFolder->BindToObject(pidlAbsolute, NULL, IID_PPV_ARGS(&pShellFolder));
	bBind = TRUE;
}

このコードは、pidlAbsoluteで表されるアイテムのIShellFolderを取得する部分です。 g_pDesktopFolderはデスクトップのフォルダを表しており、 これのBindToObjectを呼び出せば目的のアイテムのIShellFolderを取得することができます。 ただし、pidlAbsoluteがデスクトップを表している場合は、 自分自身にバインドを行うことになりエラーが発生するため、 pidlAbsoluteがデスクトップのPIDLと一致するかを調べるためにCompareIDsを呼び出しています。 この結果が0である場合は、pShellFolderがg_pDesktopFolderを指すようにし、 バインドの有無を示すbBindにFALSEを格納します。 bBindがFALSEの場合は、pShellFolder->Releaseを呼び出すことはありません。

if (pShellFolder->EnumObjects(hwnd, SHCONTF_FOLDERS, &pEnumIdList) != S_OK) {
	if (bBind)
		pShellFolder->Release();
	return;
}

このコードは、IShellFolderからIEnumIDListを取得する部分です。 第2引数にSHCONTF_FOLDERSを指定していることからフォルダを列挙することになります。

while (pEnumIdList->Next(1, &pidlChild, NULL) == S_OK) {
	pShellFolder->GetDisplayNameOf(pidlChild, SHGDN_NORMAL, &strret);
	StrRetToBuf(&strret, pidlChild, szDisplayName, sizeof(szDisplayName) / sizeof(TCHAR));
	
	pidlNew = ILCombine(pidlAbsolute, pidlChild);
	
	attributes = SFGAO_HASSUBFOLDER;
	pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild, &attributes);
	InsertTreeItem(pidlNew, hitem, szDisplayName, attributes);
	
	CoTaskMemFree(pidlChild);
}

このコードは、列挙したアイテムをInsertTreeItemでツリービューに追加する部分です。 この関数の第3引数はアイテムの名前が必要でしたが、 これはGetDisplayNameOfとStrRetToBufで取得することができます。 InsertTreeItemの第1引数は完全なPIDLを指定しますが、 pEnumIdList->Nextで取得できるpidlは、子アイテムとしての情報しか含まれていません。 よって、親であるpidlAbsoluteと子であるpidlをILCombineで連結し、 この結果であるpidlNewを指定することになります。 InsertTreeItemにはアイテムの属性も指定しなければなりませんが、 これはGetAttributesOfで取得することができます。 今回は第2引数のアイテムが下位にフォルダを持つかを調べるために、 attributesにSFGAO_HASSUBFOLDERを指定しています。 GetAttributesOfが制御を返しても、依然としてSFGAO_HASSUBFOLDERが格納されている場合は、 アイテムはフォルダを持つということが分かります。

アイテムに関連付けたPIDLは、アイテムが不要になった時点で削除するべきといえます。 今回のプログラムは、WM_DESTROYでTreeView_DeleteAllItemsを呼び出しているため、 アイテムの数だけTVN_DELETEITEMが送られることになります。

else if (lpNmhdr->code == TVN_DELETEITEM) {
	LPTVITEM lp = &((LPNMTREEVIEW)lParam)->itemOld;
	CoTaskMemFree((LPVOID)lp->lParam);
}

lParamにはNMTREEVIEW構造体のアドレスが格納されており、関連付けられたPIDLはitemOld.lParamに格納されています。 よって、これをCoTaskMemFreeで開放します。 デスクトップのPIDLであるg_pidlDesktopも上記の処理で削除されることになります。

IPersistFolder2について

IShellFolderは1つのフォルダを表すことができますが、 そのフォルダのPIDLを取得するためのメソッドは含んでいません。 ただし、通常はIShellFolderからIPersistFolder2を取得できるようになっているため、 IPersistFolder2::GetCurFolderを呼び出すことでPIDLを取得することができます。

IPersistFolder2 *pPersistFolder2;

g_pDesktopFolder->QueryInterface(IID_PPV_ARGS(&pPersistFolder2));
pPersistFolder2->GetCurFolder(&g_pidlDesktop);
pPersistFolder2->Release();

g_pDesktopFolderはデスクトップを表すIShellFolderです。 今回のプログラムではSHGetSpecialFolderLocationでデスクトップのPIDLを取得していましたが、 上記のようなコードを記述することでもPIDLを取得することができます。

IPersistFolder2はIPersistFolderを継承していますが、IPersistFolderのメソッドは呼び出すことは基本的にないでしょう。 IPersistFolder::Initializeは、シェルが仮想フォルダに対してPIDLを与えるために使用されるため、 シェル拡張で仮想フォルダを実装する場合のみ意味を持ちます。



戻る