EternalWindows
シェル名前空間 / 簡単なファイラー

前節では、シェル名前空間における階層をツリービューで表示しましたが、 ツリービューの各アイテムにはアイコンが表示されていませんでした。 こうしたアイコンはアイテムの属性によって様々に変化するため、 IShellFolder::GetAttributesOfで属性を取得することになります。

HRESULT IShellFolder::GetAttributesOf(
  UINT cidl,
  PCUITEMID_CHILD_ARRAY apidl,
  SFGAOF *rgfInOut
);

cidlは、属性を取得するアイテムの数を指定します。 apidlは、ITEMIDLIST構造体の配列を指定します。 cidlが1である場合は、構造体のアドレスを指定するだけで構いません。 rgfInOutは、取得したい属性を格納した変数のアドレスを指定します。 メソッドが成功し、依然としてその定数が変数に格納されている場合は、 アイテムがその属性を持っていることになります。

GetAttributesOfの第3引数に指定可能な定数の一部を示します。

定数 意味
SFGAO_CANCOPY アイテムはコピーすることができる。
SFGAO_CANMOVE アイテムは移動することができる。
SFGAO_CANLINK アイテムのショートカットを作成することができる。
SFGAO_CANRENAME アイテムは名前を変更することができる。
SFGAO_DROPTARGET アイテムはドロップターゲットである。
SFGAO_GHOSTED アイテムは薄いアイコンで表示されている。
SFGAO_LINK アイテムはショートカットである。
SFGAO_SHARE アイテムは共有することができる。
SFGAO_HIDDEN アイテムは隠しアイテムである。
SFGAO_FOLDER アイテムはフォルダである。
SFGAO_FILESYSTEM アイテムはファイルシステムの一部である。
SFGAO_HASSUBFOLDER アイテムは下位のアイテムとしてフォルダを持っている。

たとえば、第3引数にSFGAO_HASSUBFOLDERを指定し、 関数が成功しても第3引数にSFGAO_HASSUBFOLDERが格納されている場合は、 アイテムはフォルダを持つことを意味します。

アイテムに関連するアイコンやアイテムの種類を取得したい場合は、SHGetFileInfoを呼び出します。

DWORD_PTR SHGetFileInfo(
  LPCTSTR pszPath,
  DWORD dwfileAttributess,
  SHFILEINFO *psfi,
  UINT cbFileInfo,
  UINT uFlags
);

pszPathは、アイテム(ファイルやフォルダ)のフルパスを指定します。 uFlagsにSHGFI_PIDLを指定した場合は、完全なPIDLを指定することもできます。 dwfileAttributessは、FILE_ATTRIBUTE_NORMALなどの定数を指定することができますが、通常は0で問題ありません。 psfiは、SHFILEINFO構造体のアドレスを指定します。 cbFileInfoは、psfiのサイズを指定します。 uFlagsは、取得する情報の種類を表す定数を指定します。

SHGetFileInfoの第5引数に指定可能な定数の一部を示します。

定数 意味
SHGFI_DISPLAYNAME ファイルの表示名をSHFILEINFO構造体のszDisplayNameに格納する。
SHGFI_ICON アイコンのハンドルをhIconに格納し、アイコンのインデックスをiIconに格納する。hIconは、DestroyIconで開放する。
SHGFI_OPENICON 開かれたアイコンのインデックスをiIconに格納する。
SHGFI_PIDL SHGetFileInfoの第1引数に完全なPIDLを指定する。
SHGFI_SYSICONINDEX システムイメージリスト上におけるアイコンのインデックスをiIconに格納する。 戻り値はシステムイメージリストのハンドルになる。
SHGFI_SMALLICON 小さいアイコンのインデックスをiIconに格納する。
SHGFI_TYPENAME ファイルの種類をszTypeNameに格納する。

ツリービューやリストビューを使用する場合はアイコンのインデックスが必要になるため、 アイコンのハンドルを取得するSHGFI_ICONを指定することは基本的にありません。

今回のプログラムは、アイコン付きのツリービューを表示します。 また、フォルダの中に存在するファイルをリストビューに列挙します。

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

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

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

