EternalWindows
シェル名前空間 / シェル変更通知

ファイラーによって表示されたファイルやフォルダは、 常にシェル名前空間のそれと整合性がなければなりません。 たとえば、デスクトップに新しいフォルダが作成されたならば、 ファイラーのツリービューにはそのためのフォルダが即座に追加される必要があります。 このようなシェルに対して発生したイベントを受け取るには、 SHChangeNotifyRegisterを呼び出すことになります。

ULONG SHChangeNotifyRegister(
  HWND hwnd,
  int fSources,
  LONG fEvents,
  UINT wMsg,
  int cEntries,
  const SHChangeNotifyEntry *pshcne
);

hwndは、通知メッセージを受け取るウインドウハンドルを指定します。 fSourcesは、通知を受け取りたいイベントのタイプを指定します。 SHCNRF_ShellLevelという定数は必ず指定することになります。 fEventsは、通知を受け取りたいイベントの種類を指定します。 wMsgは、通知メッセージの値を指定します。 イベントが発生した場合は、この値がウインドウプロシージャに送られます。 cEntriesは、pshcneの要素数を指定します。 pshcneは、SHChangeNotifyEntry構造体のアドレスを指定します。 この構造体には、通知の対象とするアイテムのPIDLを指定することになります。

次に、SHChangeNotifyRegisterの第3引数に指定できる定数の一部を示します。

定数 意味
SHCNE_CREATE ファイルが作成されたことを意味する。
SHCNE_DELETE ファイルが削除されたことを意味する。
SHCNE_RENAMEITEM ファイルの名前が変更されたことを意味する。
SHCNE_MKDIR フォルダが作成されたことを意味する。
SHCNE_RMDIR フォルダが削除されたことを意味する。
SHCNE_RENAMEFOLDER フォルダの名前が変更されたことを意味する。

SHCNE_DELETEが送られた場合は、その直後にSHCNE_CREATEが送られることがありますが、これは誤動作ではありません。 この場合のSHCNE_CREATEは、ごみ箱に新しいファイルを作成したという意味になります。

イベントの通知が不要になった場合は、SHChangeNotifyDeregisterを呼び出します。

BOOL SHChangeNotifyDeregister(
  ULONG ulID
);

uIDは、SHChangeNotifyRegisterの戻り値を指定します。

今回のプログラムは、ファイルの作成と削除、及び名前変更を監視します。 フォルダの作成や削除は監視していないので注意してください。

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

#define WM_SHELLCHANGE WM_APP

#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;
PIDLIST_ABSOLUTE g_pidlCurFolder = NULL;

