EternalWindows
ExplorerBrowser / コンテキストメニューの拡張

前節では、INameSpaceTreeControl::TreeAdviseを呼び出してオブジェクトを登録し、 INameSpaceTreeControlCustomDrawの通知を受け取ることに成功しました。 この仕組みは、TreeAdviseがオブジェクトのQueryInterfaceを呼び出し、 そこにINameSpaceTreeControlCustomDrawのIIDで指定することで実現されていますが、 指定されるIIDはこれ以外にもいくつか存在します。 たとえば、アイテムが選択されたことを通知するINameSpaceTreeControlEventsがあります。 このインターフェースをオブジェクトが実装すれば、 NameSpaceTreeControlの動作を大きく拡張することができます。

今回のプログラムは、INameSpaceTreeControlEventsを実装してコンテキストメニューに関する通知を受け取ります。 そして、コンテキストメニューに独自の項目を追加します。

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

#define ID_ITEM 500

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

class CNameSpaceTreeHost : public INameSpaceTreeControlEvents
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();

	STDMETHODIMP OnItemClick(IShellItem *psi, NSTCEHITTEST nstceHitTest, NSTCSTYLE nsctsFlags);
	STDMETHODIMP OnPropertyItemCommit(IShellItem *psi);
	STDMETHODIMP OnItemStateChanging(IShellItem *psi, NSTCITEMSTATE nstcisMask, NSTCITEMSTATE nstcisState);
	STDMETHODIMP OnItemStateChanged(IShellItem *psi, NSTCITEMSTATE nstcisMask, NSTCITEMSTATE nstcisState);
	STDMETHODIMP OnSelectionChanged(IShellItemArray *psiaSelection);
	STDMETHODIMP OnKeyboardInput(UINT uMsg, WPARAM wParam, LPARAM lParam);
	STDMETHODIMP OnBeforeExpand(IShellItem *psi);
	STDMETHODIMP OnAfterExpand(IShellItem *psi);
	STDMETHODIMP OnBeginLabelEdit(IShellItem *psi);
	STDMETHODIMP OnEndLabelEdit(IShellItem *psi);
	STDMETHODIMP OnGetToolTip(IShellItem *psi, LPWSTR pszTip, int cchTip);
	STDMETHODIMP OnBeforeItemDelete(IShellItem *psi);
	STDMETHODIMP OnItemAdded(IShellItem *psi, BOOL fIsRoot);
	STDMETHODIMP OnItemDeleted(IShellItem *psi, BOOL fIsRoot);
	STDMETHODIMP OnBeforeContextMenu(IShellItem *psi, REFIID riid, void **ppv);
	STDMETHODIMP OnAfterContextMenu(IShellItem *psi, IContextMenu *pcmIn, REFIID riid, void **ppv);
	STDMETHODIMP OnBeforeStateImageChange(IShellItem *psi);
	STDMETHODIMP OnGetDefaultIconIndex(IShellItem *psi, int *piDefaultIcon, int *piOpenIcon);

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

class CContextMenu : public IContextMenu2
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags);
	STDMETHODIMP GetCommandString(UINT_PTR idCmd, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax);
	STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici);

	STDMETHODIMP HandleMenuMsg(UINT uMsg, WPARAM wParam, LPARAM lParam);
	
	CContextMenu(IContextMenu *pDefContextMenu);
	~CContextMenu();

private:
	LONG          m_cRef;
	IContextMenu2 *m_pDefContextMenu2;
};

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_INameSpaceTreeControlEvents))
		*ppvObject = static_cast<INameSpaceTreeControlEvents *>(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::OnItemClick(IShellItem *psi, NSTCEHITTEST nstceHitTest, NSTCSTYLE nsctsFlags)
{
	return S_FALSE;
}

STDMETHODIMP CNameSpaceTreeHost::OnPropertyItemCommit(IShellItem *psi)
{
	return S_FALSE;
}

STDMETHODIMP CNameSpaceTreeHost::OnItemStateChanging(IShellItem *psi, NSTCITEMSTATE nstcisMask, NSTCITEMSTATE nstcisState)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnItemStateChanged(IShellItem *psi, NSTCITEMSTATE nstcisMask, NSTCITEMSTATE nstcisState)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnSelectionChanged(IShellItemArray *psiaSelection)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnKeyboardInput(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	return S_FALSE;
}

STDMETHODIMP CNameSpaceTreeHost::OnBeforeExpand(IShellItem *psi)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnAfterExpand(IShellItem *psi)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnBeginLabelEdit(IShellItem *psi)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnEndLabelEdit(IShellItem *psi)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnGetToolTip(IShellItem *psi, LPWSTR pszTip, int cchTip)
{
	return E_NOTIMPL;
}

STDMETHODIMP CNameSpaceTreeHost::OnBeforeItemDelete(IShellItem *psi)
{
	return E_NOTIMPL;
}

STDMETHODIMP CNameSpaceTreeHost::OnItemAdded(IShellItem *psi, BOOL fIsRoot)
{
	return E_NOTIMPL;
}

STDMETHODIMP CNameSpaceTreeHost::OnItemDeleted(IShellItem *psi, BOOL fIsRoot)
{
	return E_NOTIMPL;
}

STDMETHODIMP CNameSpaceTreeHost::OnBeforeContextMenu(IShellItem *psi, REFIID riid, void **ppv)
{
	*ppv = NULL;

	return E_NOTIMPL;
}

STDMETHODIMP CNameSpaceTreeHost::OnAfterContextMenu(IShellItem *psi, IContextMenu *pcmIn, REFIID riid, void **ppv)
{
	*ppv = new CContextMenu(pcmIn);

	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnBeforeStateImageChange(IShellItem *psi)
{
	return S_OK;
}

STDMETHODIMP CNameSpaceTreeHost::OnGetDefaultIconIndex(IShellItem *psi, int *piDefaultIcon, int *piOpenIcon)
{
	return E_NOTIMPL;
}

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<INameSpaceTreeControlEvents *>(this), &m_dwCookie);

	return TRUE;
}