void ResetListViewColumn(IShellFolder *pShellFolder);
void EnumItem(PIDLIST_ABSOLUTE pidlAbsolute, HWND hwnd, BOOL bInsertList, HTREEITEM hitem);
void InsertListItem(PIDLIST_ABSOLUTE pidlAbsolute, IShellFolder *pShellFolder, PITEMID_CHILD pidlChild, SFGAOF attributes, int i);
void InsertTreeItem(PIDLIST_ABSOLUTE pidlAbsolute, HTREEITEM hitemParent, LPTSTR lpszName, ULONG uAttributes);
int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
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 hwndListView = NULL;
	static HWND hwndTreeView = NULL;
	
	switch (uMsg) {
		
	case WM_CREATE: {
		HIMAGELIST           himl;
		SHFILEINFO           fileInfo;
		INITCOMMONCONTROLSEX ic;

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

		hwndListView = CreateWindowEx(0, WC_LISTVIEW, TEXT(""), WS_CHILD | WS_VISIBLE | WS_BORDER | LVS_REPORT, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		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);

		himl = (HIMAGELIST)SHGetFileInfo(TEXT("C:\\"), 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_SMALLICON);
		ListView_SetImageList(hwndListView, himl, LVSIL_SMALL);
		TreeView_SetImageList(hwndTreeView, himl, TVSIL_NORMAL);
		
		SHGetDesktopFolder(&g_pDesktopFolder);
		SHGetSpecialFolderLocation(hwnd, CSIDL_DESKTOP, &g_pidlDesktop);
		g_hwndListView = hwndListView;
		g_hwndTreeView = hwndTreeView;
		
		ResetListViewColumn(g_pDesktopFolder);
		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, FALSE, lp->hItem);
		}
		else if (lpNmhdr->code == TVN_SELCHANGED) {
			LPTVITEM lp = &((LPNMTREEVIEW)lParam)->itemNew;
			EnumItem((PIDLIST_ABSOLUTE)lp->lParam, hwnd, TRUE, NULL);
		}
		else if (lpNmhdr->code == TVN_DELETEITEM) {
			LPTVITEM lp = &((LPNMTREEVIEW)lParam)->itemOld;
			CoTaskMemFree((LPVOID)lp->lParam);
		}
		else if (lpNmhdr->code == LVN_DELETEITEM) {
			LVITEM item;
			item.mask  = LVIF_PARAM;
			item.iItem = ((LPNMLISTVIEW)lParam)->iItem;
			ListView_GetItem(hwndListView, &item);
			CoTaskMemFree((LPVOID)item.lParam);
		}
		else
			;
		return 0;
	}
	
	case WM_SIZE:{
		int nTreeViewWidth = LOWORD(lParam) / 3;
		int nListViewWidth = LOWORD(lParam) - nTreeViewWidth;
		MoveWindow(hwndTreeView, 0, 0, nTreeViewWidth, HIWORD(lParam), TRUE);
		MoveWindow(hwndListView, nTreeViewWidth, 0, nListViewWidth, HIWORD(lParam), TRUE);
		return 0;
	}

	case WM_DESTROY:
		if (g_pDesktopFolder != NULL)
			g_pDesktopFolder->Release();

		ListView_DeleteAllItems(hwndListView);
		TreeView_DeleteAllItems(hwndTreeView);

		CoUninitialize();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

void ResetListViewColumn(IShellFolder *pShellFolder)
{
	int           i, nCol;
	TCHAR         szBuf[256];
	LVCOLUMN      column;
	SHELLDETAILS  details;
	IShellFolder2 *pShellFolder2;

	nCol = Header_GetItemCount(ListView_GetHeader(g_hwndListView));
	for (i = 0; i < nCol; i++)
		ListView_DeleteColumn(g_hwndListView, 0);

	if (pShellFolder->QueryInterface(IID_PPV_ARGS(&pShellFolder2)) != S_OK)
		return;
	
	for (i = 0; i < 4; i++) {
		if (pShellFolder2->GetDetailsOf(NULL, i, &details) != S_OK)
			break;

		StrRetToBuf(&details.str, NULL, szBuf, sizeof(szBuf) / sizeof(TCHAR));

		column.mask    = LVCF_WIDTH | LVCF_TEXT;
		column.cx      = 150;
		column.pszText = szBuf;
		ListView_InsertColumn(g_hwndListView, i, &column);
	}

	pShellFolder2->Release();
}

void EnumItem(PIDLIST_ABSOLUTE pidlAbsolute, HWND hwnd, BOOL bInsertList, HTREEITEM hitem)
{
	int              i = 0;
	BOOL             bBind;
	TCHAR            szDisplayName[256];
	SFGAOF           attributes = 0;
	SHCONTF          flag;
	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 (bInsertList) {
		ListView_DeleteAllItems(g_hwndListView);
		ResetListViewColumn(pShellFolder);
	}

	flag = bInsertList ? SHCONTF_FOLDERS | SHCONTF_NONFOLDERS : SHCONTF_FOLDERS;
	if (pShellFolder->EnumObjects(hwnd, flag | SHCONTF_INCLUDEHIDDEN, &pEnumIdList) != S_OK) {
		if (bBind)
			pShellFolder->Release();
		return;
	}

	while (pEnumIdList->Next(1, &pidlChild, NULL) == S_OK) {
		pidlNew = ILCombine(pidlAbsolute, pidlChild);
		if (bInsertList) {
			attributes = SFGAO_GHOSTED | SFGAO_SHARE | SFGAO_LINK;
			pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild, &attributes);
			InsertListItem(pidlNew, pShellFolder, pidlChild, attributes, i);
		}
		else {
			attributes = SFGAO_HASSUBFOLDER | SFGAO_GHOSTED | SFGAO_SHARE | SFGAO_LINK;
			pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild, &attributes);
			pShellFolder->GetDisplayNameOf(pidlChild, SHGDN_NORMAL, &strret);
			StrRetToBuf(&strret, pidlChild, szDisplayName, sizeof(szDisplayName) / sizeof(TCHAR));
			InsertTreeItem(pidlNew, hitem, szDisplayName, attributes);
		}
		
		i++;
		
		CoTaskMemFree(pidlChild);
	}

	if (bInsertList)
		ListView_SortItems(g_hwndListView, CompareFunc, (LPARAM)pShellFolder);

	if (bBind)
		pShellFolder->Release();

	pEnumIdList->Release();
}