void NotifyShellChange(PIDLIST_ABSOLUTE pidlAbsplute1, PIDLIST_ABSOLUTE pidlAbsolute2, LONG lEvent);
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;
	static ULONG uRegisterId = 0;
	
	switch (uMsg) {
		
	case WM_CREATE: {
		HIMAGELIST           himl;
		SHFILEINFO           fileInfo;
		INITCOMMONCONTROLSEX ic;
		SHChangeNotifyEntry  entry;

		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);
		
		entry.pidl = g_pidlDesktop;
		entry.fRecursive = TRUE;
		uRegisterId = SHChangeNotifyRegister(hwnd, SHCNRF_ShellLevel | SHCNRF_NewDelivery, SHCNE_CREATE | SHCNE_DELETE | SHCNE_RENAMEITEM, WM_SHELLCHANGE, 1, &entry);

		return 0;
	}

	case WM_SHELLCHANGE: {
		LONG             lEvent;
		HANDLE           hLock;
		PIDLIST_ABSOLUTE *pidlAbsolute;
	
		hLock = SHChangeNotification_Lock((HANDLE)wParam, (DWORD)lParam, &pidlAbsolute, &lEvent);
		if (hLock != NULL) {
			if (ILIsParent(g_pidlCurFolder, pidlAbsolute[0], TRUE))
				NotifyShellChange(pidlAbsolute[0], pidlAbsolute[1], lEvent);
			SHChangeNotification_Unlock(hLock);
		}
		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);
			if (g_pidlCurFolder != NULL)
				CoTaskMemFree(g_pidlCurFolder);
			g_pidlCurFolder = ILClone((PIDLIST_ABSOLUTE)lp->lParam);
		}
		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:
		SHChangeNotifyDeregister(uRegisterId);

		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 NotifyShellChange(PIDLIST_ABSOLUTE pidlAbsolute1, PIDLIST_ABSOLUTE pidlAbsolute2, LONG lEvent)
{
	int    i, nCount;
	LVITEM item;

	if (lEvent == SHCNE_CREATE) {
		SFGAOF           attributes;
		PITEMID_CHILD    pidlChild;
		PIDLIST_ABSOLUTE pidlNew;
		IShellFolder     *pShellFolder;

		SHBindToParent(pidlAbsolute1, IID_PPV_ARGS(&pShellFolder), NULL);
		
		pidlNew = ILClone(pidlAbsolute1);
		pidlChild = ILFindLastID(pidlNew);
	
		attributes = SFGAO_FILESYSTEM | SFGAO_GHOSTED | SFGAO_SHARE | SFGAO_LINK;
		pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild, &attributes);
		InsertListItem(pidlNew, pShellFolder, pidlChild, attributes, -1);
		
		pShellFolder->Release();
	}
	else if (lEvent == SHCNE_DELETE) {
		nCount = ListView_GetItemCount(g_hwndListView);
		for (i = 0; i < nCount; i++) {
			item.mask     = LVIF_PARAM;
			item.iItem    = i;
			item.iSubItem = 0;
			ListView_GetItem(g_hwndListView, &item);

			if (ILIsEqual((PIDLIST_ABSOLUTE)item.lParam, pidlAbsolute1)) {
				ListView_DeleteItem(g_hwndListView, i);
				break;
			}
		}
	}
	else if (lEvent == SHCNE_RENAMEITEM) {
		nCount = ListView_GetItemCount(g_hwndListView);
		for (i = 0; i < nCount; i++) {
			item.mask     = LVIF_PARAM;
			item.iItem    = i;
			item.iSubItem = 0;
			ListView_GetItem(g_hwndListView, &item);

			if (ILIsEqual((PIDLIST_ABSOLUTE)item.lParam, pidlAbsolute1)) {
				TCHAR  szPath[256];
				LPTSTR lpszName;

				SHGetPathFromIDList(pidlAbsolute2, szPath);
				lpszName = PathFindFileName(szPath);

				item.mask       = LVIF_TEXT | LVIF_PARAM;
				item.iItem      = i;
				item.iSubItem   = 0;
				item.pszText    = lpszName;
				item.cchTextMax = lstrlen(lpszName);
				item.lParam     = (LPARAM)ILClone(pidlAbsolute2);
				ListView_SetItem(g_hwndListView, &item);
				
				break;
			}
		}
	}
}

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;
	BOOL          bTimeWrite = FALSE;

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

	if (i == -1) {
		bTimeWrite = TRUE;
		i = ListView_GetItemCount(g_hwndListView);
	}

	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 (bTimeWrite) {
			TCHAR      szPath[256];
			HANDLE     hFile;
			FILETIME   fileTime, ftLastWriteTime;
			SYSTEMTIME systemTime;
			SHCOLUMNID scid;

			pShellFolder2->MapColumnToSCID(j, &scid);
			if (scid.pid == 14) {
				SHGetPathFromIDList(pidlAbsolute, szPath);
				hFile = CreateFile(szPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
				GetFileTime(hFile, NULL, NULL, &ftLastWriteTime);
				CloseHandle(hFile);
				
				FileTimeToLocalFileTime(&ftLastWriteTime, &fileTime);
				FileTimeToSystemTime(&fileTime, &systemTime);
				wsprintf(szBuf, TEXT("%04d/%02d/%02d %02d:%02d"), systemTime.wYear, systemTime.wMonth, systemTime.wDay, systemTime.wHour, systemTime.wMinute);
			}
		}

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

ファイルの監視が本当に機能しているかを確認するためには、次のような処理を行えばよいでしょう。 まず、ツリービューのデスクトップを選択して、デスクトップの中身がリストビューに表示されるようにします。 次に、エクスプローラーでファイルを作成すると、 その時点でリストビューに新しいアイテムが追加されます。 また、エクスプローラーで作成したファイルの名前を変更すると、リストビューのアイテムの名前も変更されます。 そして、エクスプローラーでファイルを削除したときには、対応するリストビューのアイテムが削除されるでしょう。 外部アプリケーションがCreateFileやDeleteFileを呼び出しても、アプリケーションには正常に通知されます。

SHChangeNotifyRegisterの呼び出しはWM_CREATEで行っています。

entry.pidl = g_pidlDesktop;
entry.fRecursive = TRUE;
uRegisterId = SHChangeNotifyRegister(hwnd, SHCNRF_ShellLevel | SHCNRF_NewDelivery, SHCNE_CREATE | SHCNE_DELETE | SHCNE_RENAMEITEM, WM_SHELLCHANGE, 1, &entry);

第2引数はSHCNRF_ShellLevelを必ず指定し、さらに通知メッセージの際にアイテムのPIDLを参照する場合はSHCNRF_NewDeliveryも指定します。 第3引数は通知を受け取る種類であり、引数の内容からファイルの作成、ファイルの削除、ファイル名の変更が通知されることになります。 SHChangeNotifyEntry構造体のメンバから分かるように、 通知の対象となるのは仮想フォルダのデスクトップであり、 さらにfRecursiveがTRUEであることから、デスクトップの以下のフォルダやファイルも通知対象になります。

SHChangeNotifyRegisterの第3引数に指定したイベントが発生した場合は、 第4引数に指定したメッセージがWindowProcに通知されることになります。 今回はこのメッセージをWM_SHELLCHANGEという名前で定義しています。

case WM_SHELLCHANGE: {
	LONG             lEvent;
	HANDLE           hLock;
	PIDLIST_ABSOLUTE *pidlAbsolute;
	
	hLock = SHChangeNotification_Lock((HANDLE)wParam, (DWORD)lParam, &pidlAbsolute, &lEvent);
	if (hLock != NULL) {
		if (ILIsParent(g_pidlCurFolder, pidlAbsolute[0], TRUE))
			NotifyShellChange(pidlAbsolute[0], pidlAbsolute[1], lEvent);
		SHChangeNotification_Unlock(hLock);
	}
	return 0;
}

イベントが発生したアイテムのPIDLを取得するにためには、SHChangeNotification_Lockを呼び出します。 第1引数と第2引数は、メッセージのwParamとlParamを指定します。 第3引数は、完全PIDLの配列を受け取る変数のアドレスを指定します。 配列の要素数は2であり、1番目の要素にはイベントが発生したアイテムのPIDLが格納され、 2番目の要素はイベントによって異なります。 PIDLが不要になった場合は、SHChangeNotification_Unlockを呼び出すことになります。 第4引数は発生したイベントを受け取る変数のアドレスです。 イベントに応じた処理を行うのはNotifyShellChangeという自作関数ですが、 今回は必ずしもこの関数を呼び出すとは限りません。 たとえば、現在表示しているフォルダがデスクトップである場合は、Cドライブに作成されたファイルをデスクトップのリストビューに表示するわけにはいかないでしょう。 つまり、現在のリストビューに表示されているフォルダがイベントの発生したファイルの親フォルダに相当しない場合は、 リストビューを更新する意味がないわけです。 よって、そのような判定をILIsParentで行うようにしています。 現在表示されているフォルダを表すPIDLは、次の部分で初期化されています。

else if (lpNmhdr->code == TVN_SELCHANGED) {
	LPTVITEM lp = &((LPNMTREEVIEW)lParam)->itemNew;
	EnumItem((PIDLIST_ABSOLUTE)lp->lParam, hwnd, TRUE, NULL);
	if (g_pidlCurFolder != NULL)
		CoTaskMemFree(g_pidlCurFolder);
	g_pidlCurFolder = ILClone((PIDLIST_ABSOLUTE)lp->lParam);
}

フォルダ内のアイテムをリストビューに追加するのはEnumItemであり、 そのEnumItemが呼ばれるのはツリービューのアイテムが選択された場合です。 つまり、ここで表示されるフォルダが決定されるため、 そのフォルダのPIDLを複製して保存することになります。

通知されたイベントを処理するべき場合は、NotifyShellChangeが呼ばれることになります。 まず、ファイルの作成が通知された際の処理を確認します。

if (lEvent == SHCNE_CREATE) {
	SFGAOF           attributes;
	PITEMID_CHILD    pidlChild;
	PIDLIST_ABSOLUTE pidlNew;
	IShellFolder     *pShellFolder;

	SHBindToParent(pidlAbsolute1, IID_PPV_ARGS(&pShellFolder), NULL);
	
	pidlNew = ILClone(pidlAbsolute1);
	pidlChild = ILFindLastID(pidlNew);

	attributes = SFGAO_FILESYSTEM | SFGAO_GHOSTED | SFGAO_SHARE | SFGAO_LINK;
	pShellFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&pidlChild, &attributes);
	InsertListItem(pidlNew, pShellFolder, pidlChild, attributes, -1);
	
	pShellFolder->Release();
}

