获得 Win32 窗口句柄的更好的方法 ----动态生成并显示 HTML 文档 ----再谈禁用HTML的上下文菜单...

 

翻译文档 本文适合中级读者 已阅读33548次 ] 
  •                  
  •                  
  •                  
  •       

  • 获得 Win32 窗口句柄的更好的方法
    ----动态生成并显示 HTML 文档
      ----再谈禁用HTML的上下文菜单...

    编译/NorthTibet

    原文出处:MSDN Magazine C++ Q&A


    下载源代码

     译者注:
    在以前的VC知识库 Online Journal 上有三篇文章:

    “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 时,如果关闭程序会遇到麻烦(我费了好大的劲才发现这个问题)。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值