程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 獲得Win32窗口句柄的更好的方法

獲得Win32窗口句柄的更好的方法

編輯:關於VC++

“VC6中使用CHtmlView在對話框控制中顯示HTML文件”

“如何禁用HTML頁面的上下文菜單”

“Convert CHtmlView to CHtmlCtrl...”

這三篇文章的原文實際上都出自 MSDN Magazine 及其前身 MSJ 的“C++ Q&A”專欄作家 Paul DiLascia 之手。此君從1995年開始就成為 MS 在 C++/MFC 方面的高級寫手,Paul 在 Windows 應用開發領域的造詣頗深。直到現在仍然在為該專欄撰寫技術文章,只不過其文章已不僅僅涉及 C++/MFC,偶爾也寫一些 C#。為了微軟的 .NET 戰略,Paul 可謂忠實、勤奮和敬業......

本文是以上文章所涉及內容的延伸。如果你已經對前述文章討論的東西了然於心,那麼可以直接切入本文的正題。如果你沒有看過上面提到的文章,建議最好先看一下,以便了解本文內容的背景,這樣對於理解本文所討論的東西會更有幫助。

背景簡介

話說在第六期的“VC6中使用CHtmlView在對話框控制中顯示HTML文件”一文中,主要討論並示范了如何改進 MFC 的 CHtmlView 類,使它能處理基於對話框的應用和各種其它類型的窗口應用,其思路是通過創建 CHtmlView 的派生類 CHtmlCtrl,使得 CHtmlView 擺脫了對文檔/視圖的依賴。

在第十一期的“如何禁用HTML頁面的上下文菜單”一文中,主要討論了如何通過子類化 IE 服務器窗口(Internet Explorer_Server)來禁用 CHtmlCtrl 的上下文菜單。實際上,真正顯示HTML的窗口並不是浏覽器(CHtmlView/CHtmlCtrl)窗口,而是一個名為“Internet Explorer_Server”的最底層的子孫窗口。這一點可以通過 Spy++ 來證實,為了獲得該窗口的句柄(HWND),在實現過程中使用了一個函數 GetLastChild(HWND hwndParent),其定義如下:

static HWND GetLastChild(HWND hwndParent)
{
  HWND hwnd = hwndParent;
  while (TRUE) {
   HWND hwndChild = ::GetWindow(hwnd, GW_CHILD);
   if (hwndChild==NULL)
     return hwnd;
   hwnd = hwndChild;
  }
  return NULL;
}

通過這個函數返回某個父窗口下的最後一個子窗口,也就是說返回子窗口的子窗口的子窗口......直到不再有子窗口為止。可惜這個函數要獲得正確的運行結果是有前提的,那就是窗口層次只能是一層,並且最終的窗口後裔是“Internet Explorer_Server”窗口。 在通常情況下,這個假設都成立。不幸的是,如果 HTML 文檔中包含象 ComBoxes(組合框) 這樣的控制時,這個假設就不靈了。用 Spy++ 不難發現情況並不象你期望的那樣─Internet Explorer_Server是最後的子窗口。實際上,在IE中,Edit 和 Button 控制並非人們所想象的那樣是子窗口。

獲得 Win32 窗口句柄的更好的方法

為了解決這個問題,本文設計了一個更加完善的類:CFindWnd,用更好的算法專門來獲取 IE 窗口。CFindWnd 查找某個窗口(給定窗口名字)的第一個子窗口。 例如,它的使用方法如下:

CFindWnd ies(m_hWnd, "Internet Explorer_Server");
myHwndIE = ies.m_hWnd;

這個類的構造函數調用函數:

FindChildClassHwnd(hwndParent, (LPARAM)this)

函數,該函數又調用:

EnumChildWindows 和 FindWindowEx  

搜索所有後裔窗口直到找到類名匹配窗口為止。FindWindow 用來查找最頂層窗口,而搜索子窗口還得用 FindWindowEx,它是 Win32 API 函數。CFindWnd 返回第一個匹配的窗口,所以它只被用於查找你期望只有一個實例的窗口。通常在搜索特定窗口時,一般最保險的做法都是檢查窗口類名。

