转自: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);
如果第一个方案不行
上这段代码
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<IDispatch> spDisp = GetHtmlDocument();
if
(spDisp != NULL)
{
CComPtr<IHTMLDocument2> 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那样的限制。
转自: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的时候,这么做是明智的:
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里:
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那些给我们惹麻烦的代码。
02.
{
03.
// bypass CView doc/frame stuff
04.
return
CWnd::OnMouseActive(…);
05.
}
06.
void
CHtmlCtrl::OnDestroy()
07.
{
08.
// bypass CView doc/frame stuff
09.
CWnd::OnDestroy();
10.
}
除此之外,CHtmlCtrl也重载了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文件里。
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元素:
浏览器就会知道当前的"目录"是res://AboutHtml.exe,所以当它遇到< img src="vckcom.gif" / >,它会自动找到res://AboutHtml.exe/vckcom.gif,否则,它将在HTML文件所在的当前目录下寻找。
图一
一般来说,你可以使用res://modulename访问任何存储在EXE和DLL里的资源。res:是一个类似http:,ftp:,file:,或mailto:的协议。它告诉浏览器资源的路径和名字,细节工作浏览器知道如何去做!:) 为实现About对话框,偶写了个类,CAboutDialog,它有个CHtmlCtrl类型的成员m_page。我们来看看CaboutDialog的初始化过程:
2.
{
3.
VERIFY(Cdialog::OnInitDialog());
4.
VERIFY(m_page.CreateFromStatic(IDC_HTMLVIEW,
this
));
5.
m_page.LoadFromResource(_T(
"about.htm"
));
6.
return
TRUE;
7.
}
你可能对CHtmlCtrl::CreateFromStatic有点迷惑。 还记得我们刚开始谈到的CStatic吗? 我们打算用它来代表CHtmlCtrl控件,它将从CStatic建立一个CHtmlCtrl控件对象,这是一个子类化的过程,该对象将和CStatic有同样的ID,大小和位置。这么做很方便,很有效!:)
02.
{
03.
CStatic wndStatic;
04.
if
(!wndStatic.SubclassDlgItem(nID, pParent))
05.
return
FALSE;
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.
return
Create(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可以做任何合法的事情:
02.
LPCTSTR
lpszURL,…,
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:
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。所以,尽管麻烦,我们还是很乐意尝试。 这个也不难!! 我们同样使用子类化的原理就能实现。但不是现在,而是将来的某个时候!! :)
转自: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),其定义如下:
02.
{
03.
HWND
hwnd = hwndParent;
04.
while
(TRUE) {
05.
HWND
hwndChild = ::GetWindow(hwnd, GW_CHILD);
06.
if
(hwndChild==NULL)
07.
return
hwnd;
08.
hwnd = hwndChild;
09.
}
10.
return
NULL;
11.
}
通过这个函数返回某个父窗口下的最后一个子窗口,也就是说返回子窗口的子窗口的子窗口......直到不再有子窗口为止。可惜这个函数要获得正确的运行结果是有前提的,那就是窗口层次只能是一层,并且最终的窗口后裔是“Internet Explorer_Server”窗口。 在通常情况下,这个假设都成立。不幸的是,如果 HTML 文档中包含象 ComBoxes(组合框) 这样的控制时,这个假设就不灵了。用 Spy++ 不难发现情况并不象你期望的那样─Internet Explorer_Server是最后的子窗口。实际上,在IE中,Edit 和 Button 控制并非人们所想象的那样是子窗口。
获得 Win32 窗口句柄的更好的方法
为了解决这个问题,本文设计了一个更加完善的类:CFindWnd,用更好的算法专门来获取 IE 窗口。CFindWnd 查找某个窗口(给定窗口名字)的第一个子窗口。 例如,它的使用方法如下:
2.
myHwndIE = ies.m_hWnd;
这个类的构造函数调用函数:
函数,该函数又调用:
搜索所有后裔窗口直到找到类名匹配窗口为止。FindWindow 用来查找最顶层窗口,而搜索子窗口还得用 FindWindowEx,它是 Win32 API 函数。CFindWnd 返回第一个匹配的窗口,所以它只被用于查找你期望只有一个实例的窗口。通常在搜索特定窗口时,一般最保险的做法都是检查窗口类名。
百家争鸣
有一个读者来信指出:根本没有必要使用子类IE窗口的方法来禁用上下文菜单。完全可以在 CHtmlCtrl 内部实现,象下面这样:
2.
{
3.
if
(pMsg->message == WM_CONTEXTMENU)
4.
return
TRUE;
// eat it
5.
return
CHtmlView::PreTranslateMessage(pMsg);
6.
}
这样做是可行的,因为MFC实现了非常有独创性的、强大的特性─在 CWinThread 的主消息泵中,MFC 调用 CWnd::WalkPreTranslateTree 函数。这个函数循环消息目的地窗口的所有父窗口,调用每一个父窗口的 PreTranslateMessage ,一旦截获消息发送到后裔窗口则停止循环。非常聪明!
经验证明:要使前面的代码段按照期望的结果运行,你还必须截获 WM_RBUTTONDOWN 和 WM_RBUTTONDBLCLK 消息,同时还要做必要的检查以保证目标窗口的类名是 “Internet Explorer_Server”,这样就不会意外地捕获其它子窗口的上下文菜单(除非你确实要这么做)。下面是 CHtmlCtrl::PreTranslateMessage 的最终代码:
002.
003.
////////////////////////////////////////////////////////////////
004.
#pragma once
005.
006.
////////////////////////////////////////////////////////////////
007.
// 该结构在命令映射中定义一个入口,这个映射将文本串映射到命令IDs,
008.
// 如果命令映射中有一个映射到 ID_APP_ABOUT 的入口 “about”,并且
009.
// HTML 有一个链接锚 <A HREF="app:about">,那么单击该链接时将执行
010.
// ID_APP_ABOUT 命令。为了设置这个映射,调用 CHtmlCtrl::SetCmdMap.
011.
//
012.
//
013.
struct
HTMLCMDMAP {
014.
LPCTSTR
name;
// command name used in "app:name" HREF in
015.
// <A UINT nID;
016.
};
017.
018.
//////////////////
019.
// 这个类将 CHtmlView 转换为普通的能在对话框和框架中使用的控制
020.
//
021.
class
CHtmlCtrl :
public
CHtmlView {
022.
protected
:
023.
HTMLCMDMAP* m_cmdmap;
// command map
024.
BOOL
m_bHideMenu;
// hide context menu
025.
026.
public
:
027.
CHtmlCtrl() : m_bHideMenu(FALSE), m_cmdmap(NULL) { }
028.
~CHtmlCtrl() { }
029.
030.
// get/set HideContextMenu property
031.
BOOL
GetHideContextMenu() {
return
m_bHideMenu; }
032.
void
SetHideContextMenu(
BOOL
val) { m_bHideMenu=val; }
033.
034.
// Set doc contents from string
035.
HRESULT
SetHTML(
LPCTSTR
strHTML);
036.
037.
// set command map
038.
void
SetCmdMap(HTMLCMDMAP* val) { m_cmdmap = val; }
039.
040.
// create control in same place as static control
041.
BOOL
CreateFromStatic(
UINT
nID, CWnd* pParent);
042.
043.
// create control from scratch
044.
BOOL
Create(
const
RECT& rc, CWnd* pParent,
UINT
nID,
045.
DWORD
dwStyle = WS_CHILD|WS_VISIBLE,
046.
CCreateContext* pContext = NULL)
047.
{
048.
return
CHtmlView::Create(NULL, NULL, dwStyle, rc, pParent,
049.
nID, pContext);
050.
}
051.
052.
// 重写该函数可以截获子窗口消息,从而禁用上下文菜单。
053.
virtual
BOOL
PreTranslateMessage(MSG* pMsg);
054.
055.
// 通常,CHtmlView 自己是在 PostNcDestroy 销毁的,但对于一个界面控制来说
056.
// 我们不想那样做,因为控制一般都是作为另一个窗口对象的成员实现的。
057.
//
058.
virtual
void
PostNcDestroy() { }
059.
060.
// 重写以便旁路掉对 MFC doc/view 框架的依赖,CHtmView 仅仅在这里依附于框架。
061.
afx_msg
void
OnDestroy();
062.
afx_msg
int
OnMouseActivate(CWnd* pDesktopWnd,
UINT
nHitTest,
063.
UINT
msg);
064.
065.
// 重写以便截获 "app:" 伪协议
066.
virtual
void
OnBeforeNavigate2(
LPCTSTR
lpszURL,
067.
DWORD
nFlags,
068.
LPCTSTR
lpszTargetFrameName,
069.
CByteArray& baPostedData,
070.
LPCTSTR
lpszHeaders,
071.
BOOL
* pbCancel );
072.
073.
// 你可以重写处理 "app:" 命令的代码。注意只是在不使用命令映射机制时才需要重写
074.
virtual
void
OnAppCmd(
LPCTSTR
lpszCmd);
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<ifacename> 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.
return
hr;\
101.
}
102.
103.
...
// same as earlier version
104.
105.
// Return TRUE if hwnd is Internet Explorer window.
106.
inline
BOOL
IsIEWindow(
HWND
hwnd)
107.
{
108.
static
LPCSTR
IEWNDCLASSNAME =
"Internet Explorer_Server"
;
109.
char
classname[32];
// always char, never TCHAR
110.
GetClassName(hwnd, classname,
sizeof
(classname));
111.
return
strcmp
(classname, IEWNDCLASSNAME)==0;
112.
}
113.
114.
//////////////////
115.
// 重写后捕获 "Internet Explorer_Server" 窗口上下文菜单消息.
116.
//
117.
BOOL
CHtmlCtrl::PreTranslateMessage(MSG* pMsg)
118.
{
119.
if
(m_bHideMenu) {
120.
switch
(pMsg->message) {
121.
case
WM_CONTEXTMENU:
122.
case
WM_RBUTTONUP:
123.
case
WM_RBUTTONDOWN:
124.
case
WM_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.
return
TRUE;
// eat it
131.
}
132.
}
133.
}
134.
return
CHtmlView::PreTranslateMessage(pMsg);
135.
}
136.
137.
//////////////////
138.
// 重写后将 "app:" 链接传递到虚函数,而不是浏览器.
139.
//
140.
void
CHtmlCtrl::OnBeforeNavigate2(
LPCTSTR
lpszURL,
141.
DWORD
nFlags,
LPCTSTR
lpszTargetFrameName, CByteArray& baPostedData,
142.
LPCTSTR
lpszHeaders,
BOOL
* pbCancel )
143.
{
144.
const
char
APP_PROTOCOL[] =
"app:"
;
145.
int
len = _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.
void
CHtmlCtrl::OnAppCmd(
LPCTSTR
lpszCmd)
159.
{
160.
if
(m_cmdmap) {
161.
for
(
int
i=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.
HRESULT
CHtmlCtrl::SetHTML(
LPCTSTR
strHTML)
174.
{
175.
HRESULT
hr;
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<VARIANT> 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.
return
S_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页面是从资源中加载的:
CHtmlView::LoadFromResource 打开 res://AboutHtml.exe/about.htm,这里“AboutHtml.exe” 是可执行程序的实际名字,“res://”是一个伪协议。为了显示顶层窗口的信息,最好的办法是动态产生 HTML 页面,而不是从资源中加载,为此我在 CHtmlCtrl类中添加了一个新函数:CHtmlCtrl::SetHTML,
02.
// 通过串设置 HTML 文档内容
03.
//
04.
HRESULT
CHtmlCtrl::SetHTML(
LPCTSTR
strHTML)
05.
{
06.
HRESULT
hr;
07.
08.
// 获得文档对象
09.
SPIHTMLDocument2 doc = GetHtmlDocument();
10.
11.
// 创建只有一个元素(串)的 BSTR 数组元素
12.
// IHTMLDocument2::write.
13.
CComSafeArray<VARIANT> 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.
return
S_OK;
30.
}
下面我们一步一步来分析实现过程,首先必须获取 IHTMLDocument2 接口:
SPIHTMLDocument2 与 CComQIPtr<IHTMLDocument2> 一样是一个指向 IHTMLDocument2 的ATL智能指针,(当今 Windows 编程已进入 COM 时代,作为一名编写 Windows 应用程序的开发人员,如果你使用 COM 技术,但没有用过智能指针,那么这段代码会对你有所裨益),接着,必须创建一个SAFEARRAY,以便存放作为 BSTR 数组唯一元素的 HTML 串,SAFEARRAY是一个 COM 数据结构,其作用是在不同平台之间安全地传递数组数据,ATL提供了 CComBSTR 和 CComSafeArray 两个类,为开发人员在处理 BSTRs 和安全数组时减轻了许多痛苦:
2.
CComSafeArray<VARIANT> 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又一次显示了它的优势:
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 之前,你必须创建一个文档,最简单的方法就是打开一个空文档,就象下面这样:
此外,如果HTML很简单,你可以用 about: 代替 CHtmlCtrl::SetHTML 来得到HTML,如下面的代码:
针对简单的HTML可以这么做,如果比较复杂的文档则要调用 SetHTML。本文附带的例子程序动态构造了一个包含图像、表格、链接等元素的HTML文档, 该文档列出所有顶层窗口的信息,然后将它们显示出来,如图一所示。
例子程序的参考代码如下:
002.
// HtmlApp.cpp
003.
004.
class
CMyApp :
public
CWinApp {
005.
public
:
006.
virtual
BOOL
InitInstance();
007.
protected
:
008.
afx_msg
void
OnAppAbout();
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.
BOOL
CMyApp::InitInstance()
018.
{
019.
// Create main frame window (don''t use doc/view stuff)
020.
CMainFrame* pMainFrame =
new
CMainFrame;
021.
if
(!pMainFrame->LoadFrame(IDR_MAINFRAME))
022.
return
FALSE;
023.
pMainFrame->ShowWindow(m_nCmdShow);
024.
pMainFrame->UpdateWindow();
025.
m_pMainWnd = pMainFrame;
026.
027.
return
TRUE;
028.
}
029.
030.
031.
//////////////////////////////////////////////////////
032.
// “关于”对话框使用 HTML 控制显示内容.
033.
//
034.
class
CAboutDialog :
public
CDialog {
035.
protected
:
036.
CHtmlCtrl m_page;
// HTML control
037.
virtual
BOOL
OnInitDialog();
038.
public
:
039.
CAboutDialog() : CDialog(IDD_ABOUTBOX, NULL) { }
040.
DECLARE_DYNAMIC(CAboutDialog)
041.
};
042.
IMPLEMENT_DYNAMIC(CAboutDialog, CDialog)
043.
044.
045.
///////////////////////
046.
// 初始化“关于”对话框
047.
//
048.
BOOL
CAboutDialog::OnInitDialog()
049.
{
050.
// cmd map for CHtmlCtrl handles "app:ok"
051.
static
HTMLCMDMAP 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.
return
TRUE;
073.
}
074.
075.
/////////////////////
076.
// 运行“关于”对话框
077.
078.
void
CMyApp::OnAppAbout()
079.
{
080.
static
CAboutDialog dlg;
// static to remember state of hyperlinks
081.
dlg.DoModal();
// run it
082.
}
083.
084.
////////////////////////////////////////////////////////////////
085.
// MainFrm.h
086.
// 典型的主框架处理例程......
087.
088.
class
CMainFrame :
public
CFrameWnd {
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_msg
int
OnCreate(LPCREATESTRUCT lpCreateStruct);
098.
afx_msg
void
OnContextMenu(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.
int
CMainFrame::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.
return
0;
138.
}
139.
140.
///////////////////////////////////////////////////////////////////
141.
// 处理上下文菜单的命令函数,当前该函数只是显示TRACE信息,以示被调用过。
142.
//
143.
144.
void
CMainFrame::OnContextMenu(CWnd* pWnd, CPoint pos)
145.
{
146.
TRACE(_T(
"CMainFrame::OnContextMenu\n"
));
147.
}
148.
149.
//////////////////////////////////////////////////////////////////////
150.
// 这个函数创建在主窗口视图中显示的 HTML。
151.
// EnumWindows 回调该函数:如果窗口可见,则将窗口信息添加到 HTML table中。
152.
//
153.
static
BOOL
CALLBACK MyEnumWindowsProc(
HWND
hwnd,
LPARAM
lp)
154.
{
155.
DWORD
style = GetWindowLong(hwnd, GWL_STYLE);
156.
if
(style & WS_VISIBLE) {
157.
CString& s = *(CString*)lp;
158.
char
cname[256];
159.
GetClassName(hwnd, cname,
sizeof
(cname));
160.
TCHAR
text[1024];
161.
GetWindowText(hwnd, text,
sizeof
(text)/
sizeof
(text[0]));
162.
CString temp;
163.
temp.Format(_T(
"<TR><TD>%p </TD><TD>%s</TD><TD>%s</TD></TR>\n"
),
164.
hwnd, cname, text);
165.
s += temp;
166.
}
167.
return
TRUE;
168.
}
169.
170.
/////////////////////////////////////////////////////////////
171.
// 该函数创建一个文本串,这个串就是要显示在主窗口的 HTML 文档。
172.
173.
CString CMainFrame::FormatWindowListHTML()
174.
{
175.
// start w/top matter
176.
CString html = _T(
"<HTML><BODY STYLE=\"font-family:Verdana;\"
\
177.
link=\
"#02B7B7\" vlink=\"#02B7B7\"
>\n\
178.
<TABLE WIDTH=\
"100%\"
>\n\
179.
<TR><TD VALIGN=top><A target=\
"new\" HREF=\"http://www.vckbase.com\"
><IMG \
180.
BORDER=0 ALT=\
"VCKBASE Online Journal\" SRC=\"res://AboutHtml3.exe/mlogo.gif\"
></A></TD>\
181.
<TD COLSPAN=2><B>AboutHtml3 例子程序 -- 顶层可见窗口清单</B><BR>\n\
182.
<SMALL><A target=\
"new\"
\
183.
HREF=\
"http://www.vckbase.com\"
> VC知识库 </A></SMALL></TD></TR>\n\
184.
<TR><TD><B>窗口句柄(hwnd)</B></TD><TD><B>窗口类名</B></TD><TD WIDTH=75%><B>窗口标题</B>\
185.
</TD><TR>\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("</TABLE>\n\
192.
<P><B>[<A HREF=\
"app:about\"
>关于</A>] \
193.
[<A HREF=\
"app:exit\"
>退出</A>]</B>\n\
194.
</BODY>\n\
195.
</HTML>");
196.
197.
return
html;
198.
}
最后,我想说明一下本文例子程序中其它的一些编程技巧和诀窍,主要是针对CHtmlCtrl类的功能扩展。早在“VC6中使用CHtmlView在对话框控制中显示HTML文件”(第六期)一文中,我曾经演示了如何实现“app:”伪协议来创建HTML链接(也就是锚点)与应用程序通信。例如:你可以象下面这样添加一个链接:
然后,CHtmlCtrl::OnBeforeNavigate2 会识别出“app:”伪协议并以“about”作为参数调用专门的虚函数 CHtmlCtrl::OnAppCmd 。你可以创建自己的命令并在派生类中改写 OnAppCmd 来处理自己建立的命令。使用了 CHtmlCtrl 一段时间后。我发现经常需要派生 CHtmlCtrl 类,每次都得改写这个函数,自己感觉很麻烦!为了简化这个过程,我发明了一个简单的命令映射机制,利用这种机制可以轻松将“app:command”之类的转换为通常熟知的 WM_COMMAND 命令 ID:
2.
{ _T(
"about"
), ID_APP_ABOUT },
3.
{ _T(
"exit"
), ID_APP_EXIT },
4.
{ NULL, 0 },
5.
};
这个映射机制的使用方法是象下面这样调用 CHtmlCtrl::SetCmdMap 函数:
这样一来,当用户单击“app:about”链接时,CHtmlCtrl::OnAppCmd 便会搜索命令映射,找到“about”入口,然后将与ID_APP_ABOUT 对应的 WM_COMMAND 消息发送到其父窗口,这个技巧主要是仰仗MFC神奇的命令路由通道实现的,借助此通道,任何窗口都可以处理此命令。真是爽啊!本文例子程序正是用这种特性将“关于”和“退出”命令作为HTML链接直接添加到主窗口中。CHtmlCtrl类实现的细节代码如下:
002.
// HtmlCtrl.h
003.
#pragma once
004.
005.
//////////////////////////////////////////////////////////////
006.
// 此结构定义一个命令映射入口,映射将文本串映射到命令IDs。如果你的命令映射
007.
// 入口包含 "about" 映射到ID_APP_ABOUT,并且HTML文档中有一个锚点链接是
008.
// <A HREF="app:about">,则单击该链接将调用 ID_APP_ABOUT 命令。设置命令
009.
// 映射的方法是调用 CHtmlCtrl::SetCmdMap 函数.
010.
//
011.
struct
HTMLCMDMAP {
012.
LPCTSTR
name;
// 用于" <A HREF..." 中的 "app:name" 的命令名.
013.
UINT
nID;
014.
};
015.
016.
///////////////////////////////////////////////////////////////
017.
// 将 CHtmlView 转换为框架或对话框中常规控制的类.类似于CListView/CListCtrl
018.
// 和 CTreeView/CTreeCtrl
019.
//
020.
class
CHtmlCtrl :
public
CHtmlView {
021.
protected
:
022.
HTMLCMDMAP* m_cmdmap;
// 命令映射
023.
BOOL
m_bHideMenu;
// 隐藏上下文菜单
024.
025.
public
:
026.
CHtmlCtrl() : m_bHideMenu(FALSE), m_cmdmap(NULL) { }
027.
~CHtmlCtrl() { }
028.
029.
// 获取/设置 HideContextMenu 属性
030.
BOOL
GetHideContextMenu() {
return
m_bHideMenu; }
031.
void
SetHideContextMenu(
BOOL
val) { m_bHideMenu=val; }
032.
033.
// 根据串创建 HTML 文档
034.
HRESULT
SetHTML(
LPCTSTR
strHTML);
035.
036.
// 设置命令映射
037.
void
SetCmdMap(HTMLCMDMAP* val) { m_cmdmap = val; }
038.
039.
// 先创建一个静态控制,然后用相同的再创建一个控制
040.
BOOL
CreateFromStatic(
UINT
nID, CWnd* pParent);
041.
042.
// 创建控制
043.
BOOL
Create(
const
RECT& rc, CWnd* pParent,
UINT
nID,
044.
DWORD
dwStyle = WS_CHILD|WS_VISIBLE,
045.
CCreateContext* pContext = NULL)
046.
{
047.
return
CHtmlView::Create(NULL, NULL, dwStyle, rc, pParent,
048.
nID, pContext);
049.
}
050.
051.
// 重写用以解释子窗口消息来禁用上下文菜单
052.
virtual
BOOL
PreTranslateMessage(MSG* pMsg);
053.
054.
// 通常,CHtmlView 是在 PostNcDestroy 中将自己摧毁,
055.
// 但用于窗口控制,我们不想那么做,因为控制通常是作为
056.
// 另一个窗口对象的成员来实现的.
057.
//
058.
virtual
void
PostNcDestroy() { }
059.
060.
// 重写该函数以便旁路掉对 MFC 文档/视图框架的依赖. 此处是 CHtmView 依赖框架才能生存的唯一一个地方.
061.
afx_msg
void
OnDestroy();
062.
afx_msg
int
OnMouseActivate(CWnd* pDesktopWnd,
UINT
nHitTest,
063.
UINT
msg);
064.
065.
// 改写该函数用以捕获 "app:" 伪协议
066.
virtual
void
OnBeforeNavigate2(
LPCTSTR
lpszURL,
067.
DWORD
nFlags,
068.
LPCTSTR
lpszTargetFrameName,
069.
CByteArray& baPostedData,
070.
LPCTSTR
lpszHeaders,
071.
BOOL
* pbCancel );
072.
073.
// 你可以重写这个虚函数用以处理 "app:" 命令.
074.
// 如果不涉及命令映射,则不用该写.
075.
virtual
void
OnAppCmd(
LPCTSTR
lpszCmd);
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<ifacename> 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.
return
hr;\
103.
}
104.
105.
...
// same as earlier version
106.
107.
// 如果 hwnd 是 IE 窗口,则返回 TRUE。
108.
inline
BOOL
IsIEWindow(
HWND
hwnd)
109.
{
110.
static
LPCSTR
IEWNDCLASSNAME =
"Internet Explorer_Server"
;
111.
char
classname[32];
// 必须是 char 类型, 不能是 TCHAR
112.
GetClassName(hwnd, classname,
sizeof
(classname));
113.
return
strcmp
(classname, IEWNDCLASSNAME)==0;
114.
}
115.
116.
///////////////////////////////////////////////////////////////
117.
// 重写函数捕获 "Internet Explorer_Server" 窗口上下文菜单消息。
118.
//
119.
BOOL
CHtmlCtrl::PreTranslateMessage(MSG* pMsg)
120.
{
121.
if
(m_bHideMenu) {
122.
switch
(pMsg->message) {
123.
case
WM_CONTEXTMENU:
124.
case
WM_RBUTTONUP:
125.
case
WM_RBUTTONDOWN:
126.
case
WM_RBUTTONDBLCLK:
127.
if
(IsIEWindow(pMsg->hwnd)) {
128.
if
(pMsg->message==WM_RBUTTONUP)
129.
// 让父窗口处理上下文菜单
130.
GetParent()->SendMessage(WM_CONTEXTMENU, pMsg->wParam,
131.
pMsg->lParam);
132.
return
TRUE;
// eat it
133.
}
134.
}
135.
}
136.
return
CHtmlView::PreTranslateMessage(pMsg);
137.
}
138.
139.
/////////////////////////////////////////////////////////////
140.
// 重写函数传递 "app:" 链接到虚函数,而不是浏览器。
141.
//
142.
void
CHtmlCtrl::OnBeforeNavigate2(
LPCTSTR
lpszURL,
143.
DWORD
nFlags,
LPCTSTR
lpszTargetFrameName, CByteArray& baPostedData,
144.
LPCTSTR
lpszHeaders,
BOOL
* pbCancel )
145.
{
146.
const
char
APP_PROTOCOL[] =
"app:"
;
147.
int
len = _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.
void
CHtmlCtrl::OnAppCmd(
LPCTSTR
lpszCmd)
160.
{
161.
if
(m_cmdmap) {
162.
for
(
int
i=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.
HRESULT
CHtmlCtrl::SetHTML(
LPCTSTR
strHTML)
174.
{
175.
HRESULT
hr;
176.
177.
// 获取文档对象
178.
SPIHTMLDocument2 doc = GetHtmlDocument();
179.
180.
// 创建串,将它作为BSTR数组的唯一个元素,因为 IHTMLDocument2::write 使用BSTR类型
181.
CComSafeArray<VARIANT> 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.
return
S_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代码
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<HTML xmlns="http://www.w3.org/1999/xhtml">
<HEAD></HEAD>
<link href="res://AppFrameA.exe/style.css" rel="stylesheet" type="text/css">
<script type="text/javascript" src="res://AppFrameA.exe/commn.js"></script>
<BODY onload="TestFunc();">
......
将生成的代码写入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<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
}
生成html代码后调用
SetHTML(html)将生成的写入htmlView进行显示。
请问有没有什么办法可以让动态生成的html也可以调用js文件呢?
为什么调用的css没问题,同样的方法调用的js文件却无法执行呢?
谢谢啦!
回答:动态插入js,需要APP异步可插入协议支持
http://www.cnblogs.com/chinese-zmm/archive/2009/03/28/1424050.html