前言
由於本人在開發中經常要在程序中嵌入浏覽器,為了符合自己的 需求經常要對浏覽器進行擴展和定制,解決這些問題需在網上找資料和學習的過 程,我想可能很多開發者或許會遇到同樣的問題,特寫此文,以供大家參考。
在MFC中使用浏覽器
在MFC中微軟為我們提供了CHtmlView、 CDHtmlDialog類讓我們的程序很方便的嵌入浏覽器和進行浏覽器的二次開發,這 比直 接使用WebBrowser控件要方便很多,所以本文中討論的浏覽器的問題都是 針對CHtmlView來討論的。文中將提到一個類CLhpHtmlView, 它是CHtmlView的派 生類,文中提及的擴展或定制都將在CLhpHtmlView類(或派生類)上實現。
怎樣擴展或定制浏覽器
浏覽器定義了一些擴展接口(如 IDocHostUIHandler可以定制浏覽器界面有關的行為),以便開發者進行定制和 擴展。浏覽 器會在需要的時候向他的控制站點查詢這些接口,在控制站點裡實 現相應的接口就可以進行相應的擴展。在MFC7.01類 庫中,CHtmlView使用的控 制站點是CHtmlControlSite的,在CHtmlControlSite類中 只實現了接口 IDocHostUIHandler,而要實現更多的擴展接口,必須用自定義的控制站類來取 代CHtmlControlSite,在下文中提及的類CDocHostSite即為自定義 的控制站類 。
關於接口的介紹請參考: http://dev.csdn.net/develop/article/48/48483.shtm
如何 使自定義的控制站點來替換默認的控制站點呢?在MFC7.0中只需重載CHtmlView 的虛函數CreateControlSite即可:
BOOL CLhpHtmlView::CreateControlSite(COleControlContainer * pContainer,
VC6.0要替換控制站要復雜的多,這裡就不討論了,如需要6.0版本的請 給我發郵件到[email protected]。
COleControlSite ** ppSite, UINT /*nID*/, REFCLSID /*clsid*/)
{
*ppSite = new CDocHostSite(pContainer, this);// 創建自己的 控制站點實例
return (*ppSite) ? TRUE : FALSE;
}
定制鼠標右鍵彈出出菜單
要 定制浏覽器的鼠標右鍵彈出菜單,必須在自定義的控制站點類中實現 IDocHostUIHandler2接口,並且IE的 版本是5.5或以上。在接口 IDocHostUIHandler2的ShowContextMenu方法中調用浏覽器類的 OnShowContextMenu虛函數,我們 在浏覽器類的派生類重載此虛函數即可實現右 鍵菜單的定制,參見代碼
HRESULT CDocHostSite::XDocHostUIHandler::ShowContextMenu(DWORD dwID,
POINT * ppt,
IUnknown * pcmdtReserved,
IDispatch * pdispReserved)
{
METHOD_PROLOGUE (CDocHostSite, DocHostUIHandler);
return pThis->m_pView- >OnShowContextMenu( dwID, ppt, pcmdtReserved,pdispReserved );
}
HRESULT CLhpHtmlView::OnShowContextMenu(DWORD dwID,
LPPOINT ppt,
LPUNKNOWN pcmdtReserved,
LPDISPATCH pdispReserved)
{
HRESULT result = S_FALSE;
switch(m_ContextMenuMode)
{
case NoContextMenu: // 無菜單
result=S_OK;
break;
case DefaultMenu: // 默認菜單
break;
case TextSelectionOnly: // 僅文本選擇菜單
if(!(dwID == CONTEXT_MENU_TEXTSELECT || dwID == CONTEXT_MENU_CONTROL))
result=S_OK;
break;
case CustomMenu: // 自定義菜單
if(dwID! =CONTEXT_MENU_TEXTSELECT)
result=OnShowCustomContextMenu(ppt,pcmdtReserved,pdispReserved);
break;
}
return result;
}
在 CLhpHtmlView中定義的枚舉類型CONTEXT_MENU_MODE舉出了定制右鍵彈出菜單的 四種類型
enum CONTEXT_MENU_MODE // 上下文菜單
{
NoContextMenu, // 無菜單
DefaultMenu, // 默認菜單
TextSelectionOnly, // 僅文本選擇菜單
CustomMenu // 自定義菜單
};
通過 CLhpHtmlView的函數SetContextMenuMode來設置右鍵菜單的類型。如果設定的右 鍵彈出菜單是“自定義菜單”類型,我們只要在CLhpHtmlView的派生 類中重載OnShowCustomContextMenu虛函數即可,如下代碼 CDemoView是 CLhpHtmlView的派生類 HRESULT CDemoView::OnShowCustomContextMenu (LPPOINT ppt, LPUNKNOWN pcmdtReserved,LPDISPATCH pdispReserved)
{
if ((ppt==NULL)||(pcmdtReserved==NULL)|| (pcmdtReserved==NULL))
return S_OK;
HRESULT hr=0;
IOleWindow *oleWnd=NULL;
hr=pcmdtReserved ->QueryInterface(IID_IOleWindow, (void**)&oleWnd);
if ((hr != S_OK)||(oleWnd == NULL))
return S_OK;
HWND hwnd=NULL;
hr=oleWnd->GetWindow(&hwnd);
if((hr!=S_OK)||(hwnd==NULL))
{
oleWnd- >Release();
return S_OK;
}
IHTMLElementPtr pElem=NULL;
hr = pdispReserved- >QueryInterface(IID_IHTMLElement, (void**)&pElem);
if (hr != S_OK)
{
oleWnd->Release();
return S_OK;
}
IHTMLElementPtr pParentElem=NULL;
_bstr_t tagID;
BOOL go=TRUE;
pElem->get_id(&tagID.GetBSTR());
while(go && tagID.length()==0)
{
hr=pElem->get_parentElement(&pParentElem);
if (hr==S_OK && pParentElem!=NULL)
{
pElem->Release();
pElem=pParentElem;
pElem->get_id(&tagID.GetBSTR());
}
else
go=FALSE;
};
if (tagID.length()==0)
tagID="no id";
CMenu Menu,SubMenu;
Menu.CreatePopupMenu();
CString strTagID = ToStr(tagID);
if(strTagID == "red")
Menu.AppendMenu(MF_BYPOSITION, ID_RED, "您點擊的是紅色");
else if(strTagID == "green")
Menu.AppendMenu(MF_BYPOSITION, ID_GREEN, "您點擊的是綠色");
else if(strTagID == "blue")
Menu.AppendMenu(MF_BYPOSITION, ID_BLUE, "您點擊的是藍色");
else
Menu.AppendMenu(MF_BYPOSITION, ID_NONE, "你點了也白點,請在指定的 地方點擊");
int MenuID=Menu.TrackPopupMenu (TPM_RETURNCMD|TPM_LEFTALIGN|TPM_RIGHTBUTTON,ppt->x, ppt->y, this);
switch(MenuID)
{
case ID_RED:
MessageBox("紅色");
break;
case ID_GREEN:
MessageBox("紅色");
break;
case ID_BLUE:
MessageBox("紅色 ");
break;
case ID_NONE:
MessageBox("haha");
break;
}
oleWnd->Release();
pElem->Release();
return S_OK;
}
實現腳本擴展(很重要的external接口)
在 你嵌入了浏覽器的工程中,如果網頁的腳本中能調用C++代碼,那將是一件很惬 意的事情,要實現這種交互,就必須實現腳本擴展。實現腳本擴展就是在程序中 實現一個IDispatch接口,通過CHtmlView類的OnGetExternal虛函數返回此接口 指針,這樣就可以在腳本中通過window.external.XXX(關鍵字window可以省略)來 引用接口暴露的方法或屬性(XXX為方法或屬性名)。在MFC中從CCmdTarget派生的 類都可以實現自動化,而不必在MFC工程中引入繁雜的ATL。從CCmdTarget派生的 類實現自動化接口的時候不要忘了在構造函數中調用EnableAutomation函數。
要使虛函數OnGetExternal發揮作用必須在 自定義的控制站點類中實現 IDocHostUIHandler,在接口IDocHostUIHandler的GetExternal方法中調用浏覽 器類的OnGetExternal虛函數,我們在浏覽器類的派生類重載OnGetExternal虛函 數,通過參數lppDispatch返回一個IDispatch指針,這樣腳本中引用 window.external時就是引用的返回的接口,參見代碼 HRESULT CDocHostSite::XDocHostUIHandler::GetExternal(IDispatch ** ppDispatch)
請注意上面代碼中,在OnGetExternal返回的是自 身IDispatch接口,這樣就不比為腳本擴展而另外寫一個從CCmdTarget派生的新 類,CLhpHtmlView本身就是從CCmdTarget派生,直接在上面實現接口就是。
{
METHOD_PROLOGUE(CDocHostSite, DocHostUIHandler);
return pThis->m_pView->OnGetExternal( ppDispatch );
}
CLhpHtmlView::CLhpHtmlView(BOOL isview)
{
......
EnableAutomation();// 允許自動化
}
HRESULT CLhpHtmlView::OnGetExternal(LPDISPATCH *lppDispatch)
{
*lppDispatch = GetIDispatch(TRUE);// 返回自身的IDispatch接口
return S_OK;
}
下用具體示例來說明怎樣實現腳本擴展
示例會在網頁上點擊一個 按鈕而使整個窗口發生抖動
從CLhpHtmlView派生一個類CDemoView,在類 中實現IDispatch, 並通過IDispatch暴露方法WobbleWnd ------------- --------------------------------------------------------------
這裡我要 介紹一下DISP_FUNCTION宏,它的作用是將一個函數映射到Dispatch映射表中, 我們看
文 件 DemoView.h
-------------------------------------------------- -------------------------
.......
class CDemoView : public CLhpHtmlView
{
......
DECLARE_DISPATCH_MAP() // 構建dispatch映射表以暴露方法或屬性
......
void WobbleWnd();// 抖動窗口
};
-------------------------------- -------------------------------------------
文件 DemoView.cpp
------------------------------------------------------------------- --------
......
// 把成員函數映射到Dispatch映射表中,暴露方法 給腳本
BEGIN_DISPATCH_MAP(CDemoView, CLhpHtmlView)
DISP_FUNCTION(CDemoView, "WobbleWnd", WobbleWnd, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()
......
void CDemoView::WobbleWnd()
{
// 在這裡實現抖動窗口
......
}
-------------------------------------------------- -------------------------
文件 Demo.htm
------------------- --------------------------------------------------------
...... onclick="external.WobbleWnd()" ...... DISP_FUNCTION(CDemoView, "WobbleWnd", WobbleWnd, VT_EMPTY, VTS_NONE)
CDemoView是宿主類名, "WobbleWnd"是暴露給外面的名字(腳本調用時使用的名字), VT_EMPTY是返回值得類型為空,VTS_NONE說明此方法沒有參數,如果要映射的函 數有返回值和參數該 如何映射,通過下面舉例來說明
DISP_FUNCTION (CCalendarView,"TestFunc",TestFunc,VT_BOOL,VTS_BSTR VTS_I4 VTS_I4)
BOOL TestFunc(LPCSTR param1, int param2, int param3)
{
.....
}
參數表VTS_BSTR VTS_I4 VTS_I4是用空 格分隔,他們的類型映射請參考MSDN,這要提醒的是不要把VTS_BSTR與CString 對應,而應與LPCSTR對應。
C++代碼中如何調用網頁腳本中的函數
IHTMLDocument2::scripts屬性表示HTML文檔中所有腳本對象。使用腳本 對象的IDispatch接口的GetIDsOfNames方法可以得到腳本函數的 DispID,得到 DispID後,使用IDispatch的Invoke函數可以調用對應的腳本函數。 CLhpHtmlView提供了方便的調用JavaScript的函數,請參考CLhpHtmlView中有關 鍵字“JScript”的代碼。
定制消息框的標題
我們在 腳本中調用alert彈出消息框時,消息框的標題是微軟預定義的 “Microsoft Internet Explorer”,如下圖:
在自 定義的控制站點類中實現IDocHostShowUI接口,在接口的ShowMessage方法中調 用浏覽器的OnShowMessage,我們重載 OnShowMessage虛函數即可定制消息框的標 題,實現代碼如下:
// 窗口標題"Microsoft Internet Explorer"的資源標識
#define IDS_MESSAGE_BOX_TITLE 2213
HRESULT CLhpHtmlView::OnShowMessage(HWND hwnd,
LPOLESTR lpstrText,
LPOLESTR lpstrCaption,
DWORD dwType,
LPOLESTR lpstrHelpFile,
DWORD dwHelpContext,
LRESULT * plResult)
{
//載入Shdoclc.dll 和IE消息框標題字符串
HINSTANCE hinstSHDOCLC = LoadLibrary(TEXT ("SHDOCLC.DLL"));
if (hinstSHDOCLC == NULL)
return S_FALSE;
CString strBuf,strCaption (lpstrCaption);
strBuf.LoadString(hinstSHDOCLC, IDS_MESSAGE_BOX_TITLE);
// 比較IE消息框標題字符串和 lpstrCaption
// 如果相同,用自定義標題替換
if (strBuf==lpstrCaption)
strCaption = m_DefaultMsgBoxTitle;
// 創建自己的消息框並且顯示
*plResult = MessageBox(CString(lpstrText), strCaption, dwType);
//卸載Shdoclc.dll並且返回
FreeLibrary(hinstSHDOCLC);
return S_OK;
}
從代碼中可以看到通過設定 m_DefaultMsgBoxTitle的值來改變消息寬的標題,修改此值是同過 SetDefaultMsgBoxTitle來實現 void CLhpHtmlView::SetDefaultMsgBoxTitle(CString strTitle)
{
m_DefaultMsgBoxTitle=strTitle;
}
怎樣定制、修改浏覽 器向Web服務器發送的HTTP請求頭
在集成了WebBrowser控件的應用中, Web服務器有時可能希望客戶端(浏覽器)發送的HTTP請求中附帶一些額外的信息 或自定義的 HTTP頭字段,這樣就必須在浏覽器中控制向Web服務器發送的HTTP請 求。下面是捕獲的一個普通的用浏覽器發送的HTTP請求頭:GET /text7.htm HTTP/1.0
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, \
application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Referer: http://localhost
Accept- Language: en-us
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; Poco 0.31; LHP Browser 1.01; \
.NET CLR 1.1.4322)
Host: localhost
Connection: Keep-Alive
CHtmlView的
void Navigate2(
LPCTSTR lpszURL,
DWORD dwFlags = 0,
LPCTSTR lpszTargetFrameName = NULL,
LPCTSTR lpszHeaders = NULL,
LPVOID lpvPostData = NULL,
DWORD dwPostDataLen = 0
);
函數參數lpszHeaders可以指定 HTTP請求頭,示例如下:
Navigate2(_T ("http://localhost"),NULL,NULL, "MyDefineField: TestValue");
我們捕獲的HTTP頭如下:
怎樣修改浏覽器標識
在HTTP請求頭中User-Agent字段表明了浏覽器的版本以 及操作系統的版本等信息。WEB服務器經常需要知道用戶請求頁面時是來自IE還 是來自自己的客戶端中的WebBrowser控件,以便分開處理,而WebBrowser控件向 WEB服務器發送的浏覽器標識(User-Agent字段)跟用IE發送的是一樣的,怎樣區 分自己的浏覽器和IE呢? 微軟沒有提供現成的方法,要自己想法解決。前面討 論的定制HTTP請求頭就是為這一節准備的。思路是這樣的:在自己的浏覽器裡處 理每一個U頁面請求,把請求頭User-Agent改成自己想要的。在CHtmlView的 OnBeforeNavigate2虛函數裡來修改HTTP請求是再好不過了,#define WM_NVTO (WM_USER+1000)
class NvToParam
{
public:
CString URL;
DWORD Flags;
CString TargetFrameName;
CByteArray PostedData;
CString Headers;
};
void CDemoView::OnBeforeNavigate2(LPCTSTR lpszURL,
DWORD nFlags,
LPCTSTR lpszTargetFrameName,
CByteArray& baPostedData,
LPCTSTR lpszHeaders,
BOOL* pbCancel)
{
CString strHeaders(lpszHeaders);
if(strHeaders.Find("User- Agent:LHPBrowser 1.0") < 0)// 檢查頭裡有沒有自定義的User-Agent 串
{
*pbCancel = TRUE;// 沒有,取消這次導航
if(!strHeaders.IsEmpty())
strHeaders += "\r\n";
strHeaders += "User- Agent:LHPBrowser 1.0";// 加上自定義的User-Agent串
NvToParam* pNvTo = new NvToParam;
pNvTo->URL = lpszURL;
pNvTo->Flags = nFlags;
pNvTo- >TargetFrameName = lpszTargetFrameName;
baPostedData.Copy(pNvTo->PostedData);
pNvTo- >Headers = strHeaders;
// 發送一個自定義的導航消息,並 把參數發過去
PostMessage(WM_NVTO,(WPARAM)pNvTo);
return;
}
CHtmlView::OnBeforeNavigate2 (lpszURL,
nFlags,
lpszTargetFrameName,
baPostedData,
lpszHeaders,
pbCancel);
}
LRESULT CDemoView::OnNvTo(WPARAM wParam, LPARAM lParam)
{
NvToParam* pNvTo = (NvToParam*)wParam;
Navigate2 ((LPCTSTR)pNvTo->URL,
pNvTo->Flags,
pNvTo->PostedData,
(LPCTSTR)pNvTo ->TargetFrameName,
(LPCTSTR)pNvTo- >Headers);
delete pNvTo;
return 1;
}
在OnBeforeNavigate2中如果發現沒有自定義的User-Agent串,就加 上這個串,並取消本次導航,再Post一個消息(一定要POST,讓 OnBeforeNavigate2跳出以後再進行導航 ),在消息中再次導航,再次導航時請 求頭已經有了自己的標識,所以能正常的導航。
去掉討厭的異常警告
在程序中使用了CHtmlView以後,我們在調整窗口大小的時候經常會看到 輸出窗口輸出的異常警告:ReusingBrowser.exe 中的 0x77e53887 處最可能的 異常: Microsoft C++ exception: COleException @ 0x0012e348 。 Warning: constructing COleException, scode = DISP_E_MEMBERNOTFOUND($80020003).
這是由於CHtmlView在處理 WM_SIZE消息時的一點小問題引起的,采用如下代碼處理WM_SIZE消息就不會有此 警告了
void CLhpHtmlView::OnSize(UINT nType, int cx, int cy)
{
CFormView::OnSize(nType, cx, cy);
if (::IsWindow(m_wndBrowser.m_hWnd))
{
CRect rect;
GetClientRect(rect);
// 就這一句與 CHtmlView的不同
::AdjustWindowRectEx(rect, GetStyle(), FALSE, WS_EX_CLIENTEDGE);
m_wndBrowser.SetWindowPos (NULL,
rect.left,
rect.top,
rect.Width(),
rect.Height(),
SWP_NOACTIVATE | SWP_NOZORDER);
}
}
怎樣處理浏覽器內的拖放
有時可能有這樣的需求,我們希望在資源管理器裡托一個文件到浏覽器 而做出相應的處理,甚至是將文件拖到某一個網頁元素上來做出相應的處理,而 浏覽器默認的處理拖放文件操作是將文件打開,但WebBrowser控件給了我們一個 自己處理拖放的機會。那就是在自定義的控制站點類中實現IDocHostUIHandler ,在接口IDocHostUIHandler的GetDropTarget方法中調用 浏覽器類的 OnGetDropTarget虛函數。要處理網頁內的拖放,必需在OnGetDropTarget函數中 返回一個自己定義的IDropTarget接口指針,所以我們自己寫一個類 CMyOleDropTarget從COleDropTarget類派生,並且在實現IDropTarget接口,此 類的代碼在這就不列出了,請下載演示 程序,參考文件MyOleDropTarget.h和 MyOleDropTarget.cpp。我們看CLhpHtmlView中OnGetDropTarget的代碼
HRESULT CLhpHtmlView::OnGetDropTarget(LPDROPTARGET pDropTarget, LPDROPTARGET* ppDropTarget )
m_DropTarget即為自定義的處理拖放的對象。這樣就能通過在從CLhpHtmlView派 生的類中重載OnDragEnter、OnDragOver、 OnDrop、OnDragLeave虛函數來處理 拖放了。在這裡順帶講一下視圖是怎樣處理拖放的。要使視圖處理拖放,首先在 視圖裡添加一個COleDropTarget(或派生類)成員變量,如CLhpHtmlView中的 “CMyOleDropTarget m_DropTarget;”,再在 視圖創建時調用 COleDropTarget對象的Register,即把視圖與COleDropTarget對象關聯起來,如 CLhpHtmlView中的“m_DropTarget.Register(this);”,再對拖放 觸發的事件進行相應的處理,OnDragEnter 把某對象拖入到視圖時觸發,在此檢 測拖入的對象是不是視圖想接受的對象,如是返回 “DROPEFFECT_MOVE”表示接受此對象,如
{
m_DropTarget.SetIEDropTarget(pDropTarget);
LPDROPTARGET pMyDropTarget;
pMyDropTarget = (LPDROPTARGET) m_DropTarget.GetInterface(&IID_IDropTarget);
if (pMyDropTarget)
{
*ppDropTarget = pMyDropTarget;
pMyDropTarget->AddRef();
return S_OK;
}
return S_FALSE;
}
if (pDataObject->IsDataAvailable(CF_HDROP))// 被拖對象是文件嗎?
return DROPEFFECT_MOVE;
OnDragOver 被拖對象在視圖上移動, 同OnDragEnter一樣檢測拖入對象,如果要接受此對象返回 “DROPEFFECT_MOVE”。OnDrop 拖著被拖對象在視圖上放開鼠標,在 這裡對拖入對象做出處理; OnDragLeave 拖著被拖對象離開視圖。C++的代碼寫 好了,但事情還沒完,還必須在網頁裡用腳本對拖放事件進行處理,即頁面裡哪 個元素要接受拖放對象哪個元素就要處理ondragenter、ondragover、ondrop, 代碼其實很簡單,讓事件的返回值為false即可,這樣 C++的代碼才有機會處理 拖放事件,代碼如下:......
<td ondragenter="event.returnValue = false" ondragover="event.returnValue = false" \
ondrop="event.returnValue = false">
......
如果要使整個視圖都接受拖放,則在Body元素中處理此三個 事件。注意:別忘了讓工程對OLE的支持即在初始化應用程序時調用AfxOleInit ()。
怎樣禁止網頁元素的選取
用網頁做界面時多數情況下是不希 望網頁上的元素是能夠被鼠標選中的,要使網頁元素不能被選中做法是:給浏覽 器的“宿主信息標記”加上DOCHOSTUIFLAG_DIALOG標記。
“宿主信息標記”用N個標記位來控制浏覽器的許多性質,如 :
禁用浏覽器的3D的邊緣;
禁止滾動條;
禁用腳本;
定義雙擊處理的方式;
禁用浏覽器的自動完成功能;
...... 更多詳情請參考MSDN的DOCHOSTUIFLAG幫助。
怎樣修改 “宿主信息標記”?
在CDocHostSite中實現 IDocHostUIHandler,在GetHostInfo方法中調用浏覽器的OnGetHostInfo虛函數 ,在虛函數OnGetHostInfo中便可指定“宿主信息標記”,如:
HRESULT CLhpHtmlView::OnGetHostInfo(DOCHOSTUIINFO * pInfo)
{
pInfo->cbSize = sizeof(DOCHOSTUIINFO);
pInfo->dwFlags = DOCHOSTUIFLAG_DIALOG |
DOCHOSTUIFLAG_THEME |
DOCHOSTUIFLAG_NO3DBORDER |
DOCHOSTUIFLAG_SCROLL_NO;
pInfo->dwDoubleClick = DOCHOSTUIDBLCLK_DEFAULT;
return S_OK;
}
用腳本也可 實現:在Head中加入腳本:
document.onselectstart=new Function (''return false'');
或者
<body onselectstart="return false">。
其它
在CLhpHtmlView中還提供了幾個函數,修改網頁元素的內容:BOOL PutElementHtml(CString ElemID,CString Html);
取表單元素的值:
BOOL GetElementValue(CString ElemID,CString& Value);
設置表單元素的值:BOOL PutElementValue(CString ElemID,CString Value);
給表單元素設置焦點:void ElementSetFocus(CString EleName);
本文配套源碼