EternalWindows
WebBrowser コントロール / お気に入りの実装

今回は、サイドバーに表示されているお気に入りのウインドウを実装します。 お気に入りのウインドウは次のような外観を持ちます。

目的のアイテムを選択したら、そのアイテムに関連するページが表示されます。 また、右クリックメニューから「新しいタブで開く」を選択した場合は、 新しいタブが作成されてそこにページが表示されます。 詳しい原因は分かりませんが、ページをお気に入りに追加する際に多少の時間が掛かるようです。

お気に入りのウインドウはCFavoriteTreeで識別され、CSidebarによって使用されます。 CFavoriteTree::Createでは、お気に入りのウインドウを作成するための処理を行います。

BOOL CFavoriteTree::Create(HWND hwndParent)
{
	HRESULT    hr;
	RECT       rc;
	IShellItem *psi;
	
	hr = CoCreateInstance(CLSID_NamespaceTreeControl, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_pNameSpaceTreeControl));
	if (FAILED(hr))
		return FALSE;

	SetRectEmpty(&rc);
	hr = m_pNameSpaceTreeControl->Initialize(hwndParent, &rc, NSTCS_SHOWSELECTIONALWAYS | NSTCS_FAVORITESMODE);
	if (FAILED(hr))
		return FALSE;
	
	SHCreateItemInKnownFolder(FOLDERID_Favorites, 0, NULL, IID_PPV_ARGS(&psi));
	m_pNameSpaceTreeControl->AppendRoot(psi, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, NSTCRS_HIDDEN, NULL);
	psi->Release();

	m_pNameSpaceTreeControl->TreeAdvise(static_cast<INameSpaceTreeControlEvents *>(this), &m_dwCookie);
	
	IUnknown_GetWindow(m_pNameSpaceTreeControl, &m_hwndFavorite);

	return TRUE;
}

お気に入りのウインドウを表示するには、Windows Vistaから登場したNamespaceTreeControlを使用するのが簡単です。 このオブジェクトを使用すれば、ツリービューの作成やD&Dの実装、ファイルシステムとの同期などがアプリケーションから隠蔽されます。 また、Initializeの第3引数にNSTCS_FAVORITESMODEを指定することで、IEの順番通りの並び替えを実現できます。 AppendRootの第1引数はルートとして追加するフォルダであり、 これはSHCreateItemInKnownFolderで作成しています。 このとき、フォルダがお気に入りになるようにFOLDERID_Favoritesを指定します。 AppendRootの第2引数にSHCONTF_FOLDERS | SHCONTF_NONFOLDERSを指定した場合はフォルダとファイルが列挙され、 第3引数にNSTCRS_HIDDENを指定した場合は、ルートアイテム(今回の場合お気に入りフォルダ)が表示されなくなります。 TreeAdviseを呼び出しているのは、NamespaceTreeControlで発生したイベントを受け取るためです。 このため、CFavoriteTreeはINameSpaceTreeControlEventsを実装しています。 NamespaceTreeControlのウインドウはサイズ調整の際などで必要になるため、 IUnknown_GetWindowで取得しておきます。

NamespaceTreeControlにNSTCS_FAVORITESMODEを指定すれば、お気に入りの外観として問題ないものになりますが、 機能面としては十分であるといえません。 既定ではアイテムをクリックしても何も反応しないため、該当ページにアクセスするよう実装しなければなりません。

STDMETHODIMP CFavoriteTree::OnItemClick(IShellItem *psi, NSTCEHITTEST nstceHitTest, NSTCSTYLE nsctsFlags)
{
	if (IsFolder(psi))
		return S_FALSE;

	NavigateFromShortcut(psi, FALSE);

	return S_OK;
}

NavigateFromShortcutという自作メソッドは、引数のIShellItemから関連するURLショートカットを取得し、そこにアクセスします。 このメソッドの内部については、メニューの実装を取り上げた際に説明しています。 クリックされたアイテムがフォルダである場合はページにアクセスする必要がないため、 IsFolderという自作メソッドでそれを確認しています。