// CContextMenu


CContextMenu::CContextMenu(IContextMenu *pDefContextMenu)
{
	m_cRef = 1;
	pDefContextMenu->QueryInterface(IID_PPV_ARGS(&m_pDefContextMenu2));
}

CContextMenu::~CContextMenu()
{
	m_pDefContextMenu2->Release();
}

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

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IContextMenu) || IsEqualIID(riid, IID_IContextMenu2))
		*ppvObject = static_cast<IContextMenu *>(this);
	else
		return E_NOINTERFACE;

	AddRef();

	return S_OK;
}

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

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

	return m_cRef;
}

STDMETHODIMP CContextMenu::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
	int          nPos;
	MENUITEMINFO mii;

	if (uFlags & CMF_DEFAULTONLY)
		return MAKE_SCODE(SEVERITY_SUCCESS, FACILITY_NULL, 0);

	m_pDefContextMenu2->QueryContextMenu(hmenu, indexMenu, idCmdFirst, idCmdLast, uFlags);

	mii.cbSize     = sizeof(MENUITEMINFO);
	mii.fMask      = MIIM_ID | MIIM_TYPE;
	mii.fType      = MFT_STRING;
	mii.wID        = idCmdFirst + ID_ITEM;
	mii.dwTypeData = TEXT("独自のアイテム");

	nPos = GetMenuItemCount(hmenu);
	InsertMenuItem(hmenu, nPos, TRUE, &mii);

	return MAKE_SCODE(SEVERITY_SUCCESS, FACILITY_NULL, GetMenuItemCount(hmenu));
}

STDMETHODIMP CContextMenu::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
{
	UINT idCmd = LOWORD(pici->lpVerb);

	if (HIWORD(pici->lpVerb) != 0)
		return E_INVALIDARG;
	
	if (idCmd == ID_ITEM)
		MessageBox(NULL, TEXT("アイテムが選択されました。"), TEXT("OK"), MB_OK);
	else
		m_pDefContextMenu2->InvokeCommand(pici);

	return S_OK;
}

STDMETHODIMP CContextMenu::GetCommandString(UINT_PTR idCmd, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax)
{
	return m_pDefContextMenu2->GetCommandString(idCmd, uFlags, pwReserved, pszName, cchMax);
}

STDMETHODIMP CContextMenu::HandleMenuMsg(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	return m_pDefContextMenu2->HandleMenuMsg(uMsg, wParam, lParam);
}

CNameSpaceTreeHostが実装するメソッドの中で、INameSpaceTreeControlEventsのメソッドはOnItemClickからOnGetDefaultIconIndexまでです。 この中でコンテキストメニューに関するメソッドは次の2つです。

STDMETHODIMP CNameSpaceTreeHost::OnBeforeContextMenu(IShellItem *psi, REFIID riid, void **ppv)
{
	*ppv = NULL;

	return E_NOTIMPL;
}

STDMETHODIMP CNameSpaceTreeHost::OnAfterContextMenu(IShellItem *psi, IContextMenu *pcmIn, REFIID riid, void **ppv)
{
	*ppv = new CContextMenu(pcmIn);

	return S_OK;
}

OnBeforeContextMenuは、コンテキストメニューが表示する前に呼ばれます。 psiはコンテキストメニューの表示対象となるアイテムであり、 riidはIContextMenu(2)のIID、 ppvはIContextMenu(2)を実装したオブジェクトのアドレスを返すことができます。 しかし、ここでオブジェクトのアドレスを返さなくても特に何もないようなので、 ppvにNULLを格納してE_NOTIMPLを返しています。 OnAfterContextMenuは、OnBeforeContextMenuが呼ばれた後に呼ばれます。 ここでE_NOTIMPLを返すと既定のコンテキストメニューが表示されてしまうため、 ppvに独自のオブジェクトのアドレスを格納してS_OKを返すようにします。 OnAfterContextMenuが制御を返すと、実際にコンテキストメニューが表示されます。

