MFC界面--利用CHtmlView和HTML制作新风格的界面(包括CView和Dialog)

一、文档视图形式,以一个视图cview(chtmlview)作为首页界面

转自:http://liuxiang031130.blog.163.com/blog/static/1176665520111117914849/

用过 Outlook Express 的很多人都对其第一页的 HTML 界面感到新奇,很明显这是使用 DHTML 技术,加入了一些 Java Script 的一个网页,但它能够和应用程序进行交互操作。其实利用 VC6.0 的新加入的 MFC 类 CHtmlView ,你也可以实现这样一个令人激动的程序界面。这个界面可以利用 HTML ,这是很有意义的,想象一下,你在 HTML 中实现的效果,全部可以放在程序的界面中,而你所做的只是写了一个 HTML 文件和少量的编程。
   CHtmlView 是 MFC 新加入的一个类,如果你看一下 MFC 关于这个类的源代码,就会发现在其内部封装了接口 IWebBrowser2 。这个接口实际上是 IE 的接口。也就是说,你可以通过这个类来调用强大的 IE 来显示 HTML 页面,每个人都可以利用这个类,轻松的写出一个浏览器。 VC 中也带了一个使用这个类写的浏览器的例子 MFCIE ,可以参考。

   利用 CHtmlView 显示页面是很简单的。你只要在资源中加入 HTML 页面资源,程序中加入下面一句语句就可以实现。

   LoadFromResource(ID_XXX);//ID_XXX 是资源的定义

   解决这个问题的核心是如何利用 CHtmlView 把用户对 HTML 页面的操作传送给应用程序。这里看似很神秘,但实际上有一个技巧,可以截获用户的输入。在类 CHtmlView 中有一个事件 OnBeforeNavigate2 ,当浏览器被重新导向之前,会激活这个事件。比如说每当用户按下了 HTML 中的超级链接,或者用户在地址栏输入新的地址,还有程序员调用接口的 Navigate 方法,浏览器要转向新地址的时候,都会激活这个事件。而在这里,当你实现 HTML 界面的时候,用户通过点击页面上的链接来激活命令,所以我们可以在这个事件里做一些处理,这个事件的参数中有两个比较重要, lpszURL 就是在 HTML 页面中 href 指定的地址,你可以给各个链接设置相应的地址,通过这个参数的内容就可以识别用户点击的链接。而 pbCancel 可以指定是否取消导向,只要写入 *pbCancel = TRUE ,导向就被取消,不会发生了, CHtmlView 显示的还是现在的页面。让我们来看一下我写的例子程序 HtmlGUI 。
void CHtmlGUIView::OnBeforeNavigate2(LPCTSTR lpszURL, DWORD nFlags, LPCTSTR lpszTargetFrameName, CByteArray& baPostedData, LPCTSTR lpszHeaders, BOOL* pbCancel)
{
// TODO: Add your specialized code here and/or call the base class

if (ProcessCommand(lpszURL))
{
//URL was processed by the programmer defined code.
//cancel navigation
*pbCancel = TRUE;
}
else
{
CHtmlView::OnBeforeNavigate2(lpszURL, nFlags, lpszTargetFrameName, baPostedData, lpszHeaders, pbCancel);
}

}

BOOL CHtmlGUIView::ProcessCommand(LPCTSTR lpszURL)
{
if ( _tcscmp(lpszURL, _T("app:link1")) == 0 )
{
AfxMessageBox("Link1 was pressed, you can process your command here!");
return TRUE;
}

if ( _tcscmp(lpszURL, _T("app:link2")) == 0 )
{
AfxMessageBox("Link2 was pressed, you can process your command here!");
return TRUE;
}
//NOT processed by me.
if ( _tcscmp(lpszURL, _T("app:about")) == 0 )
{
::SendMessage(AfxGetMainWnd()->GetSafeHwnd(), WM_MAINAPP_ABOUT,0,0);
return TRUE;
}
return FALSE;
}

   这里面我写了一个函数 BOOL CHtmlGUIView::ProcessCommand(LPCTSTR lpszURL) 来处理命令的映射,如果被导向的地址应该映射到程序的命令,则作相应的处理,并返回 TRUE ,表示已经被处理了,取消导向。否则返回 FALSE ,则调用 MFC 父类的函数,进行正常的操作。

   为了不露出破绽,最好屏蔽掉鼠标右键,所以还应该重载 PreTranslateMessage ,并加入如下代码。
BOOL CHtmlGUIView::PreTranslateMessage(MSG* pMsg)
{
// TODO: Add your specialized code here and/or call the base class
// Do not let View get Right button Message.
if ((pMsg->message == WM_RBUTTONDOWN) ||
(pMsg->message == WM_RBUTTONDBLCLK))
return TRUE;
else
return CHtmlView::PreTranslateMessage(pMsg);
}


   当用户按下右键或者双击右键的时候,返回 TRUE ,这样 CHtmlView 窗口就不会得到这两条消息了。当然你也可以利用页面中的 Script 来实现这个功能。

   另外在写 HTML 页面时有一些东西要注意。微软为了使资源中的 HTML 文件能够得到访问,为 IE 定义了一个可以称为 res 协议的东西。你可以在 IE 的地址栏中输入 res://x:\xxx\htmlgui.exe/gui.htm ( x:\xxx\ 为路径), IE 中应该显示出我的程序的 HTML 文件。其实当你用 IE 访问页面出错的的时候, IE 显示的出错信息也是放在 C:\WINDOWS\SYSTEM\SHDOCLC.DLL 中的。 HTML 中用到的图片,声音等等,显然也要放入程序资源中才比较好。所以这些东西要全部当作 HTML 加入到资源中。而且,最好直接使用文件名用作资源 ID ,在资源文件中按下面格式加入定义。

// HTML
GUI.HTM HTML DISCARDABLE "res\\gui.htm"
PIC1.JPG HTML DISCARDABLE "res\\pic1.jpg"
WRITE.GIF HTML DISCARDABLE "res\\write.gif"
CHIMES.WAV HTML DISCARDABLE "res\\Chimes.wav"

【注意,提前把相应的htm和gif、jpg、wav等文件拷贝到工程的res文件夹下,然后在资源文件(以代码形式打开查看)中写入上述语句即可。】

   虽然 VC 的资源编辑器会把他们当作文本来显示,但是不要担心。只要你不要按文本去编辑他们就行了, CHtmlView 在加载页面的时候会正确显示他们的。不过千万不要把他们当作自定义或其他资源来加入,如果你那样做了, CHtmlView 加载时反而会不认识他们。为了确保 HTML 被正确显示,所有的图片和声音能被找到,所以在 HTML 文件中加入下面一行。

   < BASE url="res://HtmlGUI.exe/GUI.HTM">

   CHtmlView 还有一个函数 CHtmlView::GetHtmlDocument 可以得到 Ative Document 对象的 IDispatch 接口,然后你应该可以利用这个接口 QueryInterface 出其他的接口,利用这些接口你可以在程序中动态地控制 HTML 页面的内容。如果有兴趣,好好看看 MSDN 联机帮助,研究一下吧。

   HTML 界面是一种快速,方便,效果独特,容易排错的全新的界面技术,而由于微软封装了 OLE 接口 IWebBrowser2 ,使它的实现的技术细节变得这么简单,让我们来创造更新更酷的界面吧,祝编程愉快!

【补充:HTMLVIEW中打开网页,填表提交后 出现脚本错误提示 “当前的网页脚本发生错误”请问如何不让这种提示出现】
第一个方案  
SetSilent(TRUE);
如果第一个方案不行
上这段代码