百家爭鳴

有一個讀者來信指出:根本沒有必要使用子類IE窗口的方法來禁用上下文菜單。完全可以在 CHtmlCtrl 內部實現,象下面這樣:

BOOL CHtmlCtrl::PreTranslateMessage(MSG* pMsg)
{
 if (pMsg->message == WM_CONTEXTMENU)
 return TRUE; // eat it
 return CHtmlView::PreTranslateMessage(pMsg);
}

這樣做是可行的,因為MFC實現了非常有獨創性的、強大的特性─在 CWinThread 的主消息泵中,MFC 調用 CWnd::WalkPreTranslateTree 函數。這個函數循環消息目的地窗口的所有父窗口,調用每一個父窗口的 PreTranslateMessage ,一旦截獲消息發送到後裔窗口則停止循環。非常聰明!

經驗證明:要使前面的代碼段按照期望的結果運行,你還必須截獲 WM_RBUTTONDOWN 和 WM_RBUTTONDBLCLK 消息,同時還要做必要的檢查以保證目標窗口的類名是 “Internet Explorer_Server”,這樣就不會意外地捕獲其它子窗口的上下文菜單(除非你確實要這麼做)。下面是 CHtmlCtrl::PreTranslateMessage 的最終代碼:

頭文件 HtmlCtrl.h
////////////////////////////////////////////////////////////////
#pragma once
////////////////////////////////////////////////////////////////
// 該結構在命令映射中定義一個入口,這個映射將文本串映射到命令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 {
protected:
  HTMLCMDMAP* m_cmdmap; // command map
  BOOL m_bHideMenu; // hide context menu
public:
  CHtmlCtrl() : m_bHideMenu(FALSE), m_cmdmap(NULL) { }
  ~CHtmlCtrl() { }
  // get/set HideContextMenu property
  BOOL GetHideContextMenu()     { return m_bHideMenu; }
  void SetHideContextMenu(BOOL val) { m_bHideMenu=val; }
  // Set doc contents from string
  HRESULT SetHTML(LPCTSTR strHTML);
  // set command map
  void SetCmdMap(HTMLCMDMAP* val)  { m_cmdmap = val; }
  // create control in same place as static control
  BOOL CreateFromStatic(UINT nID, CWnd* pParent);
  // create control from scratch
  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 doc/view 框架的依賴,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
////////////////////////////////////////////////////////////////
// 可用於對話框和其它窗口,不需要框架
//
// 特性:
// - 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)
// 一個很有用的宏,用於檢查 HRESULT
#define HRCHECK(x) hr = x; if (!SUCCEEDED(hr)) { \
  TRACE(_T("hr=%p\n"),hr);\
  return hr;\
}
... // same as earlier version
// Return TRUE if hwnd is Internet Explorer window.
inline BOOL IsIEWindow(HWND hwnd)
{
  static LPCSTR IEWNDCLASSNAME = "Internet Explorer_Server";
  char classname[32]; // always char, never TCHAR
  GetClassName(hwnd, classname, sizeof(classname));
  return strcmp(classname, IEWNDCLASSNAME)==0;
}
//////////////////
// 重寫後捕獲 "Internet Explorer_Server" 窗口上下文菜單消息.
//
BOOL CHtmlCtrl::PreTranslateMessage(MSG* pMsg)
{
  if (m_bHideMenu) {
   switch (pMsg->message) {
   case WM_CONTEXTMENU:
   case WM_RBUTTONUP:
   case WM_RBUTTONDOWN:
   case WM_RBUTTONDBLCLK:
     if (IsIEWindow(pMsg->hwnd)) {
      if (pMsg->message==WM_RBUTTONUP)
        // let parent handle context menu
        GetParent()->SendMessage(WM_CONTEXTMENU, pMsg->wParam,
         pMsg->lParam);
      return TRUE; // eat it
     }
   }
  }
  return CHtmlView::PreTranslateMessage(pMsg);
}
//////////////////
// 重寫後將 "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); // call virtual handler fn
   *pbCancel = TRUE; // cancel navigation
  }
}
//////////////////
// 當浏覽器試圖導航到 "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;
}

