//////////////////////////////////////////////////////////////// // 該結構在命令映射中定義一個入口,這個映射將文本串映射到命令IDs, // 如果命令映射中有一個映射到 ID_APP_ABOUT 的入口 “about”,並且 // HTML 有一個鏈接錨 <A HREF="app:about">,那麼單擊該鏈接時將執行 // ID_APP_ABOUT 命令。爲了設置這個映射,調用 CHtmlCtrl::SetCmdMap. // // struct HTMLCMDMAP { LPCTSTR name; // command name used in "app:name" HREF in // <A UINT nID; };
////////////////// // 這個類將 CHtmlView 轉換爲普通的能在對話框和框架中使用的控制 // class CHtmlCtrl : public CHtmlView {}
////////////////// // 當瀏覽器試圖導航到 "app:foo" 時調用該函數. // 默認的處理例程查找"foo"命令的命令映射,並向找到的父窗口發送 // WM_COMMAND 消息。調用 SetCmdMap 設置命令映射。如果要實現更 // 複雜的處理,只要重寫這個函數即可. // void CHtmlCtrl::OnAppCmd(LPCTSTR lpszCmd) { if (m_cmdmap) { for (int i=0; m_cmdmap[i].name; i++) { if (_tcsicmp(lpszCmd, m_cmdmap[i].name) == 0) // Use PostMessage to avoid problems with exit command. (Let // browser finish navigation before issuing command.) GetParent()->PostMessage(WM_COMMAND, m_cmdmap[i].nID); } } }
////////////////// // 將串轉換爲 HTML 文檔 // HRESULT CHtmlCtrl::SetHTML(LPCTSTR strHTML) { HRESULT hr; // Get document object SPIHTMLDocument2 doc = GetHtmlDocument(); // Create string as one-element BSTR safe array for // IHTMLDocument2::write. CComSafeArray<VARIANT> sar; sar.Create(1,0); sar[0] = CComBSTR(strHTML); // open doc and write LPDISPATCH lpdRet; HRCHECK(doc->open(CComBSTR("text/html"), CComVariant(CComBSTR("_self")), CComVariant(CComBSTR("")), CComVariant((bool)1), &lpdRet)); HRCHECK(doc->write(sar)); // write contents to doc HRCHECK(doc->close()); // close lpdRet->Release(); // release IDispatch returned return S_OK; }
下面我們一步一步來分析實現過程,首先必須獲取 IHTMLDocument2 接口:
SPIHTMLDocument2 doc = GetHtmlDocument();SPIHTMLDocument2 與 CComQIPtr<IHTMLDocument2> 一樣是一個指向 IHTMLDocument2 的ATL智能指針,(當今 Windows 編程已進入 COM 時代,作爲一名編寫 Windows 應用程序的開發人員,如果你使用 COM 技術,但沒有用過智能指針,那麼這段代碼會對你有所裨益),接着,必須創建一個SAFEARRAY,以便存放作爲 BSTR 數組唯一元素的 HTML 串,SAFEARRAY是一個 COM 數據結構,其作用是在不同平臺之間安全地傳遞數組數據,ATL提供了 CComBSTR 和 CComSafeArray 兩個類,爲開發人員在處理 BSTRs 和安全數組時減輕了許多痛苦:
// strHTML is LPCTSTR CComSafeArray<VARIANT> sar; sar.Create(1,0); sar[0] = CComBSTR(strHTML);如果不借助於 CComSafeArray 和 CComBSTR,而是用下列這些 API 函數來實現相同的處理,如 SafeArrayCreateVector,SafeArrayAccessData, 和 SafeArrayUnaccessData,那麼至少還得寫10-20行無聊的代碼。一旦你上手了智能指針,你會覺得ATL的這些東西用起來真的很爽。
現在有了文檔對象以及在安全數組中的內容,接下來便可以打開文檔,進行寫入操作,關閉文檔等等。IHTMLDocument2::write需要 VARIANTS 和 BSTRs 類型的數據,這裏ATL又一次顯示了它的優勢:
LPDISPATCH lpdRet; doc->open(CComBSTR("text/html"), // MIME type CComVariant(CComBSTR("_self")), // open in same window CComVariant(CComBSTR("")), // no features CComVariant((bool)1), // replace history entry &lpdRet)); // IDispatch returned doc->write(sar); // write it doc->close(); // close lpdRet->Release();CHtmlCtrl::SetHTML 非常好用。使用它時有一個技巧:當第一次創建 CHtmlCtrl 時,它沒有文檔(GetHtmlDocument返回NULL)。所以在調用 CHtmlCtrl::SetHTML 之前,你必須創建一個文檔,最簡單的方法就是打開一個空文檔,就象下面這樣:
m_wndView.Navigate(_T("about:blank"));此外,如果HTML很簡單,你可以用 about: 代替 CHtmlCtrl::SetHTML 來得到HTML,如下面的代碼:
m_wndView.Navigate(_T("about:<HTML><B>hello, world</B></HTML>"));針對簡單的HTML可以這麼做,如果比較複雜的文檔則要調用 SetHTML。本文附帶的例子程序動態構造了一個包含圖像、表格、鏈接等元素的HTML文檔, 該文檔列出所有頂層窗口的信息,然後將它們顯示出來
你可以象下面這樣添加一個鏈接:
<A HREF="app:about">About</A>然後,CHtmlCtrl::OnBeforeNavigate2 會識別出“app:”僞協議並以“about”作爲參數調用專門的虛函數 CHtmlCtrl::OnAppCmd 。你可以創建自己的命令並在派生類中改寫 OnAppCmd 來處理自己建立的命令。使用了 CHtmlCtrl 一段時間後。我發現經常需要派生 CHtmlCtrl 類,每次都得改寫這個函數,自己感覺很麻煩!爲了簡化這個過程,我發明了一個簡單的命令映射機制,利用這種機制可以輕鬆將“app:command”之類的轉換爲通常熟知的 WM_COMMAND 命令 ID:
HTMLCMDMAP MyHtmlCmds[] = { { _T("about"), ID_APP_ABOUT }, { _T("exit"), ID_APP_EXIT }, { NULL, 0 }, };這個映射機制的使用方法是象下面這樣調用 CHtmlCtrl::SetCmdMap 函數:
m_wndHtmlCtrl.SetCmdMap(MyHtmlCmds);這樣一來,當用戶單擊“app:about”鏈接時,CHtmlCtrl::OnAppCmd 便會搜索命令映射,找到“about”入口,然後將與ID_APP_ABOUT 對應的 WM_COMMAND 消息發送到其父窗口,這個技巧主要是仰仗MFC神奇的命令路由通道實現的,藉助此通道,任何窗口都可以處理此命令。真是爽啊!本文例子程序正是用這種特性將“關於”和“退出”命令作爲HTML鏈接直接添加到主窗口中。CHtmlCtrl類實現的細節代碼如下:
//////////////////////////////////////////////////////////////// // HtmlCtrl.h #pragma once ///////////////////////////////////////////////////////////////////////// // 此結構定義一個命令映射入口,映射將文本串映射到命令IDs。如果你的命令映射 // 入口包含 "about" 映射到ID_APP_ABOUT,並且HTML文檔中有一個錨點鏈接是 // <A HREF="app:about">,則單擊該鏈接將調用 ID_APP_ABOUT 命令。設置命令 // 映射的方法是調用 CHtmlCtrl::SetCmdMap 函數. // struct HTMLCMDMAP { LPCTSTR name; // 用於" <A HREF..." 中的 "app:name" 的命令名. UINT nID; }; //////////////////////////////////////////////////////////////////////// // 將 CHtmlView 轉換爲框架或對話框中常規控制的類.類似於CListView/CListCtrl // 和 CTreeView/CTreeCtrl // class CHtmlCtrl : public CHtmlView { protected: HTMLCMDMAP* m_cmdmap; // 命令映射 BOOL m_bHideMenu; // 隱藏上下文菜單 public: CHtmlCtrl() : m_bHideMenu(FALSE), m_cmdmap(NULL) { } ~CHtmlCtrl() { } // 獲取/設置 HideContextMenu 屬性 BOOL GetHideContextMenu() { return m_bHideMenu; } void SetHideContextMenu(BOOL val) { m_bHideMenu=val; } // 根據串創建 HTML 文檔 HRESULT SetHTML(LPCTSTR strHTML); // 設置命令映射 void SetCmdMap(HTMLCMDMAP* val) { m_cmdmap = val; } // 先創建一個靜態控制,然後用相同的再創建一個控制 BOOL CreateFromStatic(UINT nID, CWnd* pParent); // 創建控制 BOOL Create(const RECT& rc, CWnd* pParent, UINT nID, DWORD dwStyle = WS_CHILD|WS_VISIBLE, CCreateContext* pContext = NULL) { return CHtmlView::Create(NULL, NULL, dwStyle, rc, pParent, nID, pContext); } // 重寫用以解釋子窗口消息來禁用上下文菜單 virtual BOOL PreTranslateMessage(MSG* pMsg); // 通常,CHtmlView 是在 PostNcDestroy 中將自己摧毀, // 但用於窗口控制,我們不想那麼做,因爲控制通常是作爲 // 另一個窗口對象的成員來實現的. // virtual void PostNcDestroy() { } // 重寫該函數以便旁路掉對 MFC 文檔/視圖框架的依賴. 此處是 CHtmView 依賴框架才能生存的唯一一個地方. afx_msg void OnDestroy(); afx_msg int OnMouseActivate(CWnd* pDesktopWnd, UINT nHitTest, UINT msg); // 改寫該函數用以捕獲 "app:" 僞協議 virtual void OnBeforeNavigate2( LPCTSTR lpszURL, DWORD nFlags, LPCTSTR lpszTargetFrameName, CByteArray& baPostedData, LPCTSTR lpszHeaders, BOOL* pbCancel ); // 你可以重寫這個虛函數用以處理 "app:" 命令. // 如果不涉及命令映射,則不用該寫. virtual void OnAppCmd(LPCTSTR lpszCmd); DECLARE_MESSAGE_MAP(); DECLARE_DYNAMIC(CHtmlCtrl) }; HtmlCtrl.cpp /////////////////////////////////////////////////////////////////////// // 實現 CHtmlCtrl 類 — 窗口控制中的 Web 瀏覽器。重寫 CHtmlView 以便擺脫 // 框架約束,可以用於對話框或任何其它窗口 // // 特性: // - SetCmdMap 使你能爲"app:command"鏈接設置命令映射. // - SetHTML 使你能將一個串設置成HTML文檔內容. #include "StdAfx.h" #include "HtmlCtrl.h" // 這個宏聲明的 typedef 用於 ATL 智能指針,如:SPIHTMLDocument2 #define DECLARE_SMARTPTR(ifacename) typedef CComQIPtr<ifacename> SP##ifacename; // IHTMLDocument2 接口智能指針 DECLARE_SMARTPTR(IHTMLDocument2) // 這是個很有用的宏,用來檢查 HRESULTs #define HRCHECK(x) hr = x; if (!SUCCEEDED(hr)) { \ TRACE(_T("hr=%p\n"),hr);\ return hr;\ }
//////////////////////////////////////////////////////////////////// // 重寫函數傳遞 "app:" 鏈接到虛函數,而不是瀏覽器。 // void CHtmlCtrl::OnBeforeNavigate2( LPCTSTR lpszURL, DWORD nFlags, LPCTSTR lpszTargetFrameName, CByteArray& baPostedData, LPCTSTR lpszHeaders, BOOL* pbCancel ) { const char APP_PROTOCOL[] = "app:"; int len = _tcslen(APP_PROTOCOL); if (_tcsnicmp(lpszURL, APP_PROTOCOL, len)==0) { OnAppCmd(lpszURL + len); // 調用虛擬函數例程 *pbCancel = TRUE; // 取消導航 } } //////////////////////////////////////////////////////////////////////// // 當瀏覽器試圖導航到 "app:foo"時調用此函數. 缺省的命令處理映射爲"foo",如果 // 找到命令ID,則向父窗口發送一個 WM_COMMAND 消息,調用 SetCmdMap 設置命令 // 映射。如果你想要作稍微複雜一些的處理,必須重寫 OnAppCmd。 // void CHtmlCtrl::OnAppCmd(LPCTSTR lpszCmd) { if (m_cmdmap) { for (int i=0; m_cmdmap[i].name; i++) { if (_tcsicmp(lpszCmd, m_cmdmap[i].name) == 0) // 使用 PostMessage 發送消息,避免退出命令出現的問題 (在發出命令前瀏覽器結束導航。) GetParent()->PostMessage(WM_COMMAND, m_cmdmap[i].nID); } } } /////////////////// // 將串轉爲HTML文檔 // HRESULT CHtmlCtrl::SetHTML(LPCTSTR strHTML) { HRESULT hr; // 獲取文檔對象 SPIHTMLDocument2 doc = GetHtmlDocument(); // 創建串,將它作爲BSTR數組的唯一個元素,因爲 IHTMLDocument2::write 使用BSTR類型 CComSafeArray<VARIANT> sar; sar.Create(1,0); sar[0] = CComBSTR(strHTML); // 打開文檔進行寫操作 LPDISPATCH lpdRet; HRCHECK(doc->open(CComBSTR("text/html"), CComVariant(CComBSTR("_self")), CComVariant(CComBSTR("")), CComVariant((bool)1), &lpdRet)); HRCHECK(doc->write(sar)); // 將內容寫入文檔 HRCHECK(doc->close()); // 關閉文檔 lpdRet->Release(); // 釋放 IDispatch 然後返回 return S_OK; }最後一個關鍵的地方是 CHtmlCtrl::OnAppCmd 必須通過 PostMessage 發送命令,而不是用 SendMessage,因爲如果不這樣做,你會發現當執行 OnBeforeNavigate2 時,如果關閉程序會遇到麻煩(我費了好大的勁才發現這個問題)。