void InsertListItem(PIDLIST_ABSOLUTE pidlAbsolute, IShellFolder *pShellFolder, PITEMID_CHILD pidlChild, SFGAOF attributes, int i)
{
	int           j;
	TCHAR         szBuf[256];
	LVITEM        item;
	SHFILEINFO    fileInfo;
	SHELLDETAILS  details;
	IShellFolder2 *pShellFolder2;

	if (pShellFolder->QueryInterface(IID_PPV_ARGS(&pShellFolder2)) != S_OK)
		return;

	for (j = 0; j < 4; j++) {
		pShellFolder2->GetDetailsOf(pidlChild, j, &details);
		StrRetToBuf(&details.str, pidlChild, szBuf, sizeof(szBuf) / sizeof(TCHAR));
		
		item.mask     = LVIF_TEXT;
		item.iItem    = i;
		item.iSubItem = j;
		item.pszText  = szBuf;

		if (j == 0) {
			SHGetFileInfo((LPTSTR)pidlAbsolute, 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_PIDL);

			item.mask   |= LVIF_IMAGE | LVIF_PARAM;
			item.iImage = fileInfo.iIcon;
			item.lParam = (LPARAM)pidlAbsolute;

			if (attributes & SFGAO_GHOSTED) {
				item.mask     |= LVIF_STATE;
				item.state     = LVIS_CUT;
				item.stateMask = LVIS_CUT;
			}
			
			if (attributes & SFGAO_SHARE) {
				item.mask     |= LVIF_STATE;
				item.state     = INDEXTOOVERLAYMASK(1);
				item.stateMask = LVIS_OVERLAYMASK;
			}
			
			if (attributes & SFGAO_LINK) {
				item.mask     |= LVIF_STATE;
				item.state     = INDEXTOOVERLAYMASK(2);
				item.stateMask = LVIS_OVERLAYMASK;
			}

			ListView_InsertItem(g_hwndListView, &item);
		}
		else
			ListView_SetItem(g_hwndListView, &item);
	}

	pShellFolder2->Release();
}

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

	SHGetFileInfo((LPTSTR)pidlAbsolute, 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_PIDL);
	is.item.iImage = fileInfo.iIcon;

	SHGetFileInfo((LPTSTR)pidlAbsolute, 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_PIDL | SHGFI_OPENICON);
	is.item.iSelectedImage = fileInfo.iIcon;

	is.hParent      = hitemParent;
	is.item.mask    = TVIF_TEXT | TVIF_PARAM | TVIF_IMAGE | TVIF_SELECTEDIMAGE;
	is.item.pszText = lpszName;
	is.item.lParam  = (LPARAM)pidlAbsolute;
	
	if (attributes & SFGAO_GHOSTED) {
		is.item.mask     |= LVIF_STATE;
		is.item.state     = LVIS_CUT;
		is.item.stateMask = LVIS_CUT;
	}
	
	if (attributes & SFGAO_SHARE) {
		is.item.mask     |= LVIF_STATE;
		is.item.state     = INDEXTOOVERLAYMASK(1);
		is.item.stateMask = LVIS_OVERLAYMASK;
	}
	
	if (attributes & SFGAO_LINK) {
		is.item.mask     |= LVIF_STATE;
		is.item.state     = INDEXTOOVERLAYMASK(2);
		is.item.stateMask = LVIS_OVERLAYMASK;
	}

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

int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
	IShellFolder     *pShellFolder = (IShellFolder *)lParamSort;
	PIDLIST_ABSOLUTE pidlAbsolute1 = (PIDLIST_ABSOLUTE)lParam1;
	PIDLIST_ABSOLUTE pidlAbsolute2 = (PIDLIST_ABSOLUTE)lParam2;
	PITEMID_CHILD    pidlChild1 = ILFindLastID(pidlAbsolute1);
	PITEMID_CHILD    pidlChild2 = ILFindLastID(pidlAbsolute2);
	SFGAOF           fileAttributes = SFGAO_FILESYSTEM;
	SFGAOF           folderAttributes = SFGAO_FOLDER | SFGAO_FILESYSTEM;
	SFGAOF           virtualAttributes = SFGAO_FOLDER;
	SFGAOF           attributes1 = SFGAO_FILESYSTEM | SFGAO_FOLDER;
	SFGAOF           attributes2 = SFGAO_FILESYSTEM | SFGAO_FOLDER;
	int              nResult = -1;

	pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild1, &attributes1);
	pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild2, &attributes2);
	attributes1 &= SFGAO_FILESYSTEM | SFGAO_FOLDER;
	attributes2 &= SFGAO_FILESYSTEM | SFGAO_FOLDER;

	if (attributes1 == attributes2)
		return pShellFolder->CompareIDs(0, pidlChild1, pidlChild2) * -1;

	if (attributes1 == virtualAttributes)
		nResult = -1;
	else if (attributes1 == folderAttributes) {
		if (attributes2 == virtualAttributes)
			nResult = 1;
		else
			nResult = -1;
	}
	else if (attributes1 == fileAttributes)
		nResult = 1;
	else
		nResult = -1;
	
	return nResult;
}