和以前相比,這個類功能更強,具備了 Get/SetHideContextMenu 屬性處理機制,對 WM_CONTEXTMENU 消息的處理采取了發送到父窗口,而不是過濾掉它。這樣就使得你能實現自己的上下文菜單。注意 WM_CONTEXTMENU 消息的發送是在鼠標右鍵向上釋放的時候進行的,而不是按下時處理的。具體細節請參考源代碼。

動態生成並顯示 HTML 文檔

前面的例子程序在處理HTML文檔時,都是把它作為應用程序的資源進行處理的,如果碰到需要動態產生HTML文檔信息的情況,這種處理方法便無法滿足需要,那麼如何動態 顯示生成的HTML文檔呢?下面我們就來解決這個問題。

大家知道,將純文本格式化成HTML是再簡單不過的事情了,雖然用C++來實現這個過程有點單調乏味,但仍然是可以做到的。如果你曾經寫過JavaScript腳本,那麼肯定知道加載頁面時 ,可以調用 document.write 直接將HTML寫到文檔中。其實在C++中做法也一樣,只不過編碼看起來會有些繁瑣和凌亂,因為要用到 COM 接口 IHTMLDocument2 以及 BSTRs、SAFEARRAYs 等數據類型來處理字符串。所幸的是 ATL 具備了大量的類可以助我們一臂之力。

下圖是本文例子程序運行時的畫面,使用 HTML 文檔格式動態顯示頂層窗口的信息:

圖一 例子程序運行畫面

這個程序的代碼原型來自第十一期《在線雜志》中“如何禁用HTML頁面的上下文菜單” 一文的例子,它實現了一個 HTML “關於”對話框,其顯示的HTML頁面是從資源中加載的:

m_page.LoadFromResource(_T("about.htm"));

CHtmlView::LoadFromResource 打開 res://AboutHtml.exe/about.htm,這裡“AboutHtml.exe” 是可執行程序的實際名字,“res://”是一個偽協議。為了顯示頂層窗口的信息,最好的辦法是動態產生 HTML 頁面,而不是從資源中加載,為此我在 CHtmlCtrl類中添加了一個新函數:CHtmlCtrl::SetHTML,

////////////////////////////////////
// 通過串設置 HTML 文檔內容
//
HRESULT CHtmlCtrl::SetHTML(LPCTSTR strHTML)
{
  HRESULT hr;
  // 獲得文檔對象
  SPIHTMLDocument2 doc = GetHtmlDocument();
  // 創建只有一個元素(串)的 BSTR 數組元素
  // IHTMLDocument2::write.
  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)); // 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文檔, 該文檔列出所有頂層窗口的信息,然後將它們顯示出來,如圖一所示。

例子程序的參考代碼如下:

//////////////////////////////////////
// HtmlApp.cpp
class CMyApp : public CWinApp {
public:
  virtual BOOL InitInstance();
protected:
  afx_msg void OnAppAbout();
  DECLARE_MESSAGE_MAP()
} theApp;
BEGIN_MESSAGE_MAP(CMyApp, CWinApp)
  ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