C/C++ code
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//屏蔽IE的错误窗口
void  CSimOOView::OnNavigateComplete2( LPCTSTR  strURL)
{
     // TODO: 在此添加专用代码和/或调用基类
     CComPtr   spDisp   =   GetHtmlDocument(); 
  
     if (spDisp   !=   NULL) 
    
         CComPtr doc;
         spDisp->QueryInterface(IID_IHTMLDocument2,  reinterpret_cast < void **>(&doc));
         if (doc != NULL)
         {   
             IHTMLWindow2 * pIhtmlwindow2 = NULL;
             doc->get_parentWindow(&pIhtmlwindow2);
             if (pIhtmlwindow2 != NULL)
             {
                 //屏蔽javascript脚本错误的javascript脚本
                 //CString strJavaScriptCode = "function fnOnError(msg,url,lineno){alert('script error:\\n\\nURL:'+url+'\\n\\nMSG:'+msg +'\\n\\nLine:'+lineno);return true;}window.onerror=fnOnError;";
                 //CString strJavaScriptCode = "function fnOnError(msg,url,lineno){alert('script error:\\n\\nURL:'+url+'\\n\\nMSG:'+msg +'\\n\\nLine:'+lineno);return true;}";
                 CString strJavaScriptCode =  "function fnOnError(msg,url,lineno){return true;}window.onerror=fnOnError;" ;
  
                 //TRACE("%s\n" , strJavaScriptCode);
                 BSTR bstrScript = strJavaScriptCode.AllocSysString();
                 CString strLanguage( "JavaScript" );
                 BSTR bstrLanguage = strLanguage.AllocSysString();
                 long  lTime = 1 * 1000;
                 long  lTimeID = 0;
  
                 VARIANT varLanguage;
                 varLanguage.vt = VT_BSTR;
                 varLanguage.bstrVal = bstrLanguage;
                 VARIANT pRet;
                 //把window.onerror函数插入入当前页面中去
                 pIhtmlwindow2->execScript(bstrScript, bstrLanguage, &pRet);
                 ::SysFreeString(bstrScript);
                 ::SysFreeString(bstrLanguage);
                 pIhtmlwindow2->Release();
             }
         }
     }
     CHtmlView::OnNavigateComplete2(strURL);
}

 具体使用时,在具体CView::OnInitialUpdate()函数中添加如下代码,有三种形式,任选一种即可

法1:Navigate2(_T("www.google.com"),NULL,NULL); //此法是直接打开远程网页
法2:this->LoadFromResource(_T("firstpage.htm")); //此法是打开存放在exe中的二进制文件firstpage.hm。

法3:Navigate2(_T("WebPage\\FirstPage.htm"),NULL,NULL); //此法可以打开存放在工作目录下的WebPage目录中的FirstPage.htm文件,而且htm文件不必是二进制文件,甚至htm里面使用的图片等都可以不必是二进制文件,就像正常网页编辑那样使用,最重要的是法2不能使用外嵌的js文件,而法3想正常网页那样编辑然后嵌入到view中,没有法2那样的限制。

二、对话框形式,使用自定义的chtmlctrl控件

(1)转化htmlview为htmlctrl

转自:http://www.vckbase.com/index.php/wv/335

Wow!! 几篇让人拍案的文章,啃完之后大呼过瘾!想不到微软也有如此精通windows编程的家伙?! 此时此刻,俺想到的是分享给KBASE里的兄弟们啊! 没的说,掌声伺候!!!!

[NOTE]:

罗头说了,最好不要把Frame/Doc/View拆的妻离子散。是啊,本来好好的一家人,谁会那么残忍呢!? 嘿嘿,偶只是给他们弄了个远房的亲戚。:)

Now, Stop 费话ing!! Let''s go on the stuff…

首先,这里有两个难点需要解决! 一是:既然最后的产物是CHtmlCtrl,如何能象其他控件(比如Button)随意的丢到对话框里呢? COM->ActiveX?? 你说的,你自己做去吧!偶可是个COM稀里糊涂者!! 偶要比你想象地懒的多(鼓励程序员锻炼一下这种惰性! 好处多多)。 偶想,何不拉个替死鬼呢? 对了,CStatic不是可以随便被嗲来嗲去吗? 嗯,给它套上个SubclassDlgItem不就可以当成我们的CHtmlCtrl用了嘛! 有道理!! 然后是:View的确和Frame有着千丝万缕的联系。MFC是个半定制的框架,微软已做了很多手脚,说不定你在View里啪啪点几下,就有几个类似WM_MICROSPACE这样的消息传到了Frame里。然而控件是没有Frame可言的,而且控件也从不需要知道自己被放到了哪个容器里!!

所以,为了不至于编译器当啊当的乱叫,我们还要小心伺候着!:)

在继续往下做之前,你还要明确CHtmlView和我们最终生成的CHtmlCtrl到底有什么区别?

其实,区别仅仅是它们被使用的方法不同。控件通常是对话框里的子窗口---当然你可以把它作为任何窗口的子窗口。然而View却是专门为了实现MFC 文档视图结构而设计的。一个View有一个指向Document的指针并且被固定在一个特别的窗口里---人称:框架窗口(CFrameWnd)。对于Document来说,CView是它可以从形态上被表现的场作。但,指向Document的指针m_pDocument可能是NULL,所以每当我们在View里处理Document的时候,这么做是明智的:

1. If(m_pDocument!=NULL)
2. {
3.     // Do something here!
4. }

所以,View并不正真的需要一个Document,CHtmlView也不需要。你可能认为在CHtmlView里的Document就是一个HTML文件,但实际上,CHtmlView是使用IWebBrowser2实现的,而且它对MFC的文档视图结构一无所知。

那么需要一个Frame吗? 如果你仔细研究过相关的代码,就会发现:只是在极少地方,View知道自己属于一个Frame。大多数文档视图结构是在更高一级的类比如Frame本身和CDocTemplate里实现的,它们把Frame,View,Document紧紧的粘在了一起。View并不知道外面发生了什么,这样设计的文档视图系统有很多优点。理论上来说,View是被动地受Frame控制,而且没有其他途径,所以认为View知道它自己的父窗口是错误的。有两处CView( 也是CHtmlView的父类 )会假想自己在一个Frame里。第一处是CView::OnMouseActive,它是WM_MOUSEACTIVE消息的处理函数,当你在View里点击鼠标以后,它会做很多细致的工作。这些细致的工作是不重要的,但重要的是View调用了GetParentFrame以得到它的父窗口框架,然后CFrameWnd::GetActiveView激活当前的活动视图---所有的这一切,都建立在假设View是CFrameWnd的一个子窗口的基础之上。

另外一处是在CView::OnDestroy里:

1. voidCView::OnDestroy()
2. {
3.     CframeWnd* pFrame = GetParentFrame();
4.     If(pFrame!=NULL&&pFrame->GetActiveView==this)
5.     // deactive during death
6.     pFrame->SetActiveView(NULL);
7.     CWnd::OnDestroy();
8. }

在这里,View先让自己处于非活动状态。从另一个角度考虑,我们完全可以通过向父窗口发通知消息(Notification)代替C++方法调用从而回避掉View对Frame的依赖!! GetParentFrame可以返回一个CWnd,而非CFrameWnd,因为该函数只是强调了父窗口,而不是一定要返回一个特定的类。View可以通过发送一个类似WM_MICROSPACE的通知消息取代对CFrameWnd的方法调用,这些Notification会被"Frame"(或是CFrameWnd或是CfooWnd)正确的响应。 但是,无论如何,你必须清楚:当View被激活或进入非活动状态时,是Frame决定该如何响应,而不是View本身。任何系统都是如此;函数向下调用(从父到子),事件向上传递(从子到父)。一个子类从来都不会知道自己所在的容器!! 生活本身就不是完美的! 但幸运的是我们将会很容易的克服MFC给我们带来的难题! CHtmlCtrl类( here! )正是我们需要的:一个HTML "View" ,你可以在对话框或任何窗口里使用。CHtmlCtrl重载了OnMouseActive和OnDestroy从而回避CView那些给我们惹麻烦的代码。