新しいファイルが作成された場合は、そのファイルをアイテムとしてリストビューに追加しなければなりません。 アイテムを追加するInsertListItemではアイテムの親のフォルダを必要とするため、 SHBindToParentでこれを取得しておきます。 また、アイテムのLPARAMにはPIDLを指定しなければならないため、ILCloneでpidlAbsolute1を複製しておきます。 さらに、アイテムの相対PIDLとアイテムの属性も必要になるためこれらも取得するようにします。 InsertListItemの最終引数については後述します。

else if (lEvent == SHCNE_DELETE) {
	nCount = ListView_GetItemCount(g_hwndListView);
	for (i = 0; i < nCount; i++) {
		item.mask     = LVIF_PARAM;
		item.iItem    = i;
		item.iSubItem = 0;
		ListView_GetItem(g_hwndListView, &item);

		if (ILIsEqual((PIDLIST_ABSOLUTE)item.lParam, pidlAbsolute1)) {
			ListView_DeleteItem(g_hwndListView, i);
			break;
		}
	}
}

このコードはファイルが削除された場合に実行されます。 削除されたファイルを確認するためにはアイテムの数だけループし、 現在のアイテムのPIDLと削除されたアイテムのPIDLを比較します。 一致する場合は、現在のアイテムが削除対象ということになるため、 そのインデックスをListView_DeleteItemに指定します。

