關鍵字:“添加到收藏夾”對話框, 模態窗口,IShellUIHelper,DoAddToFavDlg, DoOrganizeFavDlg
1、概述
調用“添加到收藏夾”對話框(如下)與調用“整理收藏夾”對話框有不同之處,前者所做的工作比後者要來得複雜。將鏈接添加到收藏夾除了將鏈接保存之外,還可能會有脫機訪問的設置,從IE 4.0到IE 5.0,處理的方式也發生了一些變化。
2、IShellUIHelper接口
微軟專門提供了一個接口IShellUIHelper來實現對Windows Shell API一些功能的訪問,將鏈接添加到收藏夾也是其中之一,就是下面的AddFavorite函數。
HRESULT IShellUIHelper::AddFavorite(BSTR URL, VARIANT *Title);
實例代碼如下:
void CMyHtmlView::OnAddToFavorites()
{
IShellUIHelper* pShellUIHelper;
HRESULT hr = CoCreateInstance(CLSID_ShellUIHelper, NULL,
CLSCTX_INPROC_SERVER, IID_IShellUIHelper,(LPVOID*)&pShellUIHelper);
if (SUCCEEDED(hr))
{
_variant_t vtTitle(GetTitle().AllocSysString());
CString strURL = m_webBrowser.GetLocationURL();
pShellUIHelper->AddFavorite(strURL.AllocSysString(), &vtTitle);
pShellUIHelper->Release();
}
}
我們注意到這裏的“AddFavorite”函數並沒有像“DoOrganizeFavDlg”那樣需要一個父窗口句柄。這也導致與在IE中打開不同,通過IShellUIHelper接口顯示出來的“添加到收藏夾”對話框是“非模態”的,有一個獨立於我們應用程序的任務欄按鈕,這使我們的瀏覽器顯得非常不專業(我是個追求完美的人,這也是我的瀏覽器遲遲不能發佈的原因之一)。
於是我們很自然地想到“shdocvw.dll”中除了“DoOrganizeFavDlg”外,應該還有一個類似的函數,可以傳入一個父窗口句柄用以顯示模態窗口,也許就像這樣:
typedef UINT (CALLBACK* LPFNADDFAV)(HWND, LPTSTR, LPTSTR);
事實上,這樣的函數確實存在於“shdocvw.dll”中,那就是“DoAddToFavDlg”。
3、DoAddToFavDlg函數
“DoAddToFavDlg”函數也是“shdocvw.dll”暴露出來的函數之一,其原型如下:
typedef BOOL (CALLBACK* LPFNADDFAV)(HWND, TCHAR*, UINT, TCHAR*, UINT,LPITEMIDLIST);
第一個參數正是我們想要的父窗口句柄,第二和第四個參數分別是初始目錄(一般來說就是收藏夾目錄)和要添加的鏈接的名字(比如網頁的Title),第三和第五個參數分別是第二和第四兩個緩衝區的長度,而最後一個參數則是指向與第二個參數目錄相關的item identifier list的指針(PIDL)。但最奇怪的是這裏並沒有像“AddFavorite”函數一樣的鏈接URL,那鏈接是怎樣添加的呢?答案是“手動創建”。
第二個參數在函數調用返回後會包含用戶在“添加到收藏夾”對話框中選擇或創建的完整鏈接路徑名(如“X:/XXX/mylink.url”),我們就根據這個路徑和網頁的URL來創建鏈接,代碼如下(爲簡化,此處省去檢查"shdocvw.dll"是否已在內存中的代碼,參見《Internet Explorer 編程簡述(三)“整理收藏夾”對話框》):
void CMyHtmlView::OnFavAddtofav()
{
typedef BOOL (CALLBACK* LPFNADDFAV)(HWND, TCHAR*, UINT, TCHAR*, UINT,LPITEMIDLIST);
HMODULE hMod = (HMODULE)LoadLibrary("shdocvw.dll");
if (hMod)
{
LPFNADDFAV lpfnDoAddToFavDlg = (LPFNADDFAV)GetProcAddress( hMod, "DoAddToFavDlg");
if (lpfnDoAddToFavDlg)
{
TCHAR szPath[MAX_PATH];
LPITEMIDLIST pidlFavorites;
if (SHGetSpecialFolderPath(NULL, szPath, CSIDL_FAVORITES, TRUE) &&
(SUCCEEDED(SHGetSpecialFolderLocation(NULL, CSIDL_FAVORITES, &pidlFavorites))))
{
TCHAR szTitle[MAX_PATH];
strcpy(szTitle, GetLocationName());
TCHAR szURL[MAX_PATH];
strcpy(szURL, GetLocationURL());
BOOL bOK = lpfnDoAddToFavDlg(m_hWnd, szPath,
sizeof(szPath)/sizeof(szPath[0]), szTitle,
sizeof(szTitle)/sizeof(szTitle[0]), pidlFavorites);
CoTaskMemFree(pidlFavorites);
if (bOK)
CreateInternetShortcut( szURL, szPath, "");
//創建Internet快捷方式
}
}
FreeLibrary(hMod);
}
return;
}
實現CreateInternetShortcut函數創建Internet快捷方式,可以用讀寫INI文件的方法,但更好的則是利用IUniformResourceLocator接口。
HRESULT CMyHtmlView::CreateInternetShortcut(LPCSTR pszURL, LPCSTR pszURLfilename,
LPCSTR szDescription,LPCTSTR szIconFile,int nIndex)
{
HRESULT hres;
CoInitialize(NULL);
IUniformResourceLocator *pHook;
hres = CoCreateInstance (CLSID_InternetShortcut, NULL, CLSCTX_INPROC_SERVER,
IID_IUniformResourceLocator, (void **)&pHook);
if (SUCCEEDED (hres))
{
IPersistFile *ppf;
IShellLink *psl;
// Query IShellLink for the IPersistFile interface for
hres = pHook->QueryInterface (IID_IPersistFile, (void **)&ppf);
hres = pHook->QueryInterface (IID_IShellLink, (void **)&psl);
if (SUCCEEDED (hres))
{
WORD wsz [MAX_PATH]; // buffer for Unicode string
// Set the path to the shortcut target.
pHook->SetURL(pszURL,0);
hres = psl->SetIconLocation(szIconFile,nIndex);
if (SUCCEEDED (hres))
{
// Set the description of the shortcut.
hres = psl->SetDescription (szDescription);
if (SUCCEEDED (hres))
{
// Ensure that the string consists of ANSI characters.
MultiByteToWideChar (CP_ACP, 0, pszURLfilename, -1, wsz, MAX_PATH);
// Save the shortcut via the IPersistFile::Save member function.
hres = ppf->Save (wsz, TRUE);
}
}
// Release the pointer to IPersistFile.
ppf->Release ();
psl->Release ();
}
// Release the pointer to IShellLink.
pHook->Release ();
}
return hres;
}
好,上面的方法雖然麻煩一點,但總算解決了“模態窗口”的問題,使得我們的程序不至於讓用戶鄙視。但是問題又來了,我們發現“允許脫機使用”是Disabled的,那“自定義”也就無從談起了,儘管90%的人都沒有使用過IE提供的脫機瀏覽。
難道我們的希望要破滅嗎?我們一方面想像調用“AddFavorite”函數一樣的不必手動創建鏈接,一方面又要模態顯示窗口,就像IE那樣,還能自定義脫機瀏覽。
4、腳本方式
許多網頁上都會有一個按鈕或鏈接“添加本頁到收藏夾”,實際上通過下面的腳本顯示模態的“添加到收藏夾”對話框將網頁加入到收藏夾。
window.external.AddFavorite(location.href, document.title);
這裏的external對象是WebBrowser內置的COM自動化對象,以實現對文檔對象模型(DOM)的擴展(我們也可以通過IDocHostUIHandler實現自己的擴展).查閱MSDN可以得知external對象的的方法與IShellUIHelper接口提供的方法是一樣的。我們有理由相信,IShellUIHelper提供了對WebBrowser內置的external對象的訪問,如果在適當的地方創建IShellUIHelper接口的實例,也許調用“AddFavorite”函數顯示出來的就是模態對話框了。問題是我們還沒有找到這樣的地方。
從上面的腳本,我們很自然地又想到另一個方法。如果能夠讓網頁來執行上面的腳本,豈不是問題就解決了?說做就做,如下:
void CMyHtmlView::OnFavAddtofav()
{
CString strUrl = GetLocationURL();
CString strTitle = GetLocationName();
CString strjs = "javascript:window.external.AddFavorite('" + strUrl + "'," + "'" + strTitle + "');";
ExecScript(strjs);
}
void CMIEView::ExecScript(CString strjs)
{
CComQIPtr pHTMLDoc = (IHTMLDocument2*)GetHtmlDocument();
if ( pHTMLDoc != NULL )
{
CComQIPtr pHTMLWnd;
pHTMLDoc->get_parentWindow( &pHTMLWnd );
if ( pHTMLWnd != NULL )
{
CComBSTR bstrjs = strjs.AllocSysString();
CComBSTR bstrlan = SysAllocString(L"javascript");
VARIANT varRet;
pHTMLWnd->execScript(bstrjs, bstrlan, &varRet);
}
}
}
先從CHtmlView獲得文檔的父窗口window對象的指針,再調用其方法execScript來執行腳本(事實上可以執行任意的腳本)。試驗發現,這個方法非常有效,不僅窗口是模態的,而且不需要手動創建鏈接,更重要的是“允許脫機使用”和“自定義”按鈕也可以用了。
5、問題仍舊沒有解決
執行腳本的方式看起來有效,可一旦我們的程序實現了IDocHostUIHandler接口對WebBrowser進行高級控制,就會發現一旦執行的腳本包含有對“external”對象的調用,就會出現“缺少對象”的腳本錯誤。原因是當MSHTML解析引擎(並非WebBrowser)檢查到宿主實現了IDocHostUIHandler接口,就會調用其GetExternal方法以獲得一個用以擴展DOM的自動化接口的引用。
HRESULT IDocHostUIHandler::GetExternal(IDispatch **ppDispatch)
但有時候我們並沒有想要擴展DOM,同時我們還希望WebBrowser使用它自己的DOM擴展。糟糕的是GetExternal方法的文檔中說這種情況下必須把ppDispatch設置爲NULL,換句話說,WebBrowser連它內置的external對象也不用了,那我們的window.external.AddFavorite就變得無處爲家了。
我曾多方嘗試將WebBrowser內置的external對象找出來,雖然都沒有成功,但是解決問題的方法卻被我找到了。
6、完美的方案
WebBrowser內置的external對象我們雖然找不到,但它肯定存在,我們只要想辦法讓WebBrowser自己完成對其調用即可。實現非常簡單,找到WebBrowser中包含的“Internet Explorer_Server”窗口的句柄,發一個消息就完成了。下面的代碼中假設m_hWndIE就是“Internet Explorer_Server”窗口的句柄。
#define ID_IE_ID_ADDFAV 2261
::SendMessage( m_hWndIE, WM_COMMAND, MAKEWPARAM(LOWORD(ID_IE_ID_ADDFAV), 0x0), 0 );
試一試成果,是不是和在Internet Explorer中選擇“添加到收藏夾”的效果一模一樣。
至於爲什麼這樣做,後續文章再說。
參考資料:
MSDN: Adding Internet Explorer Favorites to Your Application
MSDN: IShellUIHelper Interface
MSDN: external Object
MSDN: IDocHostUIHandler Interface