第57回 IShellMenuにより「お気に入り」を表示する (タブブラウザーを作る)

tabbrowser258.gif
今回は「お気に入り」の表示方法を一新する。

これまでは「お気に入り」フォルダから保存されているインターネットショートカットを列挙して独自にメニューを生成/表示していた。これをInternet Explorerの「お気に入り」や「スタート」メニューと同じようにシェルメニュー(IShellMenu)ベースのものに変える。

Windows Vista以降であればITrackShellMenuを利用することで非常に簡単にIShellMenuをメニューとして表示できる。しかし2009年4月現在Windows Vistaはまだまだ普及率が悪い。そのためITrackShellMenuは使わずに力技でIShellMenuを表示することにした。

CMainFrameにIShellMenuを利用した「お気に入り」表示関数を追加する。

	CComPtr<IMenuBand>	_pIMenuBand;


	bool	PopupMenu(int nX,int nY)
	{
		LPITEMIDLIST	pidl;

		CComPtr<IShellFolder>		pIShellFolder;

		if(_pIMenuBand)
		{
			//すでにメニューが開いていたら、まずはそのメニューを閉じる

			CComPtr<IOleCommandTarget>	pIOleCommandTarget;

			_pIMenuBand->QueryInterface(&pIOleCommandTarget);
			if(pIOleCommandTarget)
				pIOleCommandTarget->Exec(&CLSID_MenuBand,22,0,NULL,NULL);
		}


		//「お気に入り」フォルダのPIDLとIShellFolderを取得
		{
			CComPtr<IShellFolder>	pIShellFolderDesktop;

			::SHGetDesktopFolder(&pIShellFolderDesktop);
			if(pIShellFolderDesktop == NULL)
				return	false;
			::SHGetSpecialFolderLocation(NULL,CSIDL_FAVORITES,&pidl);
			pIShellFolderDesktop->BindToObject(pidl,NULL,IID_IShellFolder,(void **)&pIShellFolder);
		}



		CComPtr<IDeskBand>	pIDeskBand;

		//PIDLとIShellFolderの示すIShellMenu(IDeskBand)を取得
		{
			HRESULT		hr;
			CRegKey		cRegOrder;
			CComPtr<IShellMenu>	pIShellMenu;
			CComPtr<IDeskBar>	pIDeskBar;
			CComPtr<IMenuPopup>	pIMenuPopup;

			//並び順用レジストリ。必要ならCreateしてもいいが今回は「お気に入り」のみなのでopen
			cRegOrder.Open(HKEY_CURRENT_USER,_T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MenuOrder\\Favorites"));

			::CoCreateInstance(CLSID_MenuBand,NULL,CLSCTX_INPROC_SERVER,IID_IShellMenu,(void**)&pIShellMenu);

			if(pIShellMenu)
			{
				//とりあえずCallback指定していない
				pIShellMenu->Initialize(NULL,-1,ANCESTORDEFAULT,SMINIT_TOPLEVEL | SMINIT_VERTICAL);

				#ifndef	SMSET_USEBKICONEXTRACTION
					#define SMSET_USEBKICONEXTRACTION	0x00000008
				#endif

				//メニューへのフォルダ割り当て
				hr = pIShellMenu->SetShellFolder(pIShellFolder,pidl,cRegOrder.m_hKey,SMSET_BOTTOM | SMSET_USEBKICONEXTRACTION);	//以下も指定したいが値が不明 | SMSET_HASEXPANDABLEFOLDERS)

				if(SUCCEEDED(hr))
					cRegOrder.Detach();	//レジストリのCloseはIShellMenuに任せる
				ILFree(pidl);
				pidl = NULL;
			}

			if(pIShellMenu)
				pIShellMenu->QueryInterface(&pIMenuPopup);
			if(pIMenuPopup)
				pIMenuPopup->QueryInterface(&pIDeskBar);
			if(pIDeskBar)
				pIDeskBar->QueryInterface(&pIDeskBand);
			if(pIDeskBand == NULL)
				return	false;
		}


		//IShellMenu(IDeskBand)をポップアップメニューで表示
		{
			POINTL	ptl;
			GUID	rclsid;
			CComPtr<IUnknown>			pIUnknown;
			CComPtr<IBandSite>			pIBandSite;
			CComPtr<IMenuPopup>			pIMenuPopup;

			//メニュー用のDeskBar「Menu Desk Bar」
			::CLSIDFromString(L"{ECD4FC4F-521C-11D0-B792-00A0C90312E1}",&rclsid);
			::CoCreateInstance(rclsid,NULL,CLSCTX_INPROC_SERVER,IID_IUnknown,(void**)&pIUnknown);

			//メニュー用のBandSite
			::CoCreateInstance(CLSID_MenuBandSite,NULL,CLSCTX_INPROC_SERVER,IID_IBandSite,(void**)&pIBandSite);

			if(pIUnknown)
				pIUnknown->QueryInterface(&pIMenuPopup);	//メニュー用DeskBarだからIMenuPopupを持っている
			if(pIMenuPopup)
				pIMenuPopup->SetClient(pIBandSite);			//DeskBarにIBandSiteを割り当て
			if(pIBandSite)
				pIBandSite->AddBand(pIDeskBand);			//IBandSiteに表示したいIShellMenuを割り当てる

			//位置を指定してメニュー表示
			ptl.x = nX;
			ptl.y = nY;
			if(pIMenuPopup)
				pIMenuPopup->Popup(&ptl,NULL,MPPF_POS_MASK | MPPF_FORCEZORDER | MPPF_RIGHT | MPPF_BOTTOM);
		}


		//メニューの_pIMenuBandを保存
		{
			_pIMenuBand = NULL;
			pIDeskBand->QueryInterface(&_pIMenuBand);
			if(_pIMenuBand == NULL)
				return	false;
		}

		return	true;
	}

tabbrowser259.gif そしてメッセージハンドラに「お気に入り」メニューを表示するための処理を入れる。今回はWM_MENUSELECTが届いたときに表示することにした。
		COMMAND_ID_HANDLER(ID_IE_ADDFAVORITE, OnIEAddFavorite)
		COMMAND_ID_HANDLER(ID_IE_ORGANIZE_FAVORITE, OnIEOrganizeFavorite)
		NOTIFY_CODE_HANDLER(TBVN_CONTEXTMENU, OnTabContextMenu)
		NOTIFY_CODE_HANDLER(TBVN_PAGEACTIVATED, OnTabPageActivated)
		MESSAGE_HANDLER(WM_MENUSELECT, OnMenuSelectForPopup)		//■追加
//		MESSAGE_HANDLER(WM_MENUSELECT, OnMenuSelect)		//■削除
		MESSAGE_HANDLER(WM_PARENTNOTIFY, OnParentNotify)
		COMMAND_RANGE_HANDLER(ID_FAVORITE_FIRST,ID_FAVORITE_LAST,OnFavorite)
		CHAIN_MSG_MAP_MEMBER(_cFovMenu)
		CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)
		CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)
	END_MSG_MAP()


	//■追加
	LRESULT OnMenuSelectForPopup(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
	{
		bHandled = FALSE;

		UINT	nMenuItem = LOWORD(wParam);
		HMENU	hMenu = (HMENU)lParam;

		if(HIWORD(wParam) & MF_POPUP)
			return	0;
		if(nMenuItem != 4)							//メニュー位置
			return	0;

		RECT	rect;

		m_CmdBar.GetItemRect(nMenuItem,&rect);
		m_CmdBar.MapWindowPoints(NULL,&rect);

		PopupMenu(rect.left,rect.bottom);

		return	0;
	}

tabbrowser262.gif もともとメニューリソースに用意されているメニューが表示されてしまわないように、CCommandBarCtrlのメニュー表示部分を上書きする。
//■追加
class	CCommandBarCtrl2	: public CCommandBarCtrlImpl<CCommandBarCtrl2>
{
public:
	BEGIN_MSG_MAP(CCommandBarCtrl2)
			CHAIN_MSG_MAP(__super)
		ALT_MSG_MAP(1)   // Parent window messages
			CHAIN_MSG_MAP_ALT(__super,1)
		ALT_MSG_MAP(2)   // MDI client window messages
			CHAIN_MSG_MAP_ALT(__super,2)
		ALT_MSG_MAP(3)   // Message hook messages
			CHAIN_MSG_MAP_ALT(__super,3)
	END_MSG_MAP()


	void DoPopupMenu(int nIndex, bool bAnimate)
	{
		if(nIndex == 4)		//メニュー位置固定
		{
			HWND	hWnd = ::GetAncestor(m_hWnd,GA_ROOT);
			::SendMessage(hWnd,WM_MENUSELECT,MAKEWPARAM(nIndex,MF_HILITE),(LPARAM)GetMenu().m_hMenu);
			return;
		}

		__super::DoPopupMenu(nIndex,bAnimate);
	}
};


class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>,
		public CMessageFilter, public CIdleHandler
{
	CAddressBarCtrl	_wndAddressBar;			// アドレスバー用コンボボックス


public:
	DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)

	CTabView m_view;
	CCommandBarCtrl2 m_CmdBar;

tabbrowser260.gif さらにIShellMenuがきちんとメッセージを処理できるようにIMenuBand::TranslateMenuMessageを呼び出す処理を追加する。
	virtual BOOL PreTranslateMessage(MSG* pMsg)
	{
		//■追加
		if(TranslatePopupMessage(pMsg->hwnd,pMsg->message,pMsg->wParam,pMsg->lParam,pMsg))
			return	TRUE;

		if(_wndAddressBar.IsChild(::GetFocus()) == FALSE)		//アドレスバーにフォーカスがあるときはPreTranslateMessageを回さない(アドレスバーでCtrl+Cなどを使えるようにするため)
		{
			if(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))
				return TRUE;
		}

		return m_view.PreTranslateMessage(pMsg);
	}



	//■追加
	//
	//ポップアップメニューのメッセージ処理
	//
	//pMsg == NULLのときは開いているメニューを閉じるべきかどうかのチェックも併せておこなう
	//(PreTranslateMessageではpMsgも指定する)
	//
	bool	TranslatePopupMessage(HWND hWnd,UINT uMsg, WPARAM wParam, LPARAM lParam,MSG* pMsg=NULL)
	{
		if(_pIMenuBand == NULL)
			return	false;

		HRESULT	hr;
		LRESULT	ret = 0;
		MSG		msg;

		if(pMsg == NULL)
		{
			::ZeroMemory(&msg,sizeof(MSG));
			msg.hwnd = hWnd;
			msg.message = uMsg;
			msg.wParam = wParam;
			msg.lParam = lParam;
		}
		else
		{
			msg = *pMsg;
		}

		hr = _pIMenuBand->IsMenuMessage(&msg);
		if(hr == S_OK)
			hr = _pIMenuBand->TranslateMenuMessage(&msg,&ret);
		if(hr == S_OK)
			return	true;

		//メニューを閉じるべきかどうかのチェック
		//もう少しチェック項目を増やした方がいい。。。
		if(pMsg == NULL
			&& (hr == E_FAIL
			|| (uMsg >= WM_MOUSEFIRST && uMsg <= WM_MOUSELAST)
			|| uMsg == WM_INITMENUPOPUP
			|| uMsg == WM_SIZE
			|| uMsg == WM_MOVE
			|| uMsg == WM_MOVING
			|| uMsg == WM_INITMENU
			|| 0))
		{
			//メニューを閉じる

			CComPtr<IOleCommandTarget>	pIOleCommandTarget;

			_pIMenuBand->QueryInterface(&pIOleCommandTarget);
			if(pIOleCommandTarget)
				pIOleCommandTarget->Exec(&CLSID_MenuBand,22,0,NULL,NULL);
		}

		return	false;
	}

