win32 application invoke the html files ----2

////////////////////////////////////////////////////////////////
// 該結構在命令映射中定義一個入口,這個映射將文本串映射到命令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 時,如果關閉程序會遇到麻煩(我費了好大的勁才發現這個問題)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章