EternalWindows
ExplorerBrowser / NameSpaceTreeControlの作成

NameSpaceTreeControlは、ExplorerBrowserと同じくWindows Vistaから登場したオブジェクトであり、 シェル名前空間のアイテムを基にしたツリービューの表示をサポートしています。 このオブジェクトはエクスプローラーのフォルダでも使用されていることから、 コンテキストメニューやD&Dなどの基本的な動作は既に実装されています。 特に、ツリー上のアイテムがファイルシステムと同期されている点は魅力的であると言えるでしょう。 アプリケーションはNameSpaceTreeControlを使用することで、 ツリービューを簡単に表示できるだけでなく、 多くの基本的な実装からも開放されることになります。

NameSpaceTreeControlを作成するには、CoCreateInstanceにCLSID_NamespaceTreeControlを指定します。 これにより、NameSpaceTreeControlを表すINameSpaceTreeControlを取得することができます。 INameSpaceTreeControlを取得したら、Initializeを呼び出してNameSpaceTreeControlを初期化します。

HRESULT INameSpaceTreeControl::Initialize(
  HWND hwndParent,
  RECT *prc,
  NSTCSTYLE nsctsFlags
);

hwndParentは、NameSpaceTreeControlを表示する親ウインドウのハンドルを指定します。 prcは、NameSpaceTreeControlを表示する位置を格納したRECT構造体のアドレスを指定します。 nsctsFlagsは、NameSpaceTreeControlの特徴を定義する定数を指定します。 関数が成功した場合はS_OKが返ります。

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

定数 意味
NSTCS_HASEXPANDOS 展開をするための印を表示する。
NSTCS_SINGLECLICKEXPAND シングルクリックで展開可能にする。
NSTCS_SHOWSELECTIONALWAYS 常にアイテムが選択されているように見せる。フォーカスがない場合は灰色で表示される。
NSTCS_HASLINES アイテム同士を線でつなぐようにする。
NSTCS_FULLROWSELECT アイテムを一行選択できるようにする。
NSTCS_NOINFOTIP アイテムに関連するツールチップを表示しない。
NSTCS_DISABLEDRAGDROP ツリーから外部へのD&Dを禁止する(ただし、外部からツリーへのD&Dは可能)。

NSTCS_HASEXPANDOSを指定しない場合は、マウスによってアイテムの展開できなくなります。 よって、この定数は基本的に指定することになるでしょう。

NameSpaceTreeControlにアイテムを追加するには、AppendRootを呼び出します。

HRESULT INameSpaceTreeControl::AppendRoot(
  IShellItem *psiRoot,
  SHCONTF grfEnumFlags,
  NSTCROOTSTYLE grfRootStyle,
  IShellItemFilter *pif
);

psiRootは、追加したいアイテムを表すIShellItemを指定します。 grfEnumFlagsは、どのようなアイテムを列挙するかを表す定数を指定します。 SHCONTF_FOLDERSを指定すればフォルダが列挙され、 SHCONTF_NONFOLDERSを指定すれば非フォルダが列挙されます。 grfRootStyleは、ルートアイテムのスタイルを表す定数を指定します。 NSTCRS_VISIBLEを指定すればルートアイテムが表示され、 NSTCRS_EXPANDEDを指定すればルートアイテムを展開できるようになります。 NSTCRS_HIDDENを指定するとルートアイテムは表示されず、 下位のアイテムだけが表示されます。 pifは、IShellItemFilterを実装したオブジェクトのアドレスを指定します。 表示するアイテムをフィルタするつもりがない場合は、NULLを指定しても構いません。

NameSpaceTreeControlは、自身に対して発生した操作をホスト側に通知する仕組みも用意しています。 こうした通知を受け取る場合は、TreeAdviseを呼び出します。

HRESULT INameSpaceTreeControl::TreeAdvise(
  IUnknown *punk,
  DWORD *pdwCookie
);

punkは、通知を受け取りたいオブジェクトのアドレスを指定します。 pdwCookieは、登録を識別する値を受け取る変数のアドレスを指定します。 この値は登録を解除する際に必要になります。