END_MESSAGE_MAP()
BOOL CMyApp::InitInstance()
{
  // Create main frame window (don''t use doc/view stuff)
  CMainFrame* pMainFrame = new CMainFrame;
  if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
   return FALSE;
  pMainFrame->ShowWindow(m_nCmdShow);
  pMainFrame->UpdateWindow();
  m_pMainWnd = pMainFrame;
  return TRUE;
}
//////////////////////////////////////////////////////
// “關於”對話框使用 HTML 控制顯示內容.
//
class CAboutDialog : public CDialog {
protected:
  CHtmlCtrl m_page; // HTML control
  virtual BOOL OnInitDialog();
public:
  CAboutDialog() : CDialog(IDD_ABOUTBOX, NULL) { }
  DECLARE_DYNAMIC(CAboutDialog)
};
IMPLEMENT_DYNAMIC(CAboutDialog, CDialog)
///////////////////////
// 初始化“關於”對話框
//
BOOL CAboutDialog::OnInitDialog()
{
  // cmd map for CHtmlCtrl handles "app:ok"
  static HTMLCMDMAP AboutCmds[] = {
   { _T("ok"), IDOK },
   { NULL, 0 },
  };
  VERIFY(CDialog::OnInitDialog());
  VERIFY(m_page.CreateFromStatic(IDC_HTMLVIEW, this)); // create HTML
                            // ctrl
  m_page.SetHideContextMenu(TRUE); // hide context
                            // menu
  m_page.SetCmdMap(AboutCmds); // set command
                            // table
  m_page.LoadFromResource(_T("about.htm")); // load HTML from
                            // resource
  return TRUE;
}
/////////////////////
// 運行“關於”對話框
void CMyApp::OnAppAbout()
{
  static CAboutDialog dlg; // static to remember state of hyperlinks
  dlg.DoModal(); // run it
}
////////////////////////////////////////////////////////////////
// MainFrm.h
// 典型的主框架處理例程......
class CMainFrame : public CFrameWnd {
public:
  CMainFrame(){ }
  virtual ~CMainFrame() { }
protected:
  CHtmlCtrl  m_wndView; // CHtmlCtrl 作為主窗口視圖
  CStatusBar m_wndStatusBar; // status line
  CToolBar  m_wndToolBar; // toolbar
  afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
  afx_msg void OnContextMenu(CWnd* pWnd, CPoint pos);
  // helper to format main window HTML
  CString FormatWindowListHTML();
  DECLARE_DYNCREATE(CMainFrame)
  DECLARE_MESSAGE_MAP()
};
////////////////////////////////////////////////////////////////
// MainFrm.cpp
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
  ON_WM_CREATE()
  ON_WM_CONTEXTMENU()