リストビューの作成は、CreateWindowExにWC_LISTVIEWを指定することができます。 ウインドウスタイルにLVS_REPORTを指定していることから、作成されるリストビューの形式は詳細表示になります。 今回は、ツリービューやリストビューのアイテムにアイコンを表示するということで、 各ウインドウにイメージリストを設定しています。 このイメージリストは、システム上に存在するアイコンを格納したシステムイメージリストでなければならないため、 SHGetFileInfoを呼び出すことになります。

himl = (HIMAGELIST)SHGetFileInfo(TEXT("C:\\"), 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_SMALLICON);
ListView_SetImageList(hwndListView, himl, LVSIL_SMALL);
TreeView_SetImageList(hwndTreeView, himl, TVSIL_NORMAL);

第1引数は、ドライブ文字列で構いません。 第5引数にSHGFI_SYSICONINDEXを指定することにより、戻り値がシステムイメージリストのハンドルになり、 さらにSHGFI_SMALLICONを指定していることから、小さいアイコンがシステムイメージリストに含まれることになります。 イメージリストの設定は、それぞれListView_SetImageListとTreeView_SetImageListが行うことができますが、 前者の第3引数はLVSIL_NORMALではなくLVSIL_SMALLを指定します。 ちなみに、Windows XP以降ならば、Shell_GetImageListsという関数でもシステムイメージリストのハンドルを取得することができます。

詳細表示のリストビューにはカラムを設定することができますが、このカラムの値は常に固定であってはいけません。 一般のフォルダでは、名前、サイズ、種類、更新日時というカラムになっていますが、 ごみ箱のような特殊なフォルダではまた別のカラムになっているため、 現在のフォルダに応じたカラムを設定することになります。 このための処理はResetListViewColumnで行われています。

void ResetListViewColumn(IShellFolder *pShellFolder)
{
	int           i, nCol;
	TCHAR         szBuf[256];
	LVCOLUMN      column;
	SHELLDETAILS  details;
	IShellFolder2 *pShellFolder2;

	nCol = Header_GetItemCount(ListView_GetHeader(g_hwndListView));
	for (i = 0; i < nCol; i++)
		ListView_DeleteColumn(g_hwndListView, 0);

	if (pShellFolder->QueryInterface(IID_PPV_ARGS(&pShellFolder2)) != S_OK)
		return;
	
	for (i = 0; i < 4; i++) {
		if (pShellFolder2->GetDetailsOf(NULL, i, &details) != S_OK)
			break;

		StrRetToBuf(&details.str, NULL, szBuf, sizeof(szBuf) / sizeof(TCHAR));

		column.mask    = LVCF_WIDTH | LVCF_TEXT;
		column.cx      = 150;
		column.pszText = szBuf;
		ListView_InsertColumn(g_hwndListView, i, &column);
	}

	pShellFolder2->Release();
}

この関数では第1引数のフォルダに応じたカラムを設定することが目的であるため、 まずは現在設定されているカラムを削除することになります。 カラムの数はHeader_GetItemCountで取得できるため、 この数だけListView_DeleteColumnでカラムを削除することになります。 続いてフォルダに応じたカラムの情報を取得するために、IShellFolderからIShellFolder2を取得します。 IShellFolder2::GetDetailsOfの第1引数にNULLを指定すれば、 第2引数のインデックスに相当するカラムの情報を取得することができます。 GetDetailsOfが成功すればSHELLDETAILS構造体のstrメンバが初期化されるため、 これをStrRetToBufに指定することでカラムの名前を取得することができます。 LVCOLUMN構造体のmaskにLVCF_WIDTHとLVCF_TEXTを指定していることから、 cxとpszTextを初期化することができ、前者にはカラムの幅を、後者にはカラムの名前を指定します。 ListView_InsertColumnによってカラムが設定されることになり、 今回はこれが4回行われることになるため、原則として4つのカラムが設定されることになります。

リストビューにアイテムを追加するタイミングは、ツリービューのアイテムが選択されたときになります。 このときには、通知コードがTVN_SELCHANGEDであるWM_NOTIFYが送られるため、これを検出することになります。

else if (lpNmhdr->code == TVN_SELCHANGED) {
	LPTVITEM lp = &((LPNMTREEVIEW)lParam)->itemNew;
	ListView_DeleteAllItems(hwndListView);
	EnumItem((PIDLIST_ABSOLUTE)lp->lParam, hwnd, FALSE, NULL);
}

lParamにはNMTREEVIEW構造体のアドレスが格納されており、 itemNewには選択されたアイテムの情報が格納されています。 ListView_DeleteAllItemsを呼び出しているのは、既に追加されているアイテムを全て削除するためです。 EnumItemの第3引数は、ツリービューにアイテムを追加する場合はTRUEになりますが、 リストビューはFALSEを指定します。

