瀏覽器整合教學--在瀏覽器程式中添加宏支援
這個教程提供在瀏覽器程式中添加宏支援的方法,你會看到如何給MFC的程式添加宏支援。這篇文章也討論了如何擴充VC6中的CHtmlView的功能,如何?MDI結構的瀏覽器,以及如何分析DHTML的文檔結構。
單擊這裡下載本文的代碼
本文分為以下部分
- 前提和需求
- 介紹
- 動態指令碼處理
- 為應用程式添加指令碼支援
- 安全性
- CHtmlView的增強
- 指令碼樣本
- 結論
- 參考
前提和需求
在閱讀本文之前,建議先
- 對微軟基礎類(MFC)和元件物件模型(COM)有所瞭解
- 熟悉Active Template Library(ATL)
- 安裝了微軟的網際網路探索者(IE)6.0或者更高版本
- 開發環境中具有IE6.0或者更高版本的標頭檔和庫檔案
介紹
整合瀏覽器控制項對於快速的應用程式開發(RAD)是一個強有力的工具;你可以使用動態HTML(DHTML),或者可擴充的標記語言(XML)顯示你的使用者介面。一個通常的用途是用它來顯示表單,然後通過分析表單網頁和處理遞交事件來處理表單。但是,如果你要分析表單頁面的話,分析的方式完全依賴於頁面的結構,也就是說,如果在應用程式中通過IE提供的介面分析網頁,那麼為了每個網頁結構,你要編寫和編譯一次代碼。這在應用程式和表單網頁一起發布的情況下是完全沒有問題的,但是對於表單網頁位於遠程伺服器上,並且有時候會修改的情況,或者想使應用程式對其他網站有效,就必須同時修改並且重新發布應用程式。為了避免反覆修改應用程式,可以使用——
動態指令碼處理
使用動態指令碼處理可以編寫靈活的處理代碼而無需重新編譯器。你可能已經在很多應用程式中見到過動態指令碼處理,例如IE、Microsoft Office和Visual Studio。在平台SDK(Platform SDK)的微軟視窗指令碼技術(Microsoft Windows Script Technologies)部分的微軟視窗指令碼介面介紹(Microsoft Windows Script Interfaces Introduction)一文中,介紹了動態指令碼處理的概念、背景、架構和調用步驟。
如果需要範例程式碼,可以在微軟KB中尋找kbAXScript關鍵字。下面是一些樣本
本文的範例程式碼是基於MFC6的,所以採用了Q168214中提供的代碼。
為應用程式添加指令碼支援Automation 物件
為了實現指令碼支援,我們需要讓應用程式具有Automation 伺服程式支援。實現這個支援的最簡單的辦法是使用MFC的應用程式嚮導(Application Wizard)建立IEAutomation MDI應用程式時,在MDI嚮導第三步選中Automation支援。
嚮導自動產生的CHtmlView衍生類別CIEAutomationView並不是一個Automation 物件,所以在建立樣本工程時,我把CIEAutomationView的定義和實現檔案刪除,然後刪除類嚮導中CIEAutomationView的資訊,重新建立CIEAutomationView類,在建立的時候指定基類是CHtmlView並且支援自動化。
CIEAutomationView中的指令碼解譯器是從Q168214的範例程式碼修改的,去掉了一些對象,增加了DOM擴充項物件的實現。
Scripter對象
指令碼引擎對象,可以用名字Scripter訪問。提供建立對象的方法。
WebBrowser對象
瀏覽器控制項對象,可以用名字WebBrowser訪問。可以用來訪問文件物件模型。查看原始碼功能也被增強了以顯示文件物件模型。需要更多資訊的話,可以查看文末的參考。
External對象
DOM擴充項物件,可以用名字External訪問。用於擴充瀏覽器的文件物件模型的對象。在本樣本中,我也同時用這個對象轉寄了WebBrowser對象的事件。儘管大部分功能都實現了,但是自動完成功能似乎還有點問題,看起來和IShellUIHelper的未公開方法AutoCompleteAttatch有關。(實際上,瀏覽器控制項需要實現IDocHostUIHandler::GetHostInfo,參考http://blog.joycode.com/jiangsheng/archive/2004/01/08/11037.aspx)
類型庫支援
指令碼中需要對象的類型庫資訊來訪問對象的屬性、方法和事件。預設情況下,直接從CComTarget派生的類是無法通過類嚮導添加和刪除事件的,CComTargetEx類“類比”了ActiveX的部分特性,並且欺騙了類嚮導來做到這一點。在給對象添加類型資訊時,參考了Q185720 HOWTO: Provide Type Information From an MFC Automation Server中的方法,把應用程式的類型庫添加到資源。
安全性
儘管自動化瀏覽器可以提供更多的靈活性,但是這也把應用程式的一部分暴露給使用者。例如,使用者可能修改指令碼使得應用程式不能正常工作。另外,如果使用者可以查看指令碼,那麼就可以瞭解程式結構,並且能夠藉此攻擊沒有謹慎設計的網站。
使用指令碼建立對象也可能有安全性問題。某些對象不是安全的,例如惡意的或者不正確使用的COM對象。
如果擴充了DOM,使得網頁上的指令碼可以訪問應用程式的功能,那麼需要確保指令碼是安全的,或者來自於可以信賴的網站。下面的函數用於訪問DOM中Window的擴充屬性之前檢查安全性。
BOOL CIEAutomationView::CanAccessExternal()
{
// if the dispatch we have is safe,
// we allow access
if (IsExternalDispatchSafe())
return TRUE;
// the external dispatch is not safe, so we check
// whether the current zone allows for scripting
// of objects that are not safe for scripting
if (m_spHtmlDoc == NULL)
return FALSE;
CComPtr<IInternetHostSecurityManager> spSecMan;
m_spHtmlDoc->QueryInterface(IID_IInternetHostSecurityManager,
(void **) &spSecMan);
if (spSecMan == NULL)
return FALSE;
HRESULT hr = spSecMan->ProcessUrlAction(URLACTION_ACTIVEX_OVERRIDE_OBJECT_SAFETY,
NULL, 0, NULL, 0, 0, PUAF_DEFAULT);
if (hr == S_OK)
return TRUE;
return FALSE;
}
預設設定下,一般網頁上的指令碼可以訪問同一網站上的網頁。
CHtmlView的增強使用進階宿主特性
使用進階宿主特性的好處可以參見我翻譯的CSDN文檔中心文章自訂瀏覽器。在本文的範例程式碼中,我使用這個特性擴充了DHTML文檔結構模型(DOM),使得網頁中的指令碼可以訪問應用程式。離線瀏覽功能的實現也可以參考這篇文章。
為了可以在MFC6的CHtmlView基礎上使用進階宿主特性自訂瀏覽器,需要重載預設的控制項客戶網站(這個代碼只在MFC6中有必要,MFC7的CHtmlView已經支援了進階宿主特性)。因為MFC6不能重載CWnd的虛函數CreateControlSite來建立自訂的客戶網站,所以使用Q236312 HOWTO: Disable the Default Pop-up Menu for CHtmlView這篇文章中的方法,重載預設的控制項客戶網站管理器。然後在重載過的預設控制項客戶網站中儲存控制項宿主的指標
CCustomControlSite::CCustomControlSite(COleControlContainer *pCnt):COleControlSite(pCnt){ m_pCustomImpl=NULL; CWnd* pWnd=pCnt->m_pWnd; if(pWnd){ if(pWnd->IsKindOf(RUNTIME_CLASS(CIEAutomationView))){ CIEAutomationView* pView=(CIEAutomationView*)pWnd; m_pCustomImpl=pView; } }
這樣可以在控制項客戶網站的IDocHostUIHandler2實現中調用控制項宿主的相應處理,例如
HRESULT FAR EXPORT CCustomControlSite::XDocHostUIHandler2::GetHostInfo( DOCHOSTUIINFO* pInfo ){ METHOD_PROLOGUE(CCustomControlSite, DocHostUIHandler2) if(pThis->m_pCustomImpl){ return pThis->m_pCustomImpl->GetHostInfo(pInfo ); } return S_OK;}
進階宿主特性的應用之一就是擴充DOM,使得網頁上的指令碼可以使用window.external訪問DOM擴充項物件。IE實現的DOM擴充項物件具有menuArguments屬性和IShellUIHelper介面。
控制新的視窗
預設情況下,瀏覽器收到建立新視窗請求時,會在IE中開啟新的視窗。你可以處理NewWindow2事件來在自己指定的視窗中開啟請求的頁面。
void CIEAutomationView::OnNewWindow2(LPDISPATCH* ppDisp, BOOL* Cancel)
{
// Get a pointer to the application object.
CWinApp* pApp = AfxGetApp();
// Get the correct document template.
POSITION pos = pApp->GetFirstDocTemplatePosition();
CDocTemplate* pDocTemplate = pApp->GetNextDocTemplate( pos );
// Create a new frame.
CFrameWnd* pFrame = pDocTemplate->CreateNewFrame(
GetDocument(),
(CFrameWnd*)AfxGetMainWnd() );
// Activate the frame.
pDocTemplate->InitialUpdateFrame( pFrame, NULL );
CIEAutomationView* pView = (CIEAutomationView*)pFrame->GetActiveView();
// Pass pointer of WebBrowser object.
pView->SetRegisterAsBrowser( TRUE );
*ppDisp = pView->GetApplication();
}
如果需要更多資訊,參見Q184876 HOWTO: Use the WebBrowser Control NewWindow2 Event。。
MDI瀏覽器
本文的範例程式碼是基於瀏覽器的,為了省事起見,直接在MFC的MFCIE樣本上進行了修改,改成了MDI結構。MFCIE本身就是一個簡單的瀏覽器,但是在把代碼從主架構轉移到子架構之後出了一點小問題,動態建立的收藏夾菜單不見了。這是由於MDI架構的菜單替換機制在架構啟用時恢複了預設菜單造成的,所以我重載了CDocument::GetDefaultMenu,以在MDI架構的菜單替換的時候恢複我修改過的菜單(參見)。為了在子架構的建立過程中獲得文檔指標來修改文檔中儲存的菜單,可以從建立結構獲得MDI建立上下文。
-
MDICREATESTRUCT * pMDICreateStruct=(MDICREATESTRUCT * )lpCreateStruct->lpCreateParams;CCreateContext *pCreateContext=(CCreateContext *)pMDICreateStruct->lParam;pMenu =((CIEAutomationDoc *)pCreateContext->m_pCurrentDoc)->m_menuDefault.GetSubMenu(3);
收藏夾
MFCIE樣本中示範了如何建立一個收藏夾菜單,但是在移植工具列裡面的顯示收藏夾命令到MDI子架構的時候碰見一個問題,動態建立的收藏夾菜單的位置不固定。但是通過尋找新增的“添加到收藏夾”命令的位置,可以確定這個菜單的位置。添加到收藏夾和管理收藏夾的功能是通過建立ShellUIHelper對象實現的。
自動完成
為了使用方便,我在應用程式中也添加了自動完成功能。地址欄的自動完成功能的實現比較簡單,調用系統的API SHAutoComplete就可以了。在我自己擴充了DOM的情況下,表單的自動完成似乎有些問題。
需要更多資訊的話,可以參考我翻譯的CSDN文檔中心文章在應用程式中整合自動完成功能。
訪問需要授權的網站
某些網站在訪問時需要驗證使用者身份,但是預設情況下瀏覽器控制項在無法驗證使用者身份時並不提示使用者輸入使用者名稱和密碼。通過在控制項的客戶網站實現IServiceProvider介面,並且同時實現IAuthenticate介面,使得應用程式具有輸入身分識別驗證資訊的功能。更多資訊參見微軟知識庫文章Q329802 錯誤:通過IAuthenticate進行的Proxy 驗證可能會在安全URL上失敗。
常用命令處理
為了使用方便,增加了調用尋找對話方塊、查看原始碼和設定Internet選項的功能。這是通過查詢瀏覽器控制項的IOleCommandTarget介面,執行命令組CGID_WebBrowser的命令實現的。實現這類命令的方法不只一種,例如可以載入inetcpl.cpl,調用函數LaunchInternetControlPanel來實現開啟Internet選項;使用IMarkupServices介面執行尋找、定位和選擇,以及使用流來獲得/設定網頁的內容。在範例程式碼中,示範了如何分析文檔結構,以及如何編輯選定的網頁元素的HTML代碼或者架構的源檔案。
MFC6BUG的修複
儘管應用程式已經可以具有比較完整的功能,但是為了讓應用程式能夠長期正常工作,需要修複MFC6中包含的一些問題。我在這裡只列出文章標題,有興趣的話可以去查看微軟知識庫文章或者本文的代碼
- Q241750 BUG: CHtmlView Leaks Memory by Not Releasing BSTRs in Several Methods
- Q220021 BUG: CHtmlView Does Not Repaint Correctly in an AppWizard-Generated MDI Application
- Q253219 PRB: WebBrowser Control Disappears When Script Calls window.close()
指令碼樣本
WebBrowser.Navigate "About:<H1><B>This is a test</B></h1>"
Dim msword Set msword = Scripter.HostCreateObject("Word.Basic")msword.appshowmsword.filenewmsword.Insert "hello"
Sub External_BeforeNavigate2(pDisp, URL, Flags, TargetFrameName, PostData, Headers, Cancel)MsgBox URLEnd Sub
如果你為WebBrowser對象的事件編寫指令碼,你會發現這些事件處理代碼不會被執行,這是因為CHtmlView處理了這些事件。你可以在你的CHtmlView衍生類別的處理代碼中觸發自訂對象的相應事件。在範例程式碼中,我轉寄了BeforeNavigate2事件到自訂對象的事件。
結論
給應用程式添加指令碼支援可以大幅度提高程式的靈活性和可擴充性。雖然為此會犧牲一些效能、安全性和增加一些代碼量,但是很多時候這種犧牲是值得的。
儘管我在範例程式碼沒有轉寄DocumentComplete事件,但是這僅僅是基於安全性考慮。自動化瀏覽器可以很容易地實現廣告視窗過濾、自動填寫表單,頁面分析等指令碼。如何編寫這些指令碼取決於你自己的需要。
參考
- 瀏覽器
- About the Browser
- Reusing MSHTML
- WebBrowser Customization(自訂瀏覽器)
- Loading HTML content from a Stream(使用流來獲得/設定網頁的內容)
- Reusing the WebBrowser Control
- Using MFC to Host a WebBrowser Control
- 安全
- About Cross-Frame Scripting and Security
- 文件物件模型
- About the DHTML Object Model
- About the W3C Document Object Model
- Working with Windows, Frames, and Dialog Boxes
- 自動完成
- Using AutoComplete in HTML Forms
- 在應用程式中整合自動完成功能