01. intCHtmlCtrl::OnMouseActive(…)
02. {
03.     // bypass CView doc/frame stuff
04.     returnCWnd::OnMouseActive(…);
05. }
06. voidCHtmlCtrl::OnDestroy()
07. {
08.     // bypass CView doc/frame stuff
09.     CWnd::OnDestroy();
10. }

除此之外,CHtmlCtrl也重载了PostNcDestroy

1. voidCHtmlCtrl::PostNcDestroy()
2. {
3.     // do nothing
4. }

CView正常的PostNcDestroy实现是使用delete this销毁View。对于Views来说,这是正常的处理方式,因为View是直接在堆里建立的。但是,控件一般是作为另一个窗口对象的成员存在的。这时你就不要delete了,它会被父对象delete掉。 通过以上的修改(OnMouseActive,OnDestroy和PostNcDestroy),CHtmlCtrl能顺利的在对话框里工作了!! 为此,偶写了个小程序:AboutHtml( 见源代码)显示一个About框(如图1)。那是完全用HTML写的。 HTML的资源:图片,音频文件(对了!甚至可以有声音)都被存储到EXE文件里。

1. // in AboutHtml.rc
2. ABOUT.HTML  HTML DISCARDABLE"res\\about.htm"
3. VCKBCOM.GIF HTML DISCARDABLE"res\\vckcom.gif"
4. OKUP.GIF    HTML DISCARDABLE"res\\okup.gif"
5. OKDN.GIF    HTML DISCARDABLE"res\\okdn.gif"
6. MOZART.WAV  HTML DISCARDABLE"res\\mozart.wav"

在一般的Web页面里,如果你这么做:< img src="vckcom.gif" / > 就需要把vckcom.gif放到当前目录下。对于访问一个存储在EXE文件里的资源,同样也要这样。这种情况下,你必须使用下面的代码帮助浏览器寻找你的HTML元素:

1. < BASE url="res://AboutHtml.exe/about.htm">

浏览器就会知道当前的"目录"是res://AboutHtml.exe,所以当它遇到< img src="vckcom.gif" / >,它会自动找到res://AboutHtml.exe/vckcom.gif,否则,它将在HTML文件所在的当前目录下寻找。

MFC界面--利用CHtmlView和HTML制作新风格的界面(包括CView和Dialog)_第1张图片 

图一

一般来说,你可以使用res://modulename访问任何存储在EXE和DLL里的资源。res:是一个类似http:,ftp:,file:,或mailto:的协议。它告诉浏览器资源的路径和名字,细节工作浏览器知道如何去做!:) 为实现About对话框,偶写了个类,CAboutDialog,它有个CHtmlCtrl类型的成员m_page。我们来看看CaboutDialog的初始化过程:

1. BOOLCaboutDialog::OnInitDialog()
2. {
3.     VERIFY(Cdialog::OnInitDialog());
4.     VERIFY(m_page.CreateFromStatic(IDC_HTMLVIEW,this));
5.     m_page.LoadFromResource(_T("about.htm"));
6.     returnTRUE;
7. }

你可能对CHtmlCtrl::CreateFromStatic有点迷惑。 还记得我们刚开始谈到的CStatic吗? 我们打算用它来代表CHtmlCtrl控件,它将从CStatic建立一个CHtmlCtrl控件对象,这是一个子类化的过程,该对象将和CStatic有同样的ID,大小和位置。这么做很方便,很有效!:)

01. BOOLCHtmlCtrl::CreateFromStatic(UINTnID, CWnd* pParent)
02. {
03.     CStatic wndStatic;
04.     if(!wndStatic.SubclassDlgItem(nID, pParent))
05.         returnFALSE;
06.     // Get static control rect
07.     CRect rc;
08.     wndStatic.GetWindowRect(&rc);
09.     pParent->ScreenToClient(&rc);
10.     wndStatic.DestroyWindow();
11.     // create HTML control (CHtmlView)
12.     returnCreate(NULL,                  // class name
13.         NULL,                       // title
14.         (WS_CHILD | WS_VISIBLE ),           // style
15.         rc,                    // rectangle
16.         pParent,       // parent
17.         nID,           // control ID
18.         NULL);         // frame/doc context not used
19. }

然后是使用CHtmlCtrl::LoadFromResource打开页面,它从CHtmlView继承而来。当然偶也可以这样打开页面:res://AboutHtml.exe/about.html OK! 偶已经向你展示了CHtmlCtrl如何通过回避CView而顺利代替frame在dialog里显示!。偶也介绍了如何如何在资源文件里定位HTML文件和图象文件。并且告诉你如何打开一个Web页面。 但还有一个极为精彩的处理没有告诉你!:) ,能猜到是什么吗? 哇哈哈哈!!! 看到About对话框里的OK按钮了吧? 它并不是一个按钮!!仅仅是HTML文件里的一副图片! 偶使用了Javascript使得它在被单击时有up和down两种状态,但是它是如何和我们的对话框程序通讯的呢??? 你说好玩不?

如果你搞过DHTML,你可能会想到DHTML文档层可使用COM发现IMG元素然后监听它的OnClick事件。但是那样做对于偶这样的COM半文盲是way,way,way,WAY痛苦和麻烦的工作!!:( 其实,有一个更为简单的方法。假设你让这个"button"链接到一个叫做ok的文档: 现在,当用户单击它,浏览器将转到ok文件。但在它这么做以前,控制权交给 CHtmlCtrl::OnBeforeNavigate2。这时CHtmlCtrl可以做任何合法的事情:

1. < A href="ok">< IMG … >< /A >
01. voidCmyHtmlCtrl::OnBeforeNavigate2(
02.         LPCTSTRlpszURL,…,BOOL *pbCancel)
03. {
04.     if(_tcscmp(lpszURL,_T("ok"))==0)
05.     {
06.         //"ok" clicked
07.         *pbCancle=TRUE;// abort
08.         // will close dialog
09.         GetParent()->SendMessage(WM_COMMAND,IDOK);
10.     }
11. }

[这是多么振奋人心的消息?? 想一想,我们几乎可以让对话框做几乎所有能做的事情! 而且我们可以将Web页面处理的更为美观!!:]] 所以,ok并不正真的是另一个文件,而CHtmlCtrl正是利用它来解释OK按钮!! 太完美了! 为了让这个想法更紧凑,偶引入了一个伪协议! 叫做:app:。用它来代替使用ok,在about.htm里正真的链接是app:ok。当CHtmlCtrl发现浏览器试图导航到app:somewhere时,它调用一个新的虚函数,CHtmlCtrl::OnAppCmd,它用somewhere作为参数,并cancels调航(navigation),所以CmyHtmlCtrl并没有重载OnNavigate2,而是重载了OnAppCmd:

1. voidCmyHtmlCtrl::OnAppCmd(LPCTSTRlpszWhere)
2. {
3.     if(_tcsicmp(lpszWhere,_T("ok"))==0)
4.     {
5.         GetParent()->SendMessage(WM_COMMAND,IDOK);
6.     }
7. }

你可以在HTML文件里使用其他链接,比如app:cancel,app:refresh,或者app:whatever!:) 并同时使用OnAppCmd函数寻找相应的字串,"cancel","refresh",以及"whatever"。 好了!! 可以做你自己想做的事去了!…在你疯狂的code之前,提醒几句:加载IE DLLs需要极少的等待,但是如果加载About对话框超过10秒并且搬出来个沙漏晃来晃去,用户将感到非常不舒服!:)。 最后,当你在About对话框里单击鼠标右键,会弹出个标准的浏览器快捷菜单,你可能觉得这是多余的,或者出于保护你的源代码的目的,你会买力的屏蔽调右键的功能。其实这很简单,你仅仅需要在HTML的 标签里加入一句脚本代码……但我们毕竟是在玩VC。所以,尽管麻烦,我们还是很乐意尝试。 这个也不难!! 我们同样使用子类化的原理就能实现。但不是现在,而是将来的某个时候!! :)


 