今回のEnumItemは、ツリービューのアイテムだけでなくリストビューのアイテムの列挙にも使用されるため、 どちらの列挙かを見分けるためにbInsertListという引数が用意されています。 これがTRUEである場合はリストビューのアイテムを列挙することを意味します。 bInsertListがTRUEである場合の処理を重点的に見ていきます。

if (bInsertList) {
	ListView_DeleteAllItems(g_hwndListView);
	ResetListViewColumn(pShellFolder);
}

このコードはリストビューの初期化に相当します。 リストビューのアイテムが新しく列挙されるということで、 ListView_DeleteAllItemsで全てのアイテムを削除し、 ResetListViewColumnによってカラムを再設定します。

flag = bInsertList ? SHCONTF_FOLDERS | SHCONTF_NONFOLDERS : SHCONTF_FOLDERS;
if (pShellFolder->EnumObjects(hwnd, flag | SHCONTF_INCLUDEHIDDEN, &pEnumIdList) != S_OK) {
	if (bBind)
		pShellFolder->Release();
	return;
}

このコードは、列挙するアイテムの種類を決定する部分です。 bInsertListがTRUEの場合はフォルダとファイルを列挙することになりますが、 FALSEの場合はフォルダのみを列挙することになります。 隠しアイテムを列挙することを示すSHCONTF_INCLUDEHIDDENは、 どちらの場合も指定するようにしています。

if (bInsertList) {
	attributes = SFGAO_GHOSTED | SFGAO_SHARE | SFGAO_LINK;
	pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild, &attributes);
	InsertListItem(pidlNew, pShellFolder, pidlChild, attributes, i);
}
else {
	attributes = SFGAO_HASSUBFOLDER | SFGAO_GHOSTED | SFGAO_SHARE | SFGAO_LINK;
	pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild, &attributes);
	pShellFolder->GetDisplayNameOf(pidlChild, SHGDN_NORMAL, &strret);
	StrRetToBuf(&strret, pidlChild, szDisplayName, sizeof(szDisplayName) / sizeof(TCHAR));
	InsertTreeItem(pidlNew, hitem, szDisplayName, attributes);
}

このコードは、アイテムを実際に追加する部分です。 bInsertListTRUEの場合はInsertListItemでリストビューにアイテムを追加し、 FALSEの場合はInsertTreeItemでツリービューにアイテムを追加します。

InsertListItemの内部を次に示します。

void InsertListItem(PIDLIST_ABSOLUTE pidlAbsolute, IShellFolder *pShellFolder, PITEMID_CHILD pidlChild, SFGAOF attributes, int i)
{
	int           j;
	TCHAR         szBuf[256];
	LVITEM        item;
	SHFILEINFO    fileInfo;
	SHELLDETAILS  details;
	IShellFolder2 *pShellFolder2;

	if (pShellFolder->QueryInterface(IID_PPV_ARGS(&pShellFolder2)) != S_OK)
		return;

	for (j = 0; j < 4; j++) {
		pShellFolder2->GetDetailsOf(pidlChild, j, &details);
		StrRetToBuf(&details.str, pidlChild, szBuf, sizeof(szBuf) / sizeof(TCHAR));
		
		item.mask     = LVIF_TEXT;
		item.iItem    = i;
		item.iSubItem = j;
		item.pszText  = szBuf;

		if (j == 0) {
			SHGetFileInfo((LPTSTR)pidlAbsolute, 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_PIDL);

			item.mask   |= LVIF_IMAGE | LVIF_PARAM;
			item.iImage = fileInfo.iIcon;
			item.lParam = (LPARAM)pidlAbsolute;

			if (attributes & SFGAO_GHOSTED) {
				item.mask     |= LVIF_STATE;
				item.state     = LVIS_CUT;
				item.stateMask = LVIS_CUT;
			}
			
			if (attributes & SFGAO_SHARE) {
				item.mask     |= LVIF_STATE;
				item.state     = INDEXTOOVERLAYMASK(1);
				item.stateMask = LVIS_OVERLAYMASK;
			}
			
			if (attributes & SFGAO_LINK) {
				item.mask     |= LVIF_STATE;
				item.state     = INDEXTOOVERLAYMASK(2);
				item.stateMask = LVIS_OVERLAYMASK;
			}

			ListView_InsertItem(g_hwndListView, &item);
		}
		else
			ListView_SetItem(g_hwndListView, &item);
	}

	pShellFolder2->Release();
}