OnAfterContextMenuのriidに指定されるのはIContextMenu(2)のIIDであるため、 ppvに指定するオブジェクトはこれらを実装していなければなりません。 よって、CContextMenuというクラスを定義し、これを型としてオブジェクトを作成しています。 CContextMenuのコンストラクタは次のようになっています。

CContextMenu::CContextMenu(IContextMenu *pDefContextMenu)
{
	m_cRef = 1;
	pDefContextMenu->QueryInterface(IID_PPV_ARGS(&m_pDefContextMenu2));
}

第1引数は、既定のコンテキストメニューを表すIContextMenuです。 これは、OnAfterContextMenuの第2引数から取得できます。 今回表示するコンテキストメニューは、既定の項目と独自の項目を合したものにしたいため、 既定のコンテキストメニューを受け取るようにしています。 QueryInterfaceを呼び出してIContextMenu2を取得しているのは、 既定のコンテキストメニューがWM_INITMENUPOPUPなどのメッセージを必要としているからです。 IContextMenu2にはHandleMenuMsgというメソッドが含まれるため、 これを呼び出すことで既定のコンテキストメニューにメッセージを渡すことができます。 独自の項目が選択された場合に対象のフォルダを処理したい場合は、 IShellItemを引数として受け取るようにします。

コンテキストメニューが表示される段階になると、IContextMenu::QueryContextMenuが呼ばれます。 ここでは、第1引数のメニューハンドルに既定の項目と独自の項目を追加しています。

STDMETHODIMP CContextMenu::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
	int          nPos;
	MENUITEMINFO mii;

	if (uFlags & CMF_DEFAULTONLY)
		return MAKE_SCODE(SEVERITY_SUCCESS, FACILITY_NULL, 0);

	m_pDefContextMenu2->QueryContextMenu(hmenu, indexMenu, idCmdFirst, idCmdLast, uFlags);

	mii.cbSize     = sizeof(MENUITEMINFO);
	mii.fMask      = MIIM_ID | MIIM_TYPE;
	mii.fType      = MFT_STRING;
	mii.wID        = idCmdFirst + ID_ITEM;
	mii.dwTypeData = TEXT("独自のアイテム");

	nPos = GetMenuItemCount(hmenu);
	InsertMenuItem(hmenu, nPos, TRUE, &mii);

	return MAKE_SCODE(SEVERITY_SUCCESS, FACILITY_NULL, GetMenuItemCount(hmenu));
}

uFlagsにCMF_DEFAULTONLYが含まれる場合は項目を追加する必要がないので、処理を続行しないようにします。 既定のコンテキストメニューのQueryContextMenuを呼び出せば、hmenuには既定の項目が追加されるようになります。 ここでどれだけの項目が追加されたかは、GetMenuItemCountを呼び出すことで分かります。 独自の項目を追加するにはInsertMenuItemを呼び出しますが、 このときの項目のIDはidCmdFirst + ID_ITEMのようにidCmdFirstをベースにしていなければなりません。 また、IDの値はidCmdLastを超えないようにします。 既定の項目が先に追加されている関係上、既定の項目のIDと独自の項目がIDが重複してはなりませんが、 これを避ける適切な方法がよく分かりません。 既定の項目のIDの値は小さいようなので、できるだけ大きい値を指定すれば安全なのかもしれません。 InsertMenuItemの第3引数にTRUEを指定した場合は、第2引数に追加する項目の位置を指定します。 今回は、最後尾に追加するということでGetMenuItemCountの戻り値を指定しています。 MAKE_SCODEマクロの第3引数はhmenuに追加した項目の数であり、 これはGetMenuItemCountの戻り値を指定します。

QueryContextMenuでメニューハンドルを初期化した呼び出し側は、実際にコンテキストメニューを表示することになります。 そして、項目が選択された場合はInvokeCommandが呼ばれます。

STDMETHODIMP CContextMenu::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
{
	UINT idCmd = LOWORD(pici->lpVerb);

	if (HIWORD(pici->lpVerb) != 0)
		return E_INVALIDARG;
	
	if (idCmd == ID_ITEM)
		MessageBox(NULL, TEXT("アイテムが選択されました。"), TEXT("OK"), MB_OK);
	else
		m_pDefContextMenu2->InvokeCommand(pici);

	return S_OK;
}

pici->lpVerbの上位ワードが0でない場合は、処理を続行する必要はありません。 idCmdは選択された項目のIDであり、これがID_ITEMと一致する場合は独自の処理を行います。 逆に一致しない場合は、既定の項目ということでm_pDefContextMenu2のInvokeCommandを呼び出します。 これにより、既定の処理が実行されることになります。


戻る