(2)交互深入

转自:http://www.vckbase.com/index.php/wv/686   可以下载源码

在这之前补充我发现的一个问题:

void CHtmlCtrl::OnDestroy()
{
 // This is probably unecessary since ~CHtmlView does it, but
 // safer to mimic CHtmlView::OnDestroy.
 if (m_pBrowserApp) {
  m_pBrowserApp->Release();
  m_pBrowserApp = NULL;
 }
 CWnd::OnDestroy(); // bypass CView doc/frame stuff
}

【上面代码是CHtmlCtrl.cpp中的,但不能通过vs2008编译,原因是下面讲的是vc6环境下的。可能是vs2008使用的是atl3.0以上的版本的缘故。解决方法是把函数内部的东西都注释掉,只留下最后一句CWnd::OnDestroy();即可。不可只注释掉出错的m_pBrowserApp->Release();这一句,否则表面没错,可能影响整个程序,导致崩溃。】

译者注:

在以前的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),其定义如下:

01. staticHWNDGetLastChild(HWNDhwndParent)
02. {
03.    HWNDhwnd = hwndParent;
04.    while(TRUE) {
05.       HWNDhwndChild = ::GetWindow(hwnd, GW_CHILD);
06.       if(hwndChild==NULL)
07.          returnhwnd;
08.       hwnd = hwndChild;
09.    }
10.    returnNULL;
11. }

通过这个函数返回某个父窗口下的最后一个子窗口,也就是说返回子窗口的子窗口的子窗口......直到不再有子窗口为止。可惜这个函数要获得正确的运行结果是有前提的,那就是窗口层次只能是一层,并且最终的窗口后裔是“Internet Explorer_Server”窗口。 在通常情况下,这个假设都成立。不幸的是,如果 HTML 文档中包含象 ComBoxes(组合框) 这样的控制时,这个假设就不灵了。用 Spy++ 不难发现情况并不象你期望的那样─Internet Explorer_Server是最后的子窗口。实际上,在IE中,Edit 和 Button 控制并非人们所想象的那样是子窗口。

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

为了解决这个问题,本文设计了一个更加完善的类:CFindWnd,用更好的算法专门来获取 IE 窗口。CFindWnd 查找某个窗口(给定窗口名字)的第一个子窗口。 例如,它的使用方法如下:

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

这个类的构造函数调用函数:

1. FindChildClassHwnd(hwndParent, (LPARAM)this)

函数,该函数又调用:

1. EnumChildWindows 和 FindWindowEx

搜索所有后裔窗口直到找到类名匹配窗口为止。FindWindow 用来查找最顶层窗口,而搜索子窗口还得用 FindWindowEx,它是 Win32 API 函数。CFindWnd 返回第一个匹配的窗口,所以它只被用于查找你期望只有一个实例的窗口。通常在搜索特定窗口时,一般最保险的做法都是检查窗口类名。

百家争鸣

有一个读者来信指出:根本没有必要使用子类IE窗口的方法来禁用上下文菜单。完全可以在 CHtmlCtrl 内部实现,象下面这样:

1. BOOLCHtmlCtrl::PreTranslateMessage(MSG* pMsg)
2. {
3.   if(pMsg->message == WM_CONTEXTMENU)
4.   returnTRUE;// eat it
5.   returnCHtmlView::PreTranslateMessage(pMsg);
6. }      

这样做是可行的,因为MFC实现了非常有独创性的、强大的特性─在 CWinThread 的主消息泵中,MFC 调用 CWnd::WalkPreTranslateTree 函数。这个函数循环消息目的地窗口的所有父窗口,调用每一个父窗口的 PreTranslateMessage ,一旦截获消息发送到后裔窗口则停止循环。非常聪明!

经验证明:要使前面的代码段按照期望的结果运行,你还必须截获 WM_RBUTTONDOWN 和 WM_RBUTTONDBLCLK 消息,同时还要做必要的检查以保证目标窗口的类名是 “Internet Explorer_Server”,这样就不会意外地捕获其它子窗口的上下文菜单(除非你确实要这么做)。下面是 CHtmlCtrl::PreTranslateMessage 的最终代码:

001. 头文件 HtmlCtrl.h
002.   
003.
004. #pragma once
005.   
006.
007. // 该结构在命令映射中定义一个入口,这个映射将文本串映射到命令IDs,
008. // 如果命令映射中有一个映射到 ID_APP_ABOUT 的入口 “about”,并且 
009. // HTML 有一个链接锚 ,那么单击该链接时将执行 
010. // ID_APP_ABOUT 命令。为了设置这个映射,调用 CHtmlCtrl::SetCmdMap. 
011. // 
012. //
013. structHTMLCMDMAP {
014.    LPCTSTRname;    // command name used in "app:name" HREF in 
016. };
017.   
018. //
019. // 这个类将 CHtmlView 转换为普通的能在对话框和框架中使用的控制
020. //
021. classCHtmlCtrl :publicCHtmlView {
022. protected:
023.    HTMLCMDMAP* m_cmdmap;  // command map
024.    BOOLm_bHideMenu;      // hide context menu
025.   
026. public:
027.    CHtmlCtrl() : m_bHideMenu(FALSE), m_cmdmap(NULL) { }
028.    ~CHtmlCtrl() { }
029.   
030.    // get/set HideContextMenu property
031.    BOOLGetHideContextMenu()         {returnm_bHideMenu; }
032.    voidSetHideContextMenu(BOOLval) { m_bHideMenu=val; }
033.   
034.    // Set doc contents from string
035.    HRESULTSetHTML(LPCTSTRstrHTML);
036.   
037.    // set command map
038.    voidSetCmdMap(HTMLCMDMAP* val)   { m_cmdmap = val; }
039.   
040.    // create control in same place as static control
041.    BOOLCreateFromStatic(UINTnID, CWnd* pParent);
042.   
043.    // create control from scratch
044.    BOOLCreate(constRECT& rc, CWnd* pParent, UINTnID,
045.       DWORDdwStyle = WS_CHILD|WS_VISIBLE, 
046.           CCreateContext* pContext = NULL)
047.    {
048.       returnCHtmlView::Create(NULL, NULL, dwStyle, rc, pParent,
049.           nID, pContext);
050.    }
051.   
052.    // 重写该函数可以截获子窗口消息,从而禁用上下文菜单。
053.    virtualBOOLPreTranslateMessage(MSG* pMsg);
054.   
055.    // 通常,CHtmlView 自己是在 PostNcDestroy 销毁的,但对于一个界面控制来说
056.    // 我们不想那样做,因为控制一般都是作为另一个窗口对象的成员实现的。
057.    //
058.    virtualvoidPostNcDestroy() {  }
059.   
060.    // 重写以便旁路掉对 MFC doc/view 框架的依赖,CHtmView 仅仅在这里依附于框架。
061.    afx_msgvoidOnDestroy();
062.    afx_msgint OnMouseActivate(CWnd* pDesktopWnd,UINTnHitTest, 
063.       UINTmsg);
064.   
065.    // 重写以便截获 "app:" 伪协议
066.    virtualvoidOnBeforeNavigate2(LPCTSTRlpszURL,
067.       DWORDnFlags,
068.       LPCTSTRlpszTargetFrameName,
069.       CByteArray& baPostedData,
070.       LPCTSTRlpszHeaders,
071.       BOOL* pbCancel );
072.   
073.    // 你可以重写处理 "app:" 命令的代码。注意只是在不使用命令映射机制时才需要重写
074.    virtualvoidOnAppCmd(LPCTSTRlpszCmd);
075.   
076.    DECLARE_MESSAGE_MAP();
077.    DECLARE_DYNAMIC(CHtmlCtrl)
078. };
079.   
080. 实现文件 HtmlCtrl.cpp
081.
082. // 可用于对话框和其它窗口,不需要框架
083. //
084. // 特性:
085. // - SetCmdMap 可以设置 "app:command" 链接的命令映射.
086. // - SetHTML 可以将串内容设置成 HTML.
087.   
088. #include "StdAfx.h"
089. #include "HtmlCtrl.h"
090.   
091. // 声明 typedef ATL 智能指针;如:SPIHTMLDocument2
092. #define DECLARE_SMARTPTR(ifacename) typedef CComQIPtr SP##ifacename;
093.   
094. // IHTMLDocument2 的智能指针 
095. DECLARE_SMARTPTR(IHTMLDocument2)
096.   
097. // 一个很有用的宏,用于检查 HRESULT
098. #define HRCHECK(x) hr = x; if (!SUCCEEDED(hr)) { \
099.    TRACE(_T("hr=%p\n"),hr);\
100.    returnhr;\
101. }
102.   
103. ...// same as earlier version
104.   
105. // Return TRUE if hwnd is Internet Explorer window.
106. inlineBOOLIsIEWindow(HWNDhwnd)
107. {
108.    staticLPCSTRIEWNDCLASSNAME = "Internet Explorer_Server";
109.    charclassname[32];// always char, never TCHAR
110.    GetClassName(hwnd, classname,sizeof(classname));
111.    returnstrcmp(classname, IEWNDCLASSNAME)==0;
112. }
113.   
114. //
115. // 重写后捕获 "Internet Explorer_Server" 窗口上下文菜单消息.
116. //
117. BOOLCHtmlCtrl::PreTranslateMessage(MSG* pMsg)
118. {
119.    if(m_bHideMenu) {
120.       switch(pMsg->message) {
121.       caseWM_CONTEXTMENU:
122.       caseWM_RBUTTONUP:
123.       caseWM_RBUTTONDOWN:
124.       caseWM_RBUTTONDBLCLK:
125.          if(IsIEWindow(pMsg->hwnd)) {
126.             if(pMsg->message==WM_RBUTTONUP)
127.                // let parent handle context menu
128.                GetParent()->SendMessage(WM_CONTEXTMENU, pMsg->wParam, 
129.                   pMsg->lParam);
130.             returnTRUE;// eat it
131.          }
132.       }
133.    }
134.    returnCHtmlView::PreTranslateMessage(pMsg);
135. }
136.   
137. //
138. // 重写后将 "app:" 链接传递到虚函数,而不是浏览器.
139. //
140. voidCHtmlCtrl::OnBeforeNavigate2(LPCTSTRlpszURL,
141.    DWORDnFlags,LPCTSTRlpszTargetFrameName, CByteArray& baPostedData,
142.    LPCTSTRlpszHeaders,BOOL* pbCancel )
143. {
144.    constcharAPP_PROTOCOL[] = "app:";
145.    intlen = _tcslen(APP_PROTOCOL);
146.    if(_tcsnicmp(lpszURL, APP_PROTOCOL, len)==0) {
147.       OnAppCmd(lpszURL + len);         // call virtual handler fn
148.       *pbCancel = TRUE;                // cancel navigation
149.    }
150. }
151.   
152. //
153. // 当浏览器试图导航到 "app:foo" 时调用该函数. 
154. // 默认的处理例程查找"foo"命令的命令映射,并向找到的父窗口发送
155. // WM_COMMAND 消息。调用 SetCmdMap 设置命令映射。如果要实现更
156. // 复杂的处理,只要重写这个函数即可.
157. //
158. voidCHtmlCtrl::OnAppCmd(LPCTSTRlpszCmd)
159. {
160.    if(m_cmdmap) {
161.       for(inti=0; m_cmdmap[i].name; i++) {
162.          if(_tcsicmp(lpszCmd, m_cmdmap[i].name) == 0)
163.             // Use PostMessage to avoid problems with exit command. (Let
164.             // browser finish navigation before issuing command.)
165.             GetParent()->PostMessage(WM_COMMAND, m_cmdmap[i].nID);
166.       }
167.    }
168. }
169.   
170. //
171. // 将串转换为 HTML 文档
172. //
173. HRESULTCHtmlCtrl::SetHTML(LPCTSTRstrHTML)
174. {
175.    HRESULThr;
176.   
177.    // Get document object
178.    SPIHTMLDocument2 doc = GetHtmlDocument();
179.   
180.    // Create string as one-element BSTR safe array for 
181.    // IHTMLDocument2::write.
182.    CComSafeArray sar;
183.    sar.Create(1,0);
184.    sar[0] = CComBSTR(strHTML);
185.   
186.    // open doc and write
187.    LPDISPATCH lpdRet;
188.    HRCHECK(doc->open(CComBSTR("text/html"),
189.       CComVariant(CComBSTR("_self")),
190.       CComVariant(CComBSTR("")),
191.       CComVariant((bool)1),
192.       &lpdRet));
193.      
194.    HRCHECK(doc->write(sar)); // write contents to doc
195.    HRCHECK(doc->close());    // close
196.    lpdRet->Release();        // release IDispatch returned
197.   
198.    returnS_OK;
199. }

和以前相比,这个类功能更强,具备了 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页面是从资源中加载的:

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

CHtmlView::LoadFromResource 打开 res://AboutHtml.exe/about.htm,这里“AboutHtml.exe” 是可执行程序的实际名字,“res://”是一个伪协议。为了显示顶层窗口的信息,最好的办法是动态产生 HTML 页面,而不是从资源中加载,为此我在 CHtmlCtrl类中添加了一个新函数:CHtmlCtrl::SetHTML,

01.
02. // 通过串设置 HTML 文档内容
03. //
04. HRESULTCHtmlCtrl::SetHTML(LPCTSTRstrHTML)
05. {
06.    HRESULThr;
07.   
08.    // 获得文档对象
09.    SPIHTMLDocument2 doc = GetHtmlDocument();
10.   
11.    // 创建只有一个元素(串)的 BSTR 数组元素 
12.    // IHTMLDocument2::write.
13.    CComSafeArray sar;
14.    sar.Create(1,0);
15.    sar[0] = CComBSTR(strHTML);
16.   
17.    // 打开文档并写入
18.    LPDISPATCH lpdRet;
19.    HRCHECK(doc->open(CComBSTR("text/html"),
20.       CComVariant(CComBSTR("_self")),
21.       CComVariant(CComBSTR("")),
22.       CComVariant((bool)1),
23.       &lpdRet));
24.      
25.    HRCHECK(doc->write(sar)); // write contents to doc
26.    HRCHECK(doc->close());    // close
27.    lpdRet->Release();        // release IDispatch returned
28.   
29.    returnS_OK;
30. }

下面我们一步一步来分析实现过程,首先必须获取 IHTMLDocument2 接口:

1. SPIHTMLDocument2 doc = GetHtmlDocument();

SPIHTMLDocument2 与 CComQIPtr 一样是一个指向 IHTMLDocument2 的ATL智能指针,(当今 Windows 编程已进入 COM 时代,作为一名编写 Windows 应用程序的开发人员,如果你使用 COM 技术,但没有用过智能指针,那么这段代码会对你有所裨益),接着,必须创建一个SAFEARRAY,以便存放作为 BSTR 数组唯一元素的 HTML 串,SAFEARRAY是一个 COM 数据结构,其作用是在不同平台之间安全地传递数组数据,ATL提供了 CComBSTR 和 CComSafeArray 两个类,为开发人员在处理 BSTRs 和安全数组时减轻了许多痛苦:

1. // strHTML is LPCTSTR
2. CComSafeArray sar;
3. sar.Create(1,0);
4. sar[0] = CComBSTR(strHTML);

如果不借助于 CComSafeArray 和 CComBSTR,而是用下列这些 API 函数来实现相同的处理,如 SafeArrayCreateVector,SafeArrayAccessData, 和 SafeArrayUnaccessData,那么至少还得写10-20行无聊的代码。一旦你上手了智能指针,你会觉得ATL的这些东西用起来真的很爽。

现在有了文档对象以及在安全数组中的内容,接下来便可以打开文档,进行写入操作,关闭文档等等。IHTMLDocument2::write需要 VARIANTS 和 BSTRs 类型的数据,这里ATL又一次显示了它的优势:

01. LPDISPATCH lpdRet;
02. doc->open(CComBSTR("text/html"), // MIME type
03.   CComVariant(CComBSTR("_self")),// open in same window
04.   CComVariant(CComBSTR("")),     // no features
05.   CComVariant((bool)1),          // replace history entry
06.   &lpdRet));                     // IDispatch returned
07. doc->write(sar);// write it
08. doc->close();   // close
09. lpdRet->Release();

CHtmlCtrl::SetHTML 非常好用。使用它时有一个技巧:当第一次创建 CHtmlCtrl 时,它没有文档(GetHtmlDocument返回NULL)。所以在调用 CHtmlCtrl::SetHTML 之前,你必须创建一个文档,最简单的方法就是打开一个空文档,就象下面这样:

1. m_wndView.Navigate(_T("about:blank"));

此外,如果HTML很简单,你可以用 about: 代替 CHtmlCtrl::SetHTML 来得到HTML,如下面的代码:

1. m_wndView.Navigate(_T("about:hello, world"));

针对简单的HTML可以这么做,如果比较复杂的文档则要调用 SetHTML。本文附带的例子程序动态构造了一个包含图像、表格、链接等元素的HTML文档, 该文档列出所有顶层窗口的信息,然后将它们显示出来,如图一所示。

例子程序的参考代码如下:

001. //
002. // HtmlApp.cpp
003.   
004. classCMyApp :publicCWinApp {
005. public:
006.    virtualBOOLInitInstance();
007. protected:
008.    afx_msgvoidOnAppAbout();
009.    DECLARE_MESSAGE_MAP()
010. } theApp;
011.   
012. BEGIN_MESSAGE_MAP(CMyApp, CWinApp)
013.    ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
014. END_MESSAGE_MAP()
015.   
016.   
017. BOOLCMyApp::InitInstance()
018. {
019.    // Create main frame window (don''t use doc/view stuff)
020.    CMainFrame* pMainFrame =newCMainFrame;
021.    if(!pMainFrame->LoadFrame(IDR_MAINFRAME))
022.       returnFALSE;
023.    pMainFrame->ShowWindow(m_nCmdShow);
024.    pMainFrame->UpdateWindow();
025.    m_pMainWnd = pMainFrame;
026.   
027.    returnTRUE;
028. }
029.   
030.   
031. //
032. // “关于”对话框使用 HTML 控制显示内容.
033. //
034. classCAboutDialog :publicCDialog {
035. protected:
036.    CHtmlCtrl m_page;      // HTML control
037.    virtualBOOLOnInitDialog(); 
038. public:
039.    CAboutDialog() : CDialog(IDD_ABOUTBOX, NULL) { }
040.    DECLARE_DYNAMIC(CAboutDialog)
041. };
042. IMPLEMENT_DYNAMIC(CAboutDialog, CDialog)
043.   
044.   
045. ///
046. // 初始化“关于”对话框
047. //
048. BOOLCAboutDialog::OnInitDialog()
049. {
050.    // cmd map for CHtmlCtrl handles "app:ok"
051.    staticHTMLCMDMAP AboutCmds[] = {
052.       { _T("ok"), IDOK },
053.       { NULL, 0  },
054.    };
055.    VERIFY(CDialog::OnInitDialog());
056.    VERIFY(m_page.CreateFromStatic(IDC_HTMLVIEW,this)); 
057. // create HTML 
058.                                                           
059. // ctrl
060.    m_page.SetHideContextMenu(TRUE);                    
061.  // hide context 
062.                                                           
063. // menu
064.    m_page.SetCmdMap(AboutCmds);                         
065. // set command 
066.                                                           
067. // table
068.    m_page.LoadFromResource(_T("about.htm"));           
069.  // load HTML from 
070.                                                           
071. // resource
072.    returnTRUE;
073. }
074.   
075. /
076. // 运行“关于”对话框
077.   
078. voidCMyApp::OnAppAbout()
079. {
080.    staticCAboutDialog dlg;// static to remember state of hyperlinks
081.    dlg.DoModal();          // run it
082. }
083.   
084.
085. // MainFrm.h
086. // 典型的主框架处理例程......
087.   
088. classCMainFrame :publicCFrameWnd {
089. public:
090.    CMainFrame(){ }
091.    virtual~CMainFrame() { }
092. protected:
093.    CHtmlCtrl   m_wndView;              // CHtmlCtrl 作为主窗口视图
094.    CStatusBar  m_wndStatusBar;         // status line
095.    CToolBar    m_wndToolBar;           // toolbar
096.   
097.    afx_msgint OnCreate(LPCREATESTRUCT lpCreateStruct);
098.    afx_msgvoidOnContextMenu(CWnd* pWnd, CPoint pos);
099.   
100.    // helper to format main window HTML
101.    CString FormatWindowListHTML();
102.   
103.    DECLARE_DYNCREATE(CMainFrame)
104.    DECLARE_MESSAGE_MAP()
105. };
106.   
107.
108. // MainFrm.cpp
109.   
110. IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
111. BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
112.    ON_WM_CREATE()
113.    ON_WM_CONTEXTMENU()
114. END_MESSAGE_MAP()
115.   
116. // Commmand map for app: commands in main window HTML.
117. HTMLCMDMAP MyHtmlCmds[] = {
118.    { _T("about"), ID_APP_ABOUT },
119.    { _T("exit"),  ID_APP_EXIT  },
120.    { NULL, 0  },
121. };
122.   
123. intCMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
124. {
125.    VERIFY(CFrameWnd::OnCreate(lpCreateStruct)==0);
126.   
127.    ...
128.   
129.    // create/init html control as view
130.    VERIFY(m_wndView.Create(CRect(),this, AFX_IDW_PANE_FIRST));
131.    m_wndView.SetHideContextMenu(TRUE);         // 隐藏上下文菜单
132.    m_wndView.SetCmdMap(MyHtmlCmds);            // 设置命令
133.    m_wndView.Navigate(_T("about:blank"));      // 创建文档
134.    m_wndView.SetHTML(FormatWindowListHTML());  // 设置获取的HTML内容串
135.    SetActiveView(&m_wndView);                  // 设置MFC活动视图
136.   
137.    return0;
138. }
139.   
140. ///
141. // 处理上下文菜单的命令函数,当前该函数只是显示TRACE信息,以示被调用过。
142. //
143.   
144. voidCMainFrame::OnContextMenu(CWnd* pWnd, CPoint pos)
145. {
146.    TRACE(_T("CMainFrame::OnContextMenu\n"));
147. }
148.   
149. //
150. // 这个函数创建在主窗口视图中显示的 HTML。
151. // EnumWindows 回调该函数:如果窗口可见,则将窗口信息添加到 HTML table中。
152. //
153. staticBOOLCALLBACK MyEnumWindowsProc(HWNDhwnd,LPARAMlp)
154. {
155.    DWORDstyle = GetWindowLong(hwnd, GWL_STYLE);
156.    if(style & WS_VISIBLE) {
157.       CString& s = *(CString*)lp;
158.       charcname[256];
159.       GetClassName(hwnd, cname,sizeof(cname));
160.       TCHARtext[1024];
161.       GetWindowText(hwnd, text,sizeof(text)/sizeof(text[0]));
162.       CString temp;
163.       temp.Format(_T("%p %s%s\n"),
164.          hwnd, cname, text);
165.       s += temp;
166.    }
167.    returnTRUE;
168. }
169.   
170. /
171. // 该函数创建一个文本串,这个串就是要显示在主窗口的 HTML 文档。
172.   
173. CString CMainFrame::FormatWindowListHTML()
174. {
175.     // start w/top matter
176.     CString html = _T("\
177.     link=\"#02B7B7\" vlink=\"#02B7B7\">\n\
178.     "100%\">\n\
179.     
\
181.     
\n\
184.     
\n");
186.       
187.     // enumerate top-level windows to append their info
188.     EnumWindows(MyEnumWindowsProc, (LPARAM)&html);
189.   
190.     // append bottom matter. note commands app:about and app:exit
191.     html += _T("
"new\" HREF=\"http://www.vckbase.com\"> AboutHtml3 例子程序 -- 顶层可见窗口清单
\n\
182.     "new\"\
窗口句柄(hwnd)窗口类名窗口标题\
185.     
\n\
193.     ["app:exit\">退出]\n\
194.     \n\
195.     ");
196.   
197.     returnhtml;
198. }

最后,我想说明一下本文例子程序中其它的一些编程技巧和诀窍,主要是针对CHtmlCtrl类的功能扩展。早在“VC6中使用CHtmlView在对话框控制中显示HTML文件”(第六期)一文中,我曾经演示了如何实现“app:”伪协议来创建HTML链接(也就是锚点)与应用程序通信。例如:你可以象下面这样添加一个链接:

然后,CHtmlCtrl::OnBeforeNavigate2 会识别出“app:”伪协议并以“about”作为参数调用专门的虚函数 CHtmlCtrl::OnAppCmd 。你可以创建自己的命令并在派生类中改写 OnAppCmd 来处理自己建立的命令。使用了 CHtmlCtrl 一段时间后。我发现经常需要派生 CHtmlCtrl 类,每次都得改写这个函数,自己感觉很麻烦!为了简化这个过程,我发明了一个简单的命令映射机制,利用这种机制可以轻松将“app:command”之类的转换为通常熟知的 WM_COMMAND 命令 ID:

1. HTMLCMDMAP MyHtmlCmds[] = {
2.   { _T("about"), ID_APP_ABOUT },
3.   { _T("exit"),  ID_APP_EXIT  },
4.   { NULL, 0  },
5. };

这个映射机制的使用方法是象下面这样调用 CHtmlCtrl::SetCmdMap 函数:

1. m_wndHtmlCtrl.SetCmdMap(MyHtmlCmds);

这样一来,当用户单击“app:about”链接时,CHtmlCtrl::OnAppCmd 便会搜索命令映射,找到“about”入口,然后将与ID_APP_ABOUT 对应的 WM_COMMAND 消息发送到其父窗口,这个技巧主要是仰仗MFC神奇的命令路由通道实现的,借助此通道,任何窗口都可以处理此命令。真是爽啊!本文例子程序正是用这种特性将“关于”和“退出”命令作为HTML链接直接添加到主窗口中。CHtmlCtrl类实现的细节代码如下:

001.
002. // HtmlCtrl.h
003. #pragma once
004.   
005. //
006. // 此结构定义一个命令映射入口,映射将文本串映射到命令IDs。如果你的命令映射
007. // 入口包含 "about" 映射到ID_APP_ABOUT,并且HTML文档中有一个锚点链接是
009. // 映射的方法是调用 CHtmlCtrl::SetCmdMap 函数.
010. //
011. structHTMLCMDMAP {
013.    UINTnID;
014. };
015.   
016. ///
017. // 将 CHtmlView 转换为框架或对话框中常规控制的类.类似于CListView/CListCtrl
018. // 和 CTreeView/CTreeCtrl
019. // 
020. classCHtmlCtrl :publicCHtmlView {
021. protected:
022.    HTMLCMDMAP* m_cmdmap;  // 命令映射
023.    BOOLm_bHideMenu;      // 隐藏上下文菜单
024.   
025. public:
026.    CHtmlCtrl() : m_bHideMenu(FALSE), m_cmdmap(NULL) { }
027.    ~CHtmlCtrl() { }
028.   
029.    // 获取/设置 HideContextMenu 属性
030.    BOOLGetHideContextMenu()         {returnm_bHideMenu; }
031.    voidSetHideContextMenu(BOOLval) { m_bHideMenu=val; }
032.   
033.    // 根据串创建 HTML 文档
034.    HRESULTSetHTML(LPCTSTRstrHTML);
035.   
036.    // 设置命令映射
037.    voidSetCmdMap(HTMLCMDMAP* val)   { m_cmdmap = val; }
038.   
039.    // 先创建一个静态控制,然后用相同的再创建一个控制
040.    BOOLCreateFromStatic(UINTnID, CWnd* pParent);
041.   
042.    // 创建控制
043.    BOOLCreate(constRECT& rc, CWnd* pParent, UINTnID,
044.       DWORDdwStyle = WS_CHILD|WS_VISIBLE, 
045.           CCreateContext* pContext = NULL)
046.    {
047.       returnCHtmlView::Create(NULL, NULL, dwStyle, rc, pParent,
048.           nID, pContext);
049.    }
050.   
051.    // 重写用以解释子窗口消息来禁用上下文菜单
052.    virtualBOOLPreTranslateMessage(MSG* pMsg);
053.   
054.    // 通常,CHtmlView 是在 PostNcDestroy 中将自己摧毁, 
055.    // 但用于窗口控制,我们不想那么做,因为控制通常是作为
056.    // 另一个窗口对象的成员来实现的.
057.    //
058.    virtualvoidPostNcDestroy() {  }
059.   
060.    // 重写该函数以便旁路掉对 MFC 文档/视图框架的依赖. 此处是 CHtmView 依赖框架才能生存的唯一一个地方.
061.    afx_msgvoidOnDestroy();
062.    afx_msgint OnMouseActivate(CWnd* pDesktopWnd,UINTnHitTest, 
063.       UINTmsg);
064.   
065.    // 改写该函数用以捕获 "app:" 伪协议
066.    virtualvoidOnBeforeNavigate2(LPCTSTRlpszURL,
067.       DWORDnFlags,
068.       LPCTSTRlpszTargetFrameName,
069.       CByteArray& baPostedData,
070.       LPCTSTRlpszHeaders,
071.       BOOL* pbCancel );
072.   
073.    // 你可以重写这个虚函数用以处理 "app:" 命令.
074.    // 如果不涉及命令映射,则不用该写.
075.    virtualvoidOnAppCmd(LPCTSTRlpszCmd);
076.   
077.    DECLARE_MESSAGE_MAP();
078.    DECLARE_DYNAMIC(CHtmlCtrl)
079. };
080.   
081. HtmlCtrl.cpp
082. ///
083. // 实现 CHtmlCtrl 类 — 窗口控制中的 Web 浏览器。重写 CHtmlView 以便摆脱 
084. // 框架约束,可以用于对话框或任何其它窗口
085. //
086. // 特性:
087. // - SetCmdMap 使你能为"app:command"链接设置命令映射.
088. // - SetHTML 使你能将一个串设置成HTML文档内容.
089.   
090. #include "StdAfx.h"
091. #include "HtmlCtrl.h"
092.   
093. // 这个宏声明的 typedef 用于 ATL 智能指针,如:SPIHTMLDocument2
094. #define DECLARE_SMARTPTR(ifacename) typedef CComQIPtr SP##ifacename;
095.   
096. // IHTMLDocument2 接口智能指针 
097. DECLARE_SMARTPTR(IHTMLDocument2)
098.   
099. // 这是个很有用的宏,用来检查 HRESULTs
100. #define HRCHECK(x) hr = x; if (!SUCCEEDED(hr)) { \
101.    TRACE(_T("hr=%p\n"),hr);\
102.    returnhr;\
103. }
104.   
105. ...// same as earlier version
106.   
107. // 如果 hwnd 是 IE 窗口,则返回 TRUE。
108. inlineBOOLIsIEWindow(HWNDhwnd)
109. {
110.    staticLPCSTRIEWNDCLASSNAME = "Internet Explorer_Server";
111.    charclassname[32];// 必须是 char 类型, 不能是 TCHAR
112.    GetClassName(hwnd, classname,sizeof(classname));
113.    returnstrcmp(classname, IEWNDCLASSNAME)==0;
114. }
115.   
116. ///
117. // 重写函数捕获 "Internet Explorer_Server" 窗口上下文菜单消息。
118. //
119. BOOLCHtmlCtrl::PreTranslateMessage(MSG* pMsg)
120. {
121.    if(m_bHideMenu) {
122.       switch(pMsg->message) {
123.       caseWM_CONTEXTMENU:
124.       caseWM_RBUTTONUP:
125.       caseWM_RBUTTONDOWN:
126.       caseWM_RBUTTONDBLCLK:
127.          if(IsIEWindow(pMsg->hwnd)) {
128.             if(pMsg->message==WM_RBUTTONUP)
129.                // 让父窗口处理上下文菜单
130.                GetParent()->SendMessage(WM_CONTEXTMENU, pMsg->wParam, 
131.                   pMsg->lParam);
132.             returnTRUE;// eat it
133.          }
134.       }
135.    }
136.    returnCHtmlView::PreTranslateMessage(pMsg);
137. }
138.   
139. /
140. // 重写函数传递 "app:" 链接到虚函数,而不是浏览器。
141. //
142. voidCHtmlCtrl::OnBeforeNavigate2(LPCTSTRlpszURL,
143.    DWORDnFlags,LPCTSTRlpszTargetFrameName, CByteArray& baPostedData,
144.    LPCTSTRlpszHeaders,BOOL* pbCancel )
145. {
146.    constcharAPP_PROTOCOL[] = "app:";
147.    intlen = _tcslen(APP_PROTOCOL);
148.    if(_tcsnicmp(lpszURL, APP_PROTOCOL, len)==0) {
149.       OnAppCmd(lpszURL + len);         // 调用虚拟函数例程
150.       *pbCancel = TRUE;                // 取消导航
151.    }
152. }
153.   
154. ///
155. // 当浏览器试图导航到 "app:foo"时调用此函数. 缺省的命令处理映射为"foo",如果
156. // 找到命令ID,则向父窗口发送一个 WM_COMMAND 消息,调用 SetCmdMap 设置命令 
157. // 映射。如果你想要作稍微复杂一些的处理,必须重写 OnAppCmd。
158. //
159. voidCHtmlCtrl::OnAppCmd(LPCTSTRlpszCmd)
160. {
161.    if(m_cmdmap) {
162.       for(inti=0; m_cmdmap[i].name; i++) {
163.          if(_tcsicmp(lpszCmd, m_cmdmap[i].name) == 0)
164.             // 使用 PostMessage 发送消息,避免退出命令出现的问题 (在发出命令前浏览器结束导航。)
165.             GetParent()->PostMessage(WM_COMMAND, m_cmdmap[i].nID);
166.       }
167.    }
168. }
169.   
170. ///
171. // 将串转为HTML文档
172. //
173. HRESULTCHtmlCtrl::SetHTML(LPCTSTRstrHTML)
174. {
175.    HRESULThr;
176.   
177.    // 获取文档对象
178.    SPIHTMLDocument2 doc = GetHtmlDocument();
179.   
180.    // 创建串,将它作为BSTR数组的唯一个元素,因为 IHTMLDocument2::write 使用BSTR类型
181.    CComSafeArray sar;
182.    sar.Create(1,0);
183.    sar[0] = CComBSTR(strHTML);
184.   
185.    // 打开文档进行写操作
186.    LPDISPATCH lpdRet;
187.    HRCHECK(doc->open(CComBSTR("text/html"),
188.       CComVariant(CComBSTR("_self")),
189.       CComVariant(CComBSTR("")),
190.       CComVariant((bool)1),
191.       &lpdRet));
192.      
193.    HRCHECK(doc->write(sar)); // 将内容写入文档
194.    HRCHECK(doc->close());    // 关闭文档
195.    lpdRet->Release();        // 释放 IDispatch 然后返回
196.   
197.    returnS_OK;
198. }

最后一个关键的地方是 CHtmlCtrl::OnAppCmd 必须通过 PostMessage 发送命令,而不是用 SendMessage,因为如果不这样做,你会发现当执行 OnBeforeNavigate2 时,如果关闭程序会遇到麻烦(我费了好大的劲才发现这个问题)。

最后,祝大家编程愉快!

 【补充:CHtmlCtrl继承CHtmlView类,且CStatic做容器。但是显示网页后是有边框的就像Edit那样。是不是也可以像Edit那样把边框去掉呢?】

CHtmlCtrl类中增加下面的代码  
 void   CHtmlCtrl::OnDocumentComplete(LPCTSTR   lpszURL)    
 {  
 IHTMLDocument2   *phtmldoc2   =   NULL;  
 IHTMLElement   *phtmlElement   =   NULL;  
 IDispatch   *pdisp   =   NULL;  
 pdisp   =   this->GetHtmlDocument();  
 if   (pdisp   !=   NULL)  
 {  
 pdisp->QueryInterface(IID_IHTMLDocument2,   (void**)&phtmldoc2);  
 pdisp->Release();  
 }  
 if   (phtmldoc2   !=   NULL)  
 {  
 phtmldoc2->get_body(&phtmlElement);  
 phtmldoc2->Release();  
 }  
 if   (phtmlElement   !=   NULL)  
 {  
 IHTMLBodyElement   *phtmlbody   =   NULL;  
 phtmlElement->QueryInterface(IID_IHTMLBodyElement,   (void**)&phtmlbody);  
 if   (phtmlbody   !=   NULL)  
 {  
 phtmlbody->put_scroll(L"no");  
 phtmlbody->Release();  
 phtmlElement->Release();  
 }  
 }  
   
 }

 

其他网友的问题及解决:

问题:我是将动态生成的Html调用的图片、css和js文件都放在程序资源中,
现在在HtmlView中显示动态生成的html可以正常显示,Html中引用的css也没问题,
现在问题就是生成的html需要包含了其他的js文件,而这些js文件却无法正常引用。
如果把这些文件的脚本代码复制到生成的html里面就能正常运行。
下面是我的生成的html代码







......


将生成的代码写入HtmlView的代码:
HRESULT CWebBrowseView::SetHTML(LPCTSTR strHTML, LPCTSTR strName, BOOL bHistory)
{
HRESULT hr;

// Get document object

SPIHTMLDocument2 doc = GetHtmlDocument();

// Create string as one-element BSTR safe array for IHTMLDocument2::write.
CComSafeArray 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


生成html代码后调用
SetHTML(html)将生成的写入htmlView进行显示。

请问有没有什么办法可以让动态生成的html也可以调用js文件呢?
为什么调用的css没问题,同样的方法调用的js文件却无法执行呢?
谢谢啦!

 

回答:动态插入js,需要APP异步可插入协议支持
http://www.cnblogs.com/chinese-zmm/archive/2009/03/28/1424050.html

你可能感兴趣的:(c++技术)