リストビューに追加するデータは各カラムによって異なるわけですが、 そのカラムのインデックスとアイテムのPIDLをIShellFolder2::GetDetailsOfに指定すれば、 カラムに応じた適切なデータを取得できるようになっています。 よって、最初にQueryInterfaceからIShellFolder2を取得するようにしています。 SHGetFileInfoの呼び出しは、イメージスト上におけるアイコンのインデックスを取得するために行われています。 このときには第5引数にSHGFI_SYSICONINDEXを指定し、 さらに第1引数にアイテムのPIDLを指定するためにSHGFI_PIDLを指定します。 LVITEM構造体のmaskにLVIF_IMAGEを指定していることから、 取得したアイコンのインデックスをiImageに指定することができます。 また、LVIF_PARAMも指定しているため、lParamにアイテムのPIDLを関連付けることができます。 attributesにSFGAO_GHOSTEDが含まれている場合は、アイテムが隠しアイテムであることを意味します。 このときには、アイテムが薄いアイコンで表示されるようにstateとstateMaskにLVIS_CUTを指定します。 attributesにSFGAO_SHAREが含まれる場合は、共有を表すオーバーレイアイコンを表示するために、stateにINDEXTOOVERLAYMASK(1)を指定します。 このときには、stateMaskにLVIS_OVERLAYMASKを指定します。 attributesにSFGAO_LINKが含まれている場合は、ショートカットを示すオーバーレイアイコンを表示するために、stateにINDEXTOOVERLAYMASK(2)を指定します。 このときには、stateMaskにLVIS_OVERLAYMASKを指定します。 stateとstateMaskを初期化する場合は、maskにLVIF_STATEを指定します。

InsertTreeItemの実装は次のようになっています。

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

	SHGetFileInfo((LPTSTR)pidlAbsolute, 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_PIDL);
	is.item.iImage = fileInfo.iIcon;

	SHGetFileInfo((LPTSTR)pidlAbsolute, 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_PIDL | SHGFI_OPENICON);
	is.item.iSelectedImage = fileInfo.iIcon;

	is.hParent      = hitemParent;
	is.item.mask    = TVIF_TEXT | TVIF_PARAM | TVIF_IMAGE | TVIF_SELECTEDIMAGE;
	is.item.pszText = lpszName;
	is.item.lParam  = (LPARAM)pidlAbsolute;
	
	if (attributes & SFGAO_GHOSTED) {
		is.item.mask     |= LVIF_STATE;
		is.item.state     = LVIS_CUT;
		is.item.stateMask = LVIS_CUT;
	}
	
	if (attributes & SFGAO_SHARE) {
		is.item.mask     |= LVIF_STATE;
		is.item.state     = INDEXTOOVERLAYMASK(1);
		is.item.stateMask = LVIS_OVERLAYMASK;
	}
	
	if (attributes & SFGAO_LINK) {
		is.item.mask     |= LVIF_STATE;
		is.item.state     = INDEXTOOVERLAYMASK(2);
		is.item.stateMask = LVIS_OVERLAYMASK;
	}

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

ツリービューのアイテムにアイコンを表示するためには、イメージリスト上のアイコンを取得する必要があります。 よって、SHGetFileInfoにSHGFI_SYSICONINDEXを指定し、取得したインデックスをitem.iImageに指定します。 これに加えてツリービューのアイテムは、展開時におけるアイコンも設定することができるため、 このインデックスを取得するためにSHGFI_SYSICONINDEXとSHGFI_OPENICONを共に指定します。 そして、取得したインデックスをitem.iSelectedImagを指定します。 iImageやiSelectedImagを初期化する場合は、item.maskにTVIF_IMAGEとTVIF_SELECTEDIMAGEを指定しておきます。 attributesによる条件式は、InsertListItemと同じ要領です。

IEnumIDList::Nextが返すPIDLの順序により、リストビューのアイテムは昇順で追加されることになります。 しかし、エクスプローラーのリストビューを確認すると分かるように、 フォルダは常にファイルより先行して表示されています。 つまり、aというファイルとbというフォルダが存在するならば、 bというフォルダが先に表示されなければならないということです。 この点を踏まえ、EnumItemでは、全てのアイテムをリストビューに追加し終えた後でListView_SortItemsを呼び出しています。 このマクロは、比較対象とする2つのアイテムのLPARAMをコールバック関数に指定するため、 アプリケーションはそのコールバック関数でアイテムの比較を行うことができます。 また、比較の際に必要となるデータは、第3引数を通じてコールバック関数のlParamSortに渡すことができます。 コールバック関数の実装は次のようになっています。

int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
	IShellFolder     *pShellFolder = (IShellFolder *)lParamSort;
	PIDLIST_ABSOLUTE pidlAbsolute1 = (PIDLIST_ABSOLUTE)lParam1;
	PIDLIST_ABSOLUTE pidlAbsolute2 = (PIDLIST_ABSOLUTE)lParam2;
	PITEMID_CHILD    pidlChild1 = ILFindLastID(pidlAbsolute1);
	PITEMID_CHILD    pidlChild2 = ILFindLastID(pidlAbsolute2);
	ULONG            fileAttributes = SFGAO_FILESYSTEM;
	ULONG            folderAttributes = SFGAO_FOLDER | SFGAO_FILESYSTEM;
	ULONG            virtualAttributes = SFGAO_FOLDER;
	ULONG            attributes1 = SFGAO_FILESYSTEM | SFGAO_FOLDER;
	ULONG            attributes2 = SFGAO_FILESYSTEM | SFGAO_FOLDER;
	int              nResult = -1;

	pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild1, &attributes1);
	pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild2, &attributes2);
	attributes1 &= SFGAO_FILESYSTEM | SFGAO_FOLDER;
	attributes2 &= SFGAO_FILESYSTEM | SFGAO_FOLDER;

	if (attributes1 == attributes2)
		return pShellFolder->CompareIDs(0, pidlChild1, pidlChild2) * -1;

	if (attributes1 == virtualAttributes)
		nResult = -1;
	else if (attributes1 == folderAttributes) {
		if (attributes2 == virtualAttributes)
			nResult = 1;
		else
			nResult = -1;
	}
	else if (attributes1 == fileAttributes)
		nResult = 1;
	else
		nResult = -1;
	
	return nResult;
}