else if (lEvent == SHCNE_RENAMEITEM) {
	nCount = ListView_GetItemCount(g_hwndListView);
	for (i = 0; i < nCount; i++) {
		item.mask     = LVIF_PARAM;
		item.iItem    = i;
		item.iSubItem = 0;
		ListView_GetItem(g_hwndListView, &item);

		if (ILIsEqual((PIDLIST_ABSOLUTE)item.lParam, pidlAbsolute1)) {
			TCHAR  szPath[256];
			LPTSTR lpszName;

			SHGetPathFromIDList(pidlAbsolute2, szPath);
			lpszName = PathFindFileName(szPath);

			item.mask       = LVIF_TEXT | LVIF_PARAM;
			item.iItem      = i;
			item.iSubItem   = 0;
			item.pszText    = lpszName;
			item.cchTextMax = lstrlen(lpszName);
			item.lParam     = (LPARAM)ILClone(pidlAbsolute2);
			ListView_SetItem(g_hwndListView, &item);
			
			break;
		}
	}
}

このコードはファイルの名前が変更された場合に実行されます。 まず、現在のアイテムのPIDLが変更対象のアイテムのPIDLと一致するかを比較し、 一致する場合はそのアイテムに新しい名前と新しいPIDLを指定します。 この新しいPIDLはpidlAbsolute2に格納されているため、 SHGetPathFromIDListでファイルパスを取得し、 PathFindFileNameで新しいファイル名を取得します。 そして、ListView_SetItemでアイテムの情報を更新することになります。 pszTextとlParamが考慮されるためには、maskにLVIF_TEXTとLVIF_PARAMを指定する必要があります。