アイテムはEnterキーの押下で選択される場合もあるため、キーの検出も行う必要があります。

STDMETHODIMP CFavoriteTree::OnKeyboardInput(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	HRESULT         hr;
	IShellItem      *psi;
	IShellItemArray *psiItems;
	
	if (uMsg != WM_KEYDOWN || wParam != VK_RETURN)
		return S_FALSE;
	
	hr = m_pNameSpaceTreeControl->GetSelectedItems(&psiItems);
	if (hr != S_OK)
		return S_FALSE;
	
	psiItems->GetItemAt(0, &psi);
		
	if (IsFolder(psi)) {
		psi->Release();
		psiItems->Release();
		return S_FALSE;
	}
	
	NavigateFromShortcut(psi, FALSE);

	psi->Release();
	psiItems->Release();

	return S_OK;
}

どのアイテムに対してキーが押下されたのかを確認するために、 現在選択されているアイテムをGetSelectedItemsで取得します。 選択されているアイテムは複数存在する可能性がありますが、 GetItemAtに0を指定すればその最初のアイテムを取得できます。 後はこれをNavigateFromShortcutに指定すれば、ページにアクセスできます。

今回の実装では、アイテム上で右クリックした際に表示されるコンテキストメニューに、 「新しいタブで開く」という独自の項目を追加しています。 このような事を実現するためには、IContextMenuを実装したオブジェクトを作成し、 それをINamespaceTreeControl::OnAfterContextMenuで返すようにします。

STDMETHODIMP CFavoriteTree::OnAfterContextMenu(IShellItem *psi, IContextMenu *pcmIn, REFIID riid, void **ppv)
{
	if (IsFolder(psi)) {
		*ppv = NULL;
		return E_NOTIMPL;
	}
	
	*ppv = new CContextMenu(pcmIn, psi);

	return S_OK;
}

CContextMenuがIContextMenuを実装したオブジェクトになります。 このオブジェクトは、既定のコンテキストメニューに独自の項目を追加することになるため、 既定のコンテキストメニューを表すpcmInを渡しておくようにします。 また、選択されたアイテムを識別するpsiも渡しておきます。 選択されたアイテムがフォルダである場合は、独自のオブジェクトを返す必要はありません。

OnAfterContextMenuでIContextMenuを取得したNamespaceTreeControlは、 IContextMenu::QueryContextMenuを呼び出します。 ここでは、引数として渡されたhmenuにメニュー項目を追加することになります。

STDMETHODIMP CContextMenu::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
	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_TABOPEN;
	mii.dwTypeData = TEXT("新しいタブで開く");
	
	InsertMenuItem(hmenu, 1, TRUE, &mii);

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

uFlagsにCMF_DEFAULTONLYが含まれている場合は、項目を追加する必要はありません。 m_pDefContextMenu2は既定のコンテキストメニューであり、 これのQueryContextMenuを呼び出せば既定の項目がhmenuに追加されます。 これが終われば、InsertMenuItemを呼び出して独自の項目を追加することになりますが、 このときのIDはidCmdFirstをベースにしておきます。 InsertMenuItemの第3引数にTRUEを指定した場合は、第2引数が項目を追加する位置と解釈されます。

メニュー項目が選択された場合は、IContextMenu::InvokeCommandを呼ばれます。

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

	if (HIWORD(pici->lpVerb) != 0)
		return E_INVALIDARG;
	
	if (uId == ID_TABOPEN)
		NavigateFromShortcut(m_psi, TRUE);
	else
		m_pDefContextMenu2->InvokeCommand(pici);

	return S_OK;
}

pici->lpVerbの上位ワードが0でない場合は、処理を続行する必要はありません。 選択された項目のIDはpici->lpVerbの下位ワードから参照可能であり、 これがID_TABOPENである場合は「新しいタブで開く」が選択されたことを意味します。 それ以外の場合は既定の項目ということで、m_pDefContextMenu2->InvokeCommandを呼び出します。


戻る