既に述べたように、フォルダはファイルより優先して表示しなければならないわけですが、 名前だけではそのアイテムがフォルダかどうかは判断できません。 よって、GetAttributesOfでアイテムの属性を取得するようにしています。 attributes1がlParam1で識別されるアイテムの属性を表し、 attributes2がlParam2で識別されるアイテムの属性を表します。 属性における優先順位は、仮想フォルダ(virtualAttributes)>通常のフォルダ(folderAttributes)>ファイル(fileAttributes)のようになっているため、 attributes1(2)にこれらの属性以外が含まれてはなりません。 よって、GetAttributesOfを呼び出す前に、取得したい属性(SFGAO_FILESYSTEMとSFGAO_FOLDER)をattributes1(2)に指定しておきます。 しかし、このようにしても実際には余分な属性が返ることがあるようなので、 論理積でSFGAO_FILESYSTEMとSFGAO_FOLDERだけが考慮されるようにしています。 attributes1とattributes2が同じである場合は、 どちらの優先順位が高いかを容易に判断することができません。 フォルダとファイルならフォルダを優先すべきであると判断できるわけですが、 フォルダ同士のような同属性の場合はそう簡単にはいきません。 よって、親フォルダのCompareIDsを呼び出すことで、親フォルダに判定を任せています。 コールバックの関数の戻り値は、lParam1がlParam2より優先すべき時に負の値、 lParam1よりlParam2を優先すべき時は正の値、 lParam1とlParam2が一致する場合は0となっていますが、 CompareIDsの戻り値はこの正負が逆転しています。 よって、戻り値に-1を掛けています。

attributes1がvirtualAttributesと同一であるかの条件式は、 アイテムの属性が仮想フォルダであるかどうかの判定になります。 これが真である場合は、attributes2より優先順位が高いということになるので、 戻り値として負の値を返すことになります。 attributes1がfolderAttributesと同一であるかどうかの条件式は、 アイテムの属性がフォルダであるかどうかの判定になります。 これが真である場合は、attributes2が仮想フォルダであるかを確認し、 そうであるならばattributes2を優先すべきですから正の値を返します。 逆に、attributes2が仮想フォルダでない場合は、 attributes1を優先すべきということで負の値を返します。 attributes1がfileAttributesと同一であるかの条件式は、 アイテムの属性がファイルであるかどうかの判定になります。 これが真である場合は、attributes2を優先すべきですから正の値を返します。 else文はattributes1が0の場合に実行されますが、 これは仮想フォルダのアイテムなどで該当します。

IShellFolder2::GetDetailsExについて

今回はIShellFolder2::GetDetailsOfを使用してカラムに関連するデータを取得しましたが、 これはIShellFolder2::GetDetailsExでも行うことができます。 GetDetailsOfがインデックスをベースにデータを取得するのに対し、 GetDetailsExはカラムを識別するプロパティキーをベースにデータを取得します。 GetDetailsExを呼び出すようにしたInsertListItemを次に示します。