tabbrowser261.gif ちょっと汚い実装方法だが、メッセージマップの中からもIMenuBandへメッセージが渡るようにする。
	BEGIN_MSG_MAP(CMainFrame)
		//■追加
		{
			//「お気に入り」メニュー用TranslateMessage
			if(TranslatePopupMessage(hWnd,uMsg,wParam,lParam))
				return	TRUE;
		}
		MESSAGE_HANDLER(WM_DNP_CREATENEWTAB, OnDnpCreateNewTab)
		MESSAGE_HANDLER(WM_DNP_CHANGEFOCUS, OnDnpChangeFocus)
		MESSAGE_HANDLER(WM_DNP_CHANGEADDRESS, OnDnpChangeAddress)
		NOTIFY_CODE_HANDLER(CBEN_ENDEDIT, OnAddressbarEnter)
		MESSAGE_HANDLER(WM_CREATE, OnCreate)

tabbrowser263.gif
これでビルド/実行するとIShellMenuにより「お気に入り」が表示されるようになった。

まだ実装が不十分なため「お気に入り」メニューが開いているのにコマンドバーの「お気に入り」がハイライト状態になっていない、ドラッグアンドドロップによる「お気に入り」の移動ができない、メニュー表示がクラシックスタイルなどなど機能が足りない。

しかし「お気に入り」の並び順がInternet Explorerでの表示と同じだったり、アイコンの読み込みがバックグラウンドで行われる、TIPSが表示されるなど「お気に入り」メニューとしてそこそこ使える状態になっている。

次回は選択した「お気に入り」のURLをタブとして開くようにする。

プロジェクトファイルをダウンロード


カテゴリー「タブブラウザーを作る」 のエントリー