TreeAdviseに指定するオブジェクトの型は、 NameSpaceTreeControlが理解できるインターフェースを継承したクラスでなければなりません。 たとえば、NameSpaceTreeControlは、カスタムドローの通知をINameSpaceTreeControlCustomDrawで行うようにしていますが、 これをオブジェクトが受け取りたいのであれば、 オブジェクトのクラスはINameSpaceTreeControlCustomDrawを継承しなければなりません。 カスタムドローに関する通知を受けるようになれば、アイテムの既定の描画を変更できるようになります。

今回のプログラムは、NameSpaceTreeControlをウインドウに表示します。 また、カスタムドローに関する通知を受け取って、一部のアイテムのテキスト色を変更します。

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

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

class CNameSpaceTreeHost : public INameSpaceTreeControlCustomDraw
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP PrePaint(HDC hdc, RECT *prc, LRESULT *plres);
	STDMETHODIMP PostPaint(HDC hdc, RECT *prc);
	STDMETHODIMP ItemPrePaint(HDC hdc, RECT *prc, NSTCCUSTOMDRAW *pnstccdItem, COLORREF *pclrText, COLORREF *pclrTextBk, LRESULT *plres);
	STDMETHODIMP ItemPostPaint(HDC hdc, RECT *prc, NSTCCUSTOMDRAW *pnstccdItem);
	
	CNameSpaceTreeHost();
	~CNameSpaceTreeHost();
	int Run(HINSTANCE hinst, int nCmdShow);
	LRESULT WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
	BOOL Create();

private:
	LONG                  m_cRef;
	HWND                  m_hwnd;
	DWORD                 m_dwCookie;
	INameSpaceTreeControl *m_pNameSpaceTreeControl;
};

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

CNameSpaceTreeHost *g_pNameSpaceTreeHost = NULL;

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	int nResult = 0;

	OleInitialize(NULL);
	
	g_pNameSpaceTreeHost = new CNameSpaceTreeHost;
	if (g_pNameSpaceTreeHost != NULL) {
		nResult = g_pNameSpaceTreeHost->Run(hinst, nCmdShow);
		g_pNameSpaceTreeHost->Release();
	}

	OleUninitialize();

	return nResult;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	return g_pNameSpaceTreeHost->WindowProc(hwnd, uMsg, wParam, lParam);
}


// CNameSpaceTreeHost


CNameSpaceTreeHost::CNameSpaceTreeHost()
{
	m_cRef = 1;
	m_hwnd = NULL;
	m_dwCookie = 0;
	m_pNameSpaceTreeControl = NULL;
}

CNameSpaceTreeHost::~CNameSpaceTreeHost()
{
}

STDMETHODIMP CNameSpaceTreeHost::QueryInterface(REFIID riid, void **ppvObject)
{
	*ppvObject = NULL;

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_INameSpaceTreeControlCustomDraw))
		*ppvObject = static_cast<INameSpaceTreeControlCustomDraw *>(this);
	else 
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

STDMETHODIMP_(ULONG) CNameSpaceTreeHost::AddRef()
{
	return InterlockedIncrement(&m_cRef);
}

STDMETHODIMP_(ULONG) CNameSpaceTreeHost::Release()
{
	if (InterlockedDecrement(&m_cRef) == 0) {
		delete this;
		return 0;
	}

	return m_cRef;
}