void InsertListItem(PIDLIST_ABSOLUTE pidlAbsolute, IShellFolder *pShellFolder, PITEMID_CHILD pidlChild, SFGAOF attributes, int i)
{
	int           j;
	TCHAR         szBuf[256];
	LVITEM        item;
	VARIANT       varResult;
	SHFILEINFO    fileInfo;
	IShellFolder2 *pShellFolder2;
	PROPERTYKEY   propertyKey[] = {PKEY_ItemNameDisplay, PKEY_Size, PKEY_ItemTypeText, PKEY_DateModified};

	if (pShellFolder->QueryInterface(IID_PPV_ARGS(&pShellFolder2)) != S_OK)
		return;

	for (j = 0; j < 4; j++) {
		VariantInit(&varResult);
		if (pShellFolder2->GetDetailsEx(pidlChild, &propertyKey[j], &varResult) == S_OK) {
			if (varResult.vt == VT_BSTR)
				lstrcpy(szBuf, varResult.bstrVal);
			else if (varResult.vt == VT_UI8)
				StrFormatByteSize64(varResult.ullVal, szBuf, sizeof(szBuf) / sizeof(TCHAR));
			else if (varResult.vt == VT_DATE) {
				SYSTEMTIME systemTime;
				VariantTimeToSystemTime(varResult.date, &systemTime);
				wsprintf(szBuf, TEXT("%04d/%02d/%02d %02d:%02d"), systemTime.wYear, systemTime.wMonth, systemTime.wDay, systemTime.wHour, systemTime.wMinute);
			}
			else
				lstrcpy(szBuf, TEXT(""));

			VariantClear(&varResult);
		}
		else
			lstrcpy(szBuf, TEXT(""));
		
		item.mask     = LVIF_TEXT;
		item.iItem    = i;
		item.iSubItem = j;
		item.pszText  = szBuf;

		if (j == 0) {
			SHGetFileInfo((LPTSTR)pidlAbsolute, 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_PIDL);

			item.mask   |= LVIF_IMAGE | LVIF_PARAM;
			item.iImage = fileInfo.iIcon;
			item.lParam = (LPARAM)pidlAbsolute;

			if (attributes & SFGAO_GHOSTED) {
				item.mask     |= LVIF_STATE;
				item.state     = LVIS_CUT;
				item.stateMask = LVIS_CUT;
			}
			
			if (attributes & SFGAO_SHARE) {
				item.mask     |= LVIF_STATE;
				item.state     = INDEXTOOVERLAYMASK(1);
				item.stateMask = LVIS_OVERLAYMASK;
			}
			
			if (attributes & SFGAO_LINK) {
				item.mask     |= LVIF_STATE;
				item.state     = INDEXTOOVERLAYMASK(2);
				item.stateMask = LVIS_OVERLAYMASK;
			}

			ListView_InsertItem(g_hwndListView, &item);
		}
		else
			ListView_SetItem(g_hwndListView, &item);
	}

	pShellFolder2->Release();
}

propertyKeyというPROPERTYKEY構造体の配列は、各カラムの名前を識別するプロパティキーを格納します。 本当はGetDetailsExが受け取る型はSHCOLUMNID構造体なのですが、 Windows Vista以降ではPROPERTYKEY構造体に置き換わっているので問題ありません。 GetDetailsExが成功した場合は第3引数にデータが格納されますが、この型はVARIANT構造体になっています。 VARIANT構造体ではどのような型のデータでも格納できることになっているため、 vtメンバを通じて現在格納されているデータの型を確認することになります。 VT_BSTRの場合はbstrValに文字列が格納されているため、これをszBufにコピーします。 VT_UI8の場合はullValに数値が格納されており、 上記の場合ではPKEY_Sizeのみがこれに該当するため、 StrFormatByteSize64で文字列化されたサイズを取得します。 VT_DATEの場合はdateに時間が格納されているため、 VariantTimeToSystemTimeでSYSTEMTIME構造体に変換することができます。 後はこの構造体のメンバを基にszBufをフォーマットすればよいでしょう。

プロパティキーをベースにカラムの名前を取得することは少しだけ複雑です。 GetDetailsOfでは第1引数にNULLを取得することでカラムの名前を取得できたわけですが、 GetDetailsExではこれが失敗するからです。 よって、プロパティ系の関数を明示的に呼び出すことになります。

void ResetListViewColumn(IShellFolder *pShellFolder)
{
	int                  i, nCol;
	LPWSTR               lpszCanonicalName;
	LPWSTR               lpszDisplayName;
	LVCOLUMN             column;
	IShellFolder2        *pShellFolder2;
	IPropertyDescription *pPropertyDescription;
	PROPERTYKEY          propKey[] = {PKEY_ItemNameDisplay, PKEY_Size, PKEY_ItemTypeText, PKEY_DateModified};

	nCol = Header_GetItemCount(ListView_GetHeader(g_hwndListView));
	for (i = 0; i < nCol; i++)
		ListView_DeleteColumn(g_hwndListView, 0);

	if (pShellFolder->QueryInterface(IID_PPV_ARGS(&pShellFolder2)) != S_OK)
		return;

	for (i = 0; i < 4; i++) {
		PSGetNameFromPropertyKey(propKey[i], &lpszCanonicalName);
		PSGetPropertyDescriptionByName(lpszCanonicalName, IID_PPV_ARGS(&pPropertyDescription));
		pPropertyDescription->GetDisplayName(&lpszDisplayName);

		column.mask    = LVCF_WIDTH | LVCF_TEXT;
		column.cx      = 150;
		column.pszText = lpszDisplayName;
		ListView_InsertColumn(g_hwndListView, i, &column);

		pPropertyDescription->Release();
		CoTaskMemFree(lpszDisplayName);
		CoTaskMemFree(lpszCanonicalName);
	}

	pShellFolder2->Release();
}

PSGetNameFromPropertyKeyにプロパティキーを指定すると、第2引数に関連する正規名が返ることになります。 この正規名というのはSystem.ItemNameDisplayというようなものであり、 これをカラムの名前にするわけにはいきませんから、 ここからさらに関連する表示名を取得することになります。 具体的にはPSGetPropertyDescriptionByNameでIPropertyDescriptionを取得し、 GetDisplayNameを呼び出します。 プロパティ系の定義を使用する場合はpropkey.hをインクルードし、 関数を呼び出す場合はpropsys.libへのリンクを忘れないでください。



戻る