NotifyShellChangeではlEventがSHCNE_CREATEの際にアイテムの追加を行いましたが、実はここで1つの問題が発生します。 それは、InsertListItemの内部で呼び出しているIShellFolder2::GetDetailsOfが、アイテムの更新日時を返すことができないという点です。 何が原因でこのような動作になっているのかは分かりませんが、 このままでは更新日時のカラムに空白が表示されてしまうため、 アプリケーションが明示的に日時を算出することになります。

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;
	BOOL          bTimeWrite = FALSE;

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

	if (i == -1) {
		bTimeWrite = TRUE;
		i = ListView_GetItemCount(g_hwndListView);
	}

	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 (bTimeWrite) {
			TCHAR      szPath[256];
			HANDLE     hFile;
			FILETIME   fileTime, ftLastWriteTime;
			SYSTEMTIME systemTime;
			SHCOLUMNID scid;

			pShellFolder2->MapColumnToSCID(j, &scid);
			if (scid.pid == 14) { // Windows Vista以降では IsEqualPropertyKey(scid, PKEY_DateModified) が望ましい
				SHGetPathFromIDList(pidlAbsolute, szPath);
				hFile = CreateFile(szPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
				GetFileTime(hFile, NULL, NULL, &ftLastWriteTime);
				CloseHandle(hFile);
				
				FileTimeToLocalFileTime(&ftLastWriteTime, &fileTime);
				FileTimeToSystemTime(&fileTime, &systemTime);
				wsprintf(szBuf, TEXT("%04d/%02d/%02d %02d:%02d"), systemTime.wYear, systemTime.wMonth, systemTime.wDay, systemTime.wHour, systemTime.wMinute);
			}
		}

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

InsertListItemの最終引数はリストビュー上におけるアイテムのインデックスですが、 -1のときには更新日時を求めるべきことを示すフラグになります。 ただし、-1のままでは実際にアイテムを追加する際に問題ですから、 bTimeWriteという専用の変数をTRUEにします。 そしてiにはListView_GetItemCountの戻り値を指定し、最後のアイテムとしてリストビューへ追加されるようにします。 bTimeWriteがTRUEである場合の処理については、まずMapColumnToSCIDでカラムのIDを取得します。 このIDが14(PID_STG_WRITETIME)である場合は更新日時のカラムであることを意味するため、 更新日時を取得してszBufに格納することになります。 具体的にはSHGetPathFromIDListでPIDLからアイテムのパスを取得し、 それをCreateFileでオープンします。 ファイルハンドルが取得できればGetFileTimeで更新日時を取得することができます。 後はFileTimeToLocalFileTimeでファイル時刻をローカルファイル時刻に変換し、 それをFileTimeToSystemTimeに指定してSYSTEMTIME構造体を取得します。 この構造体には日付などのメンバが含まれるため、これを基にszBufをフォーマットすることになります。

今回のプログラムはフォルダに関する通知を受け取っていませんが、 実際の開発ではこうした通知も適切に処理することになります。 この上で最大の壁となるのは、複数存在するデスクトップのツリーをいかに同期させるかという点です。 たとえば、仮想デスクトップの直下にフォルダを作成した場合は、 その仮想デスクトップのツリー以下にフォルダを作成するのは当然ですが、 C:\Users\\Desktopにおけるツリー以下にもフォルダを作成しなければなりません。 しかし、実際に通知メッセージで取得できるPIDLは一方のデスクトップのものである場合もあり、 そう簡単には同期できないのが現状です。


戻る