END_MESSAGE_MAP()
// Commmand map for app: commands in main window HTML.
HTMLCMDMAP MyHtmlCmds[] = {
  { _T("about"), ID_APP_ABOUT },
  { _T("exit"), ID_APP_EXIT },
  { NULL, 0 },
};
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
  VERIFY(CFrameWnd::OnCreate(lpCreateStruct)==0);
  ...
  // create/init html control as view
  VERIFY(m_wndView.Create(CRect(), this, AFX_IDW_PANE_FIRST));
  m_wndView.SetHideContextMenu(TRUE); // 隱藏上下文菜單
  m_wndView.SetCmdMap(MyHtmlCmds); // 設置命令
  m_wndView.Navigate(_T("about:blank")); // 創建文檔
  m_wndView.SetHTML(FormatWindowListHTML()); // 設置獲取的HTML內容串
  SetActiveView(&m_wndView); // 設置MFC活動視圖
  return 0;
}
///////////////////////////////////////////////////////////////////
// 處理上下文菜單的命令函數,當前該函數只是顯示TRACE信息,以示被調用過。
//
void CMainFrame::OnContextMenu(CWnd* pWnd, CPoint pos)
{
  TRACE(_T("CMainFrame::OnContextMenu\n"));
}
//////////////////////////////////////////////////////////////////////
// 這個函數創建在主窗口視圖中顯示的 HTML。
// EnumWindows 回調該函數:如果窗口可見,則將窗口信息添加到 HTML table中。
//
static BOOL CALLBACK MyEnumWindowsProc(HWND hwnd, LPARAM lp)
{
  DWORD style = GetWindowLong(hwnd, GWL_STYLE);
  if (style & WS_VISIBLE) {
   CString& s = *(CString*)lp;
   char cname[256];
   GetClassName(hwnd, cname, sizeof(cname));
   TCHAR text[1024];
   GetWindowText(hwnd, text, sizeof(text)/sizeof(text[0]));
   CString temp;
   temp.Format(_T("<TR><TD>%p </TD><TD>%s</TD><TD>%s</TD></TR>\n"),
     hwnd, cname, text);
   s += temp;
  }
  return TRUE;
}
////////////////////////////////////////////////////////////////////////
// 該函數創建一個文本串,這個串就是要顯示在主窗口的 HTML 文檔。
CString CMainFrame::FormatWindowListHTML()
{
  // start w/top matter
  CString html = _T("<HTML><BODY STYLE=\"font-family:Verdana;\" \
  link=\"#02B7B7\" vlink=\"#02B7B7\">\n\
  <TABLE WIDTH=\"100%\">\n\
  <TR><TD VALIGN=top><A target=\"new\" HREF=\"http://www.vckbase.com\"><IMG \
  BORDER=0 ALT=\"VCKBASE Online Journal\" SRC=\"res://AboutHtml3.exe/mlogo.gif\"></A></TD>\
  <TD COLSPAN=2><B>AboutHtml3 例子程序 -- 頂層可見窗口清單</B><BR>\n\
  <SMALL><A target=\"new\" \
  HREF=\"http://www.vckbase.com\"> VC知識庫 </A></SMALL></TD></TR>\n\
  <TR><TD><B>窗口句柄(hwnd)</B></TD><TD><B>窗口類名</B></TD><TD WIDTH=75%><B>窗口標題</B>\
  </TD><TR>\n");

  // enumerate top-level windows to append their info
  EnumWindows(MyEnumWindowsProc, (LPARAM)&html);
  // append bottom matter. note commands app:about and app:exit
  html += _T("</TABLE>\n\
  <P><B>[<A HREF=\"app:about\">關於</A>] \
  [<A HREF=\"app:exit\">退出</A>]</B>\n\
  </BODY>\n\
  </HTML>");
  return html;
}

最後,我想說明一下本文例子程序中其它的一些編程技巧和訣竅,主要是針對CHtmlCtrl類的功能擴展。早在“VC6中使用CHtmlView在對話框控制中顯示HTML文件”(第六期)一文中,我曾經演示了如何實現“app:”偽協議來創建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;\
}
... // same as earlier version
// 如果 hwnd 是 IE 窗口,則返回 TRUE。
inline BOOL IsIEWindow(HWND hwnd)
{
  static LPCSTR IEWNDCLASSNAME = "Internet Explorer_Server";
  char classname[32]; // 必須是 char 類型, 不能是 TCHAR
  GetClassName(hwnd, classname, sizeof(classname));
  return strcmp(classname, IEWNDCLASSNAME)==0;
}
///////////////////////////////////////////////////////////////////
// 重寫函數捕獲 "Internet Explorer_Server" 窗口上下文菜單消息。
//
BOOL CHtmlCtrl::PreTranslateMessage(MSG* pMsg)
{
  if (m_bHideMenu) {
   switch (pMsg->message) {
   case WM_CONTEXTMENU:
   case WM_RBUTTONUP:
   case WM_RBUTTONDOWN:
   case WM_RBUTTONDBLCLK:
     if (IsIEWindow(pMsg->hwnd)) {
      if (pMsg->message==WM_RBUTTONUP)
        // 讓父窗口處理上下文菜單
        GetParent()->SendMessage(WM_CONTEXTMENU, pMsg->wParam,
         pMsg->lParam);
      return TRUE; // eat it
     }
   }
  }
  return CHtmlView::PreTranslateMessage(pMsg);
}
////////////////////////////////////////////////////////////////////
// 重寫函數傳遞 "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 時,如果關閉程序會遇到麻煩(我費了好大的勁才發現這個問題)。

最後,祝大家編程愉快!

本文配套源碼

  1. 上一頁:
  2. 下一頁:
欄目導航
Copyright © 程式師世界 All Rights Reserved