STDMETHODIMP CNameSpaceTreeHost::PrePaint(HDC hdc, RECT *prc, LRESULT *plres)
{
	*plres = CDRF_NOTIFYITEMDRAW;

	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::PostPaint(HDC hdc, RECT *prc)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::ItemPrePaint(HDC hdc, RECT *prc, NSTCCUSTOMDRAW *pnstccdItem, COLORREF *pclrText, COLORREF *pclrTextBk, LRESULT *plres)
{
	SFGAOF attributes = 0;

	pnstccdItem->psi->GetAttributes(SFGAO_FILESYSTEM, &attributes);
	if (!(attributes & SFGAO_FILESYSTEM)) {
		*pclrText = RGB(255, 0, 0);
		if (pnstccdItem->uItemState & CDIS_FOCUS)
			*pclrTextBk = RGB(128, 0, 0);
		*plres = CDRF_NEWFONT;
	}
	else
		*plres = CDRF_DODEFAULT;

	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::ItemPostPaint(HDC hdc, RECT *prc, NSTCCUSTOMDRAW *pnstccdItem)
{
	return S_OK;
}

int CNameSpaceTreeHost::Run(HINSTANCE hinst, 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 CNameSpaceTreeHost::WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_CREATE:
		m_hwnd = hwnd;
		if (!Create())
			return -1;
		return 0;
	
	case WM_SIZE: {
		HWND hwndTree;
		IUnknown_GetWindow(m_pNameSpaceTreeControl, &hwndTree);
		MoveWindow(hwndTree, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;
	}

	case WM_DESTROY:
		if (m_pNameSpaceTreeControl != NULL) {
			if (m_dwCookie != 0)
				m_pNameSpaceTreeControl->TreeUnadvise(m_dwCookie);
			m_pNameSpaceTreeControl->Release();
		}
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

BOOL CNameSpaceTreeHost::Create()
{
	RECT       rc;
	HRESULT    hr;
	IShellItem *pShellItem;

	hr = CoCreateInstance(CLSID_NamespaceTreeControl, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_pNameSpaceTreeControl));
	if (FAILED(hr))
		return FALSE;

	SetRectEmpty(&rc);
	hr = m_pNameSpaceTreeControl->Initialize(m_hwnd, &rc, NSTCS_HASEXPANDOS | NSTCS_SHOWSELECTIONALWAYS);
	if (FAILED(hr))
		return FALSE;
	
	SHCreateItemInKnownFolder(FOLDERID_Desktop, 0, NULL, IID_PPV_ARGS(&pShellItem));
	m_pNameSpaceTreeControl->AppendRoot(pShellItem, SHCONTF_FOLDERS, NSTCRS_VISIBLE | NSTCRS_EXPANDED, NULL);
	pShellItem->Release();

	m_pNameSpaceTreeControl->TreeAdvise(static_cast<INameSpaceTreeControlCustomDraw *>(this), &m_dwCookie);

	return TRUE;
}

CNameSpaceTreeHostというクラスを定義しているのは、 NameSpaceTreeControlからの通知を受け取るためです。 このため、CNameSpaceTreeHostはINameSpaceTreeControlCustomDrawを継承しています。 クラス名にHostという単語を含んでいるのは、 このクラスがメンバとしてINameSpaceTreeControlを持っており、 これを通じてNameSpaceTreeControlを管理するからです。 WinMainやWindowProcは、CNameSpaceTreeHostへ処理を渡すことを目的とし、 実質的な作業はCNameSpaceTreeHostの中で行うようにしています。

WindowProcのWM_CREATEでは、Createという自作メソッドを呼び出しています。 このメソッドでは、CoCreateInstanceを呼び出してINameSpaceTreeControlを取得し、 必要な初期化を済まします。

BOOL CNameSpaceTreeHost::Create()
{
	RECT       rc;
	HRESULT    hr;
	IShellItem *pShellItem;

	hr = CoCreateInstance(CLSID_NamespaceTreeControl, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_pNameSpaceTreeControl));
	if (FAILED(hr))
		return FALSE;

	SetRectEmpty(&rc);
	hr = m_pNameSpaceTreeControl->Initialize(m_hwnd, &rc, NSTCS_HASEXPANDOS | NSTCS_SHOWSELECTIONALWAYS);
	if (FAILED(hr))
		return FALSE;
	
	SHCreateItemInKnownFolder(FOLDERID_Desktop, 0, NULL, IID_PPV_ARGS(&pShellItem));
	m_pNameSpaceTreeControl->AppendRoot(pShellItem, SHCONTF_FOLDERS, NSTCRS_VISIBLE | NSTCRS_EXPANDED, NULL);
	pShellItem->Release();

	m_pNameSpaceTreeControl->TreeAdvise(static_cast<INameSpaceTreeControlCustomDraw *>(this), &m_dwCookie);

	return TRUE;
}

まず、CoCreateInstanceを呼び出してINameSpaceTreeControlを取得し、 Initializeを呼び出してNameSpaceTreeControlを初期化します。 第2引数の位置についてはSetRectEmptyで0に初期化していますが、 WM_SIZEで調整することになっているので問題はありません。 NameSpaceTreeControlにアイテムを追加するためにはIShellItemが必要になるため、 SHCreateItemInKnownFolderでデスクトップのIShellItemを取得しています。 そしてこれをAppendRootの第1引数に指定すれば、デスクトップのアイテムが追加されることになります。 第2引数にSHCONTF_FOLDERSを指定していることから、展開時に列挙されるのはフォルダだけですが、 SHCONTF_NONFOLDERSを合わせて指定すればファイルも列挙されるようになります。 TreeAdviseでは、INameSpaceTreeControlCustomDrawを実装したオブジェクトを登録しています。 このオブジェクトはCNameSpaceTreeHost自身であるため、thisポインタを指定するようにしています。 第2引数に返される値は登録を解除する際に必要になるため、メンバ変数に保存するようにしています。 登録の解除は、WM_DESTROYのTreeUnadviseで行われます。

WM_SIZEでは、NameSpaceTreeControlのサイズ調整を行っています。

case WM_SIZE: {
	HWND hwndTree;
	IUnknown_GetWindow(m_pNameSpaceTreeControl, &hwndTree);
	MoveWindow(hwndTree, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
	return 0;
}

INameSpaceTreeControlにはサイズ調整を行うメソッドはありませんが、 幸いなことにオブジェクトはIOleWindowを実装しています。 よって、IOleWindow::GetWindowでウインドウハンドルを取得し、 それをMoveWindowに指定する方法が成立します。 IUnknown_GetWindowを呼び出せば、IOleWindowの取得からGetWindowの呼び出しまでを一度に行えます。

TreeAdviseによってINameSpaceTreeControlCustomDrawを実装したオブジェクトを登録したため、 オブジェクトはカスタムドローに関する通知を受け取ることができます。 具体的には、INameSpaceTreeControlCustomDrawの4つのメソッドが呼ばれることになります。

STDMETHODIMP CNameSpaceTreeHost::PrePaint(HDC hdc, RECT *prc, LRESULT *plres)
{
	*plres = CDRF_NOTIFYITEMDRAW;

	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::PostPaint(HDC hdc, RECT *prc)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::ItemPrePaint(HDC hdc, RECT *prc, NSTCCUSTOMDRAW *pnstccdItem, COLORREF *pclrText, COLORREF *pclrTextBk, LRESULT *plres)
{
	SFGAOF attributes = 0;

	pnstccdItem->psi->GetAttributes(SFGAO_FILESYSTEM, &attributes);
	if (!(attributes & SFGAO_FILESYSTEM)) {
		*pclrText = RGB(255, 0, 0);
		if (pnstccdItem->uItemState & CDIS_FOCUS)
			*pclrTextBk = RGB(128, 0, 0);
		*plres = CDRF_NEWFONT;
	}
	else
		*plres = CDRF_DODEFAULT;

	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::ItemPostPaint(HDC hdc, RECT *prc, NSTCCUSTOMDRAW *pnstccdItem)
{
	return S_OK;
}

PrePaintはNameSpaceTreeControlが描画される前に呼ばれ、 PostPaintはNameSpaceTreeControlが描画された後に呼ばれます。 これらのメソッドにはデバイスコンテキストのハンドルが送られるため、 アプリケーションは独自の描画を行うことができますが、 通常はNameSpaceTreeControl自体ではなく、NameSpaceTreeControlのアイテムを独自に描画したいはずです。 よって、アイテムの描画に関する通知を受け取るために、PrePaintではplresにCDRF_NOTIFYITEMDRAWを指定しています。 アイテムの描画に関する通知が不要な場合はCDRF_DODEFAULTを指定しますが、 そうであれば最初からTreeAdviseでオブジェクトを登録しないようにしたほうがよいでしょう。 ItemPrePaintはアイテムが描画される前に呼ばれ、 ItemPostPaintはアイテムが描画された後に呼ばれます。 ItemPrePaintでは、pclrTextにアイテムのテキスト色及びpclrTextBkにアイテムの背景色を指定できるため、 一部のアイテムの場合でこれを試しています。 ここで言う一部のアイテムとは、属性としてSFGAO_FILESYSTEMを含まないアイテムのことであり、 コントロールパネルなどの仮想フォルダが該当することになります。 アイテムの属性を取得するには、NSTCCUSTOMDRAW構造体のpsiから参照できるIShellItemのGetAttributesを呼び出せばよいでしょう。 背景色の変更はアイテムにフォーカスが当たっているときを条件とするため、 アイテムの状態を表すuItemStateにCDIS_FOCUSが含まれているかを確認しています。 pclrTextやpclrTextBkを変更した場合は、CDRF_NEWFONTを返すようにします。

IShellItemFilterについて

ルートアイテムを追加するAppendRootには、 IShellItemFilterを実装したオブジェクトのアドレスを指定することができます。 このようにした場合、NameSpaceTreeControlにアイテムが追加されていく度に通知を受け取ることができるため、 任意のアイテムの追加を拒否することができます。 今回のCNameSpaceTreeHostがIShellItemFilterを継承したとするならば、 QueryInterfaceの実装は次のようになるでしょう。

STDMETHODIMP CNameSpaceTreeHost::QueryInterface(REFIID riid, void **ppvObject)
{
	*ppvObject = NULL;

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_INameSpaceTreeControlCustomDraw))
		*ppvObject = static_cast<INameSpaceTreeControlCustomDraw *>(this);
	else if (IsEqualIID(riid, IID_IShellItemFilter))
		*ppvObject = static_cast<IShellItemFilter *>(this);
	else 
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

IID_IShellItemFilterが送られた場合は、上記のようにオブジェクトのアドレスを返すようにします。 当然ながら、QueryInterfaceにIID_IShellItemFilterが送られるためには、 AppendRootにオブジェクトのアドレスを指定しておく必要があります。

m_pNameSpaceTreeControl->AppendRoot(pShellItem, SHCONTF_FOLDERS, NSTCRS_VISIBLE | NSTCRS_EXPANDED, static_cast<IShellItemFilter *>(this));

上記のように、第4引数にはオブジェクトのアドレスを指定するようにします。 これにより、アイテムが追加される段階になるとIShellItemFilter::IncludeItemが呼ばれるようになります。

STDMETHODIMP CNameSpaceTreeHost::IncludeItem(IShellItem *psi)
{
	LPWSTR lpszDisplayName;

	psi->GetDisplayName(SIGDN_NORMALDISPLAY, &lpszDisplayName);

	if (StrCmpW(lpszDisplayName, L"コントロール パネル") == 0) {
		CoTaskMemFree(lpszDisplayName);
		return S_FALSE;
	}

	CoTaskMemFree(lpszDisplayName);

	return S_OK;
}

IncludeItemでS_OKを返すと第1引数のアイテムは追加され、それ以外の値を返すとアイテムは追加されないようになります。 上記では、コントロールパネルのアイテムを追加しないようにするため、 IShellItem::GetDisplayNameでアイテムの名前を取得し、 それがコントロールパネルの名前と一致するかを調べています。

IShellItemFilterには、GetEnumFlagsForItemというメソッドもあります。 このメソッドでは、どのようなアイテムを列挙したいのかを表す定数を返すことができ、 列挙の方針をルートアイテムと異なるようにした場合は処理することになります。

STDMETHODIMP CNameSpaceTreeHost::GetEnumFlagsForItem(IShellItem *psi, SHCONTF *pgrfFlags)
{
	LPWSTR lpszDisplayName;

	psi->GetDisplayName(SIGDN_NORMALDISPLAY, &lpszDisplayName);

	if (StrCmpW(lpszDisplayName, L"デスクトップ") != 0) {
		*pgrfFlags = SHCONTF_NONFOLDERS | SHCONTF_FOLDERS;
		CoTaskMemFree(lpszDisplayName);
		return S_OK;
	}

	CoTaskMemFree(lpszDisplayName);

	return S_OK;
}

まず、AppendRootにSHCONTF_FOLDERSを指定していたことを思い出してください。 これにより、ツリー上にはファイルが列挙されないことになりますが、 アイテム毎に列挙方法を格納した場合はその列挙方法がアイテムに適応されることになります。 つまり、AppendRootに指定したSHCONTF_FOLDERSが全てのアイテムに適応されなくなります。 上記の場合では、デスクトップ以外のアイテムに対してSHCONTF_NONFOLDERS | SHCONTF_FOLDERSを指定しているため、 デスクトップ直下のアイテムに関してはフォルダで構成されることになりますが、 そのフォルダの下にはフォルダもファイルも列挙されるようになります。 AppendRootに指定した列挙方法が全てのアイテムに適応されることを望む場合は、 単純にS_OK(他でも可)を返すだけで構いません。



戻る