问题提出:
金山词霸、网络蚂蚁等软件安装后会向IE的工具条添加自己的按钮。按下按钮后还会作出相应的动作,这种功能是如何实现的呢?读完本文,您也可以将自己应用程序的按钮添加到IE的工具条中。
基本原理:
从IE5开始便允许我们向工具栏添加自己的按钮,其本质就是修改注册表,添加创建此按钮所需的信息。
实现步骤:
1.创建此按钮的GUID(globally unique identifier)
你可以通过Visual Studio中的Guidgen.exe来产生GUID。
例如我生成的GUID是{1FBA04EE-3024-11D2-8F1F-0000F87ABD16}
以下的例子中我都使用这个GUID来作说明。
2.创建子键HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer\Extensions\{1FBA04EE-3024-11D2-8F1F-0000F87ABD16}
3.在此子键下创建如下字串值。
(1)CLSID
这是IE的CLSID,其值必须为{1FBA04EE-3024-11D2-8F1F-0000F87ABD16}
(2)Default Visible
指明此按钮默认状态下是否可见,Yes表示可见,No为不可见
(3)ButtonText
按钮文字
(4)Icon
默认状态下的图标全路径,例如c:\vckbase.ico。也可以是EXE文件中包含的图标,例如:C:\PROGRA~1\NETANTS\NetAnts.exe,1000
(5)HotIcon
鼠标移到按钮上时的图标全路径
如下子键为按下按钮后需要执行的相应动作:可以是COM对象、浏览条Explorer Bar、脚本Script、可执行文件。
下面我们逐个进行介绍。
①COM对象
你需要建立名为ClsidExtension的字串值,其值应该为此COM对象的GUID
例如金山词霸就使用ClsidExtension来调用自己的COM对象。
②浏览条Explorer Bar
所谓浏览条就是类似我们按下往IE历史按钮后打开的历史记录列表,其本质好比MFC中的CReBar对象。 浏览条也可以自己制作,
因为超出了本文的范围,暂不作讲解。
为了在按下按钮后打开一个浏览条,你需要建立名为BandCLSID的字串值,其值为浏览条的CLSID
③脚本Script
按下按钮后执行的脚本,例如:"%SystemRoot%\web\related.htm"
你可以在这个HTML文件里面加上脚本来得到IE当前的许多信息,需要注意的是假如你想通过Script打开非脚本的HTML文件是不可行的。
请参考如下NetAnts取得当前页所有链接的脚本代码
<script language="VBScript"> --On Error Resume Next --set NetAntsApi = CreateObject( "NetAnts.API" ) --if err<>0 then -- --Alert("NetAnts not properly installed on this PC!") --else -- --set links = external.menuArguments.document.links -- --ReDim params(links.length*2) -- --params(0)=external.menuArguments.document.Url -- --for i = 0 to links.length-1 -- -- --params(i*2+1)=links(i).href -- -- --params(i*2+2)=links(i).innerText -- --next -- --NetAntsApi.AddUrlList params --end if </script> |
我们再看一个比较有用的脚本,这段脚本的作用是得到当前地址,并打开此网址的首页。
<script> --//userURL得到的是当前地址,例如是http://www.vckbase.com/article/controls/index.html --userURL=external.menuArguments.location.href; --protocolIndex=userURL.indexOf("://",4); --serverIndex=userURL.indexOf("/",protocolIndex + 3); --finalURL=userURL.substring(0,serverIndex); --external.menuArguments.open(finalURL, "_blank");///打开网址http://www.vckbase.com/ </script> |
关于external等对象的具体使用方法请参阅微软的《动态HTML开发参考大全》--人民邮电出版社出版
④可执行文件
假如我们想让IE在按下按钮后执行一个可执行文件, 你可以增加名为Exec的字串值,其值为此可执行文件的全路径,
例如c:\windows\notepad.exe或者是一个网址http://www.vckbase.com/index.html
下面我们介绍一个简单的例子。
void CIEButtonDlg::OnAdd() { --///这是由GUIDGEN产生的GUID:{06926B30-424E-4f1c-8EE3-543CD96573DC} --CRegKey reg; --char KeyName[]="Software\\Microsoft\\Internet Explorer\\Extensions\\{06926B30-424E-4f1c-8EE3-543CD96573DC}"; --TCHAR PathName[MAX_PATH]; --TCHAR IconPathName[MAX_PATH]; ///正常时的图标全路径 --TCHAR HotIconPathName[MAX_PATH]; ///鼠标覆盖时的图标全路径 --GetModuleFileName(0,PathName,MAX_PATH); ///得到本可执行文件的路径 --strcpy(IconPathName,PathName); --strcpy(HotIconPathName,PathName); --strcat(HotIconPathName,",131"); ///131是图标的ID,你可以以资源方式打开EXE文件就可以看到所有资源及其ID --strcat(IconPathName,",129"); --reg.Create(HKEY_LOCAL_MACHINE,KeyName); --reg.SetValue("{1FBA04EE-3024-11D2-8F1F-0000F87ABD16}","CLSID"); --reg.SetValue("Yes","Default Visible"); --reg.SetValue("VC知识库","ButtonText"); --reg.SetValue(IconPathName,"Icon"); --reg.SetValue(HotIconPathName,"HotIcon"); --/假如是执行脚本,可以是reg.SetValue("c:\\test.html","Script"); ///在test.html存放你的脚本代码 --reg.SetValue("http://www.vckbase.com/","Exec");///打开网页 } |
在此例子中我们实现了一个按钮,点击它便会打开VC知识库首页。
具体可以参考本文提供的示例工程
如何获取网页密码框中的密码
作者:Sjx
下载源代码
前言
本人是在家中上网,经常有一些BBS的密码懒得记了,就用IE的自动密码保存功能,这样一来是方便了,但却有一个麻烦,一旦机子不行了,想要重装操作系统了,这些密码却也取不出了,还得重新申请,好麻烦!因此我就写了一个工具,可以取得网页密码框的密码.
因为网页密码框不是一般的EDIT控件,因此不能取得网页密码框的句柄.要实现这个功能,只好通过WebBrowser控件的有关COM接口了.因此取得这些接口是整个程序的关键.有两种方法可以取得WebBrowser控件的接口,接下来我们会逐一介绍,并提供示例源代码供大家参考。
示例程序运行效果图
第一种方法:使用脚本语言和IE右键菜单
我们可以使用注册表来控制IE右键菜单.当你装了FlashGet(网际快车)时,你会发现IE右键菜单多了两项:"使用网际快车下载"和"使用网际快车下载全部链接",而这时你打开注册表,在HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt\下有这两个主键.这两个主键下都有两个值,一个是默认的串值,指定了选择了这个菜单命令要打开的URL,IE在一个隐藏的窗口打开它,并这个隐藏窗口的external.menuArguments值设为当前窗口对象,执行完URL对话网页包含的脚本程序该窗口自动关闭.另一个名称是contexts是DWORD值,指定了在什么情况下需要显示这个菜单项.具体的值见下.
(0x1 << CONTEXT_MENU_DEFAULT) (等于 0x1) //缺省时显示(0x1 << CONTEXT_MENU_IMAGE) (等于 0x2) //右键点击图像时显示该项(0x1 << CONTEXT_MENU_CONTROL) (等于 0x4) //右键点击表单元素时显示该项(0x1 << CONTEXT_MENU_TABLE) (等于 0x8) //右键点击表格时显示该项(0x1 << CONTEXT_MENU_TEXTSELECT) (等于 0x10) //右键点击高亮选择的文本时显示该项(0x1 << CONTEXT_MENU_ANCHOR) (等于 0x20) //右键点击链接时显示该项(0x1 << CONTEXT_MENU_UNKNOWN) (等于 0x40)//右键点击网页中除上以外的地方显示该项现在我们写一段 VBScript 脚本程序以获取密码框的值.
Sub GetPassword()set srcEvent = external.menuArguments.eventSet doc=external.menuArguments.documentset ele=doc.elementFromPoint( srcEvent.clientX, srcEvent.clientY )if ele.type ="password" thenif ele.value="" thenAlert("密码为空")elseAlert("密码为:"+ele.value)end ifend ifend subcall GetPassword()然后在注册表HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt\下新建一下主键,键名为"取得密码",缺省值设为该htm文件的路径,在该主键下另增一个DWORD值,值为4,表示只在右击表单元素时显示该项.关闭注册表,重新启动IE窗口,点击一下密码框,就会出现该项,点击该项,弹出一个对话框,告诉你的密码 。
第二种方法:使用VC来实现
由于VC知识库是一个关于C++以及Visual C++的网站,与脚本语言没什么关系。所以我们要用另一种稍微复杂一点的方法来实现相同的事情,那就是用C++来做。在不同的进程中取得IE的Webbrowser控件的IHTMLDocument2接口,请参阅MSDN上的一篇文章,标题是:HOWTO: Get IHTMLDocument2 from a HWND(根据HWND取得IHTMLDocument2接口)(http://support.microsoft.com/default.aspx?scid=kb;EN-US;q249232).它的实现机理是向Webbrowser控件(窗口类名是"Internet Explorer_Server")发一个WM_HTML_GETOBJECT,然后把返回值传给Microsoft Active Accessibility (MSAA) 函数ObjectFromLresult,这样你会取得一个已经编排(Marshaling)过的COM接口.如下函数所示:
IHTMLDocument2* GetDocInterface(HWND hWnd){// 我们需要显示地装载OLEACC.DLL,这样我们才知道有没有安装MSAAHINSTANCE hInst = ::LoadLibrary( _T("OLEACC.DLL") );IHTMLDocument2* pDoc2=NULL;if ( hInst != NULL ){if ( hWnd != NULL ){CComPtr spDoc=NULL;LRESULT lRes;/*由于WM_HTML_GETOBJECT非Windows标准消息,所以需要RegisterWindowMessage*/UINT nMsg = ::RegisterWindowMessage( _T("WM_HTML_GETOBJECT") );::SendMessageTimeout( hWnd,nMsg,0L,0L,SMTO_ABORTIFHUNG,1000,(DWORD*)&lRes );/*取得ObjectFromLresult函数的地址*/LPFNOBJECTFROMLRESULT pfObjectFromLresult = \(LPFNOBJECTFROMLRESULT)::GetProcAddress( hInst,_T("ObjectFromLresult") );if ( pfObjectFromLresult != NULL ){HRESULT hr;hr=pfObjectFromLresult(lRes,IID_IHTMLDocument,0,(void**)&spDoc);if ( SUCCEEDED(hr) ){CComPtr spDisp;CComQIPtr spWin;spDoc->get_Script( &spDisp );spWin = spDisp;spWin->get_document( &pDoc2 );}}}::FreeLibrary(hInst);}else{//如果没有安装MSAAAfxMessageBox(_T("请您安装Microsoft Active Accessibility"));}return pDoc2;}这样,我们就取得了IHTMLDocument2*接口了,要取得密码框的密码还得一番周折,首先得构造一个基于对话框的MFC程序,增加一个按钮,在主对话框类增加一个成员变量m_bCapture,在构造函数中初始化为FALSE. 然后处理该按钮的Click事件:
void CXXXXDlg::OnGetHtmlClick(){SetCapture();//跟踪鼠标m_bCapture=TRUE;}接着应该处理WM_LBUTTONUP消息:
void CXXXXDlg::OnLButtonUp(UINT nFlags, CPoint point){if(m_bCapture){m_bCapture=FALSE;ReleaseCapture();//释放鼠标static TCHAR buf[100];POINT pt;GetCursorPos(&pt);HWND hwnd=::WindowFromPoint(pt);if(hwnd!=NULL){::GetClassName( hwnd, (LPTSTR)&buf, 100 );if ( _tcscmp( buf, _T("Internet Explorer_Server") ) == 0 ){POINT iept=pt;::ScreenToClient(hwnd,&iept);GetPassword(GetDocInterface(hwnd),iept);}}}CDialog::OnLButtonUp(nFlags, point);}GetPassword函数是这样实现的,基本可以模仿VBScript的调用,但要复杂一些:
void GetPassword(IHTMLDocument2* pDoc2,POINT pt){if(pDoc2==NULL)return;CComPtr pElement;HRESULT hr=pDoc2->elementFromPoint(pt.x,pt.y,&pElement);//取得鼠标所在的元素if(SUCCEEDED(hr)){CComPtr pPwdElement;hr=pElement->QueryInterface(IID_IHTMLInputTextElement,(void**)&pPwdElement);//是否有表单输入元素if(SUCCEEDED(hr)){CComBSTR type;hr=pPwdElement->get_type(&type);if(SUCCEEDED(hr)){if(type==_T("password")){//是密码框吗?CComBSTR pwd;hr=pPwdElement->get_value(&pwd);if(SUCCEEDED(hr)){if(pwd.Length()!=0){//有密码则显示CComBSTR msg=_T("密码是:");msg+=pwd;CString str(msg);AfxMessageBox(str);}else{AfxMessageBox(_T("密码为空!"));}}}}}}pDoc2->Release();}使用这种方法要注意:
- 如果程序在Windows95,98和NT 4.0 Service With Pack 4 or 5下运行必须要把Microsoft Active Accessibility (MSAA)运行时组件(RDK)与程序一起发布(Windows2000及Windows NT 4.0 Service With Pack 6中已经有了,所以不用);
- 这种方法只适用用于Internet Explorer (Programming) versions 4.0, 4.01, 4.01 SP1, 4.01 SP2, 5;
- 使用这种方法前要调用CoInitialize(NULL);然后应该相应地调用CoUninitialize();
- Microsoft Active Accessibility (MSAA)可从 http://www.microsoft.com/enable/msaa/download.htm 下载 ;
附: 我们也可以使用Active Accessibility(MSAA)获取IHTMLDocument2*接口,见下程序:
/*
函数名:GetDocInterfaceByMSAA
参数:hwnd,WebBrowser控件的窗口句柄
功能:取得hwnd对应的Webbrowser控件的IHTMLDocument2*接口.
*/
IHTMLDocument2* GetDocInterfaceByMSAA(HWND hwnd){HRESULT hr;HINSTANCE hInst = ::LoadLibrary( _T("OLEACC.DLL") );IHTMLDocument2* pDoc2=NULL;if ( hInst != NULL ){if ( hwnd != NULL ){//取得AccessibleObjectFromWindow函数LPFNACCESSIBLEOBJECTFROMWINDOW pfAccessibleObjectFromWindow =(LPFNACCESSIBLEOBJECTFROMWINDOW)::GetProcAddress(hInst,_T("AccessibleObjectFromWindow"));if(pfAccessibleObjectFromWindow != NULL){CComPtr spAccess;hr=pfAccessibleObjectFromWindow(hwnd,0,//取得Webbrowser控件的IAccessible接口IID_IAccessible,(void**) &spAccess);if ( SUCCEEDED(hr) ){CComPtr spServiceProv;hr=spAccess->QueryInterface(IID_IServiceProvider,(void**)&spServiceProv);if(hr==S_OK){CComPtr spWin;hr=spServiceProv->QueryService(IID_IHTMLWindow2,IID_IHTMLWindow2,(void**)&spWin);/*注意:并不是每次都能取得IHTMLWindow2接口,如果调用失败,可以尝试取得IHTMLElement接口:CComPtr spElement;hr=spServiceProv->QueryService(IID_IHTMLElement, IID_IHTMLElement,(void**)&spElement);*/if(hr==S_OK)spWin->get_document(&pDoc2);}}}}::FreeLibrary(hInst);}else{AfxMessageBox(_T("请您安装Microsoft Active Accessibility"));}return pDoc2;}具体的例程可见本文提供的源代码(在WINDOWS ME,IE 5.5,VC6.0调试通过)
如何控制IE的行为
作者:ac952_z_cn
下载源代码 |
下面演示一个控制IE进行刷新的方法: 第一步、创建 |
SHDocVw::IShellWindowsPtr m_spSHWinds; if (m_spSHWinds == NULL){ if (m_spSHWinds.CreateInstance(__uuidof(SHDocVw::ShellWindows)) != S_OK){ MessageBox("Failed"); CoUninitialize(); EndDialog(1); } } |
第二步、获取当前所有打开的IE窗口 |
if (m_spSHWinds){ int n = m_spSHWinds->GetCount(); for (int i = 0; i < n; i++){ _variant_t v = (long)i; IDispatchPtr spDisp = m_spSHWinds->Item(v); SHDocVw::IWebBrowser2Ptr spBrowser(spDisp); //生成一个IE窗口的智能指针 if (spBrowser){ _bstr_t bsName = spBrowser->GetLocationName(); //窗口名称 int nPos = m_ctrlIE.InsertItem(0, bsName); spBrowser->AddRef(); void * pData = spBrowser; m_ctrlIE.SetItemData(nPos, (DWORD)(pData)); } } } |
第三步、刷新IE窗口 |
pBrowser->Refresh(); //pBrowser指向SHDocVw::IWebBrowser2Ptr |
当然我们获取了IE窗口的IWebBrowser2后,我们就可以控制该IE,如读取设置元素的内容,以及其他一些更过的控制,参考IWebBrowser2,IHTMLDocument2等! 编译环境 win2000 professional + VC6.0(en) + sp5.0 + Platform SDK 2001.8 在win2000 professional下测试通过,如编译不过,请下载最新Platform SDK |
如何禁用HTML页面的上下文菜单
赵湘宁 |
下载本文范例代码: AboutHtml1 AboutHtml2 |
提出问题: |
VC知识库《在线杂志》第六期有一篇文章“VC6中使用CHtmlView在对话框控制中显示HTML文件”,很多读者来信说很喜欢这种功能。但是美中不足的是在对话框的HTML页面上单击鼠标右键会弹出上下文菜单。从而可以象在IE中那样看到页面的源代码。为了防止用户查看HTML代码,有人尝试过在CHtmlCtrl派生的窗口中重载WM_CONTEXTMENU,或者在CHtmlView以及CHtmlCtrl类中禁用右键的上下文菜单和弹出式菜单,这两个方法都没有成功。那么如何禁用HTML的这个上下文菜单呢? 本文就针对这个问题用不同的方法来完善上次的程序。 |
解答: |
CHtmlCtrl类可以将CHtmlView转换成在任何窗口中使用的控制。我用它写了一个程序叫AboutHtml,此程序实现了一个HTML对话框。但疏忽了鼠标右键的上下文菜单,所以在HTML对话框中单击鼠标右键,会弹出标准的浏览器上下文菜单(如图一),而这个菜单对于某些人来说可能是多余的。 |
|
图一 不想要的上下文菜单 |
其实,要解决这个问题有一个非常简单的办法,真是易如反掌,甚至不用写任何C++代码!只要在HTML页面中加一行指令即可: |
// // |
这条指令告诉浏览器不要显示上下文菜单。也可以象下面这样写: |
// οncοntextmenu="ShowMyMenu(); return false" // |
ShowMyMenu是一个显示定制菜单的JavaScript过程。本文例子代码之一AboutHtml1使用的就是oncontextmenu。源代码可以从本文的开始处下载。 由于VC知识库是一个关于C++以及Visual C++的网站,与JavaScript之类的脚本语言没什么关系。所以我们要用另一种稍微复杂一点的方法来实现相同的事情,那就是用C++来做。为此,正规的C++方法是实现IDocHostUIHandler接口,而且要做的事情很多。至于为什么要实现它,请参见有关文档。用WM_CONTEXTMENU 或者 WM_RBUTTONDOWN来处理这个问题的思路的确是通常Windows做事情的方式。但是问题是CHtmlCtrl窗口不是真正的输入窗口。窗口有很多种,只要用Spy++工具看一下我们的例子程序就知道在你眼前会出现多少种窗口。如图二所示,在实际的输入窗口上,浏览器窗口有三级父/子窗口。 |
Dialog AfxFrameOrView42d // CHtmlCtrl Shell Embedding Shell DocObject View Internet Explorer_Server |
它是个接收输入的Internet Explorer_Server服务器窗口,并且如果你想要截获WM_CONTEXTMENU消息,必须子类化这个窗口。在MFC中,这意味着你必须获取HWND并调用SubclassWindow。记住了,这是一种非常规方式,而且微软的那帮家伙也明确禁止这样做,不过我还是根据原来的程序写了另一个版本AboutHtml2,我这么做了。 |
![]() |
图二在Spy++中的父/子关系 |
获得这个神秘的Internet Explorer_Server HWND的方法有很多种。但FindWindow不行,因为它只能得到顶层窗口。由于此服务器窗口是浏览器的曾孙(great-grandchild),在所有层次上都没有同胞兄弟,所以下列算法成立: |
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窗口。一旦取得HWND,剩下的事情便是写一个新的MFC类对它进行子类化。 |
class CMyIEWnd : public CWnd { public: afx_msg void OnContextMenu(CWnd* pWnd, CPoint pos) { } DECLARE_MESSAGE_MAP(); }; |
这个类重载WM_CONTEXTMENU,其它什么事情也不做:OnContextMenu是个空函数,返回的东西不显示菜单,也不调用基类(CWnd)的方法。使用CMyIEWnd时,在CMyHtmlCtrl中添加一个实例: |
// class CMyHtmlCtrl : public CHtmlCtrl { protected: CMyIEWnd m_myIEWnd; }; // |
把这一切联系在一起的最关键的一步是调用SubclassWindow。但在哪里调用以及什么时候调用呢?最好时机是在浏览器加载页面之后。 |
void CMyHtmlCtrl::OnNavigateComplete2(LPCTSTR strURL) { if (!m_myIEWnd.m_hWnd) { HWND hwnd = GetLastChild(m_hWnd); m_myIEWnd.SubclassWindow(hwnd); } } |
具体处理过程是这样的:当用户打开“关于”对话框,对话框创建CHtmlCtrl窗口来打开文档,当浏览器将文档打开以后,它发送一个通知,MFC将这个通知定向到OnNavigateComplete2。CMyHtmlCtrl::OnNavigateComplete2调用GetLastChild来获得“真正的”输入窗口并将它子类化。这时所有的消息将通过CMyIEWnd类去往Internet Explorer_Server,包括WM_CONTEXTMENU。这里要注意,IE的HWND是可以修改的,所以如果除了“关于”对话框外,你还想做一些其它的事情的话,必须要对HWND进行反子类化(unsubclass)和重子类化(resubclass)处理。 使用这个技术有两个重要事情需要注意。第一,它功能很强,因为你子类化了“真正的”IE窗口,你可以做几乎任何事情。第二,如果你不小心而使用不当,那将会发生最糟糕最糟糕的事情。一旦你用这种方法控制了资源管理器窗口,等于是把所有赌注放进去了。记住不要用不正当的方式去玩弄浏览器,而是要通过正式接口(IDocHostUIHandler)定制它!否则后果不堪设想。 |
相关文章:VC6中使用CHtmlView在对话框控制中显示HTML文件 |
利用IEHelper实现简单网址过滤
作者:谭昕
下载本文示例源代码
Microsoft为"我的电脑",IE流浏览器都设置了接口,只要你注册一个自己定义的COM DLL库,windows 在IE启动的时候都会调用这个DLL,(相当于挂钩了),所以你在你的DLL文件中对输入的网址进行处理了! 下载一个IEhelper,IE浏览网页的时候有几个事件要发生:
ID分别为:
DISPID_BEFORENAVIGATE2
DISPID_NAVIGATECOMPLETE2
DISPID_DOWNLOADBEGIN
DISPID_DOWNLOADCOMPLETE
DISPID_DOCUMENTCOMPLETE
等等,所以你只有你在ID为DISPID_BEFORENAVIGATE2的事件中做处理,你就可以实现过滤了! 部分代码如下:
//因为IEhelper会在explorer.exe和iexplorer.exe启动时加载,//所以这里设置只允许iexplorer.exe加载这个DLLTCHAR Loader[MAX_PATH];GetModuleFileName( NULL, Loader, MAX_PATH);for ( int i = lstrlen( Loader); i > 0; i--)if ( Loader[i] == _T(''''\\'''')){lstrcpy( Loader, Loader + i + 1);break;}if ( lstrcmpi( Loader, _T("iexplore.exe")) != 0 &&lstrcmpi( Loader, _T("regsvr32.exe")) != 0)return FALSE;//设置网址过滤case DISPID_BEFORENAVIGATE2:char *str;if (pDispParams->cArgs >= 5 && pDispParams->rgvarg[5].vt == (VT_BYREF|VT_VARIANT)){CComVariant varURL(*pDispParams->rgvarg[5].pvarVal);varURL.ChangeType(VT_BSTR);//转化要访问的网址为char *型str = OLE2A(varURL.bstrVal);}//如果正要访问的网址为要被拦截的,则stopif(strstr(str,"www.163.net")!=NULL){*pDispParams->rgvarg[0].pboolVal = TRUE;MessageBox(NULL,"当前系统禁止浏览该页","警告",MB_ICONSTOP);return S_OK;}break;详细请参考源代码
注意:
加载这个DLL用:regsvr32 x:\xx\iehelper.dll
卸载用:regsvr32 /u x:\xx\iehelper.dll
x就是你的安置目录,这个dll和3721网络实名冲突,估计它也是采用这个技术。
利用IEHelper实现IE弹出窗口过滤
作者:谭昕
下载本文示例源代码
上次讲过《利用IEHelper实现简单网址过滤》,利用上次的程序框架实现“IE弹出窗口过滤”,效果比较明显。
程序原理是这样的:在IE下载网页的内容的时候截获IE的OnNewWindow事件,对他进行重载,网页未下载完的所有弹出窗口均不创建,这样大部分的弹出窗口就可以不出现。
部分代码如下:
//因为IEhelper会在explorer.exe和iexplorer.exe启动时加载,//所以这里设置只允许iexplorer.exe加载这个DLLTCHAR Loader[MAX_PATH];GetModuleFileName( NULL, Loader, MAX_PATH);for ( int i = lstrlen( Loader); i > 0; i--)if ( Loader[i] == _T(''\\'')){lstrcpy( Loader, Loader + i + 1);break;}if ( lstrcmpi(_strlwr(Loader), _T("iexplore.exe")) != 0 &&lstrcmpi( _strlwr(Loader), _T("regsvr32.exe")) != 0)return FALSE;/case DISPID_NEWWINDOW2://设置是否允许弹出窗口,如果所有内容都下载完毕了,则可以弹出窗口READYSTATE m_ReadyState;m_spWebBrowser2->get_ReadyState(&m_ReadyState);if (m_ReadyState!=READYSTATE_COMPLETE){*pDispParams->rgvarg[0].pboolVal = TRUE;return S_OK;}详细请参考源代码
注意:
加载这个DLL用:regsvr32 x:\xx\iehelper.dll
卸载就用:regsvr32 /u x:\xx\iehelper.dll
x就是你的安置目录,这个dll和3721网络实名冲突,估计它也是采用这个技术。
如何获取网页中的密码和文本输入框的内容
作者:谭昕
下载示例工程
一、介绍
网页中的密码输入框和一般不同,它没有句柄之类的,但是通过获取IE的IHTMLInputTextElement接口,就可以获取网页中的输入框(包括文本和密码输入框)的内容了。
源代码在VC知识库首页运行效果图如下:
二、具体代码
VARIANT id, index;CComPtr<IDispatch> spDispatch;CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> pDoc2;CComPtr<IHTMLElement> pElement;CComPtr<IHTMLElementCollection> pElementCol;CComPtr<IHTMLFormElement> pFormElement;CComPtr<IHTMLInputTextElement> pInputElement;//首先获取IWebBrowser2接口CoInitialize(NULL); //必须要这句初始化SHDocVw::IWebBrowser2Ptr spBrowser(spDisp);if (m_spSHWinds == NULL){if (m_spSHWinds.CreateInstance(__uuidof(SHDocVw::ShellWindows)) != S_OK){MessageBox("Failed");CoUninitialize();}}if (m_spSHWinds){int n = m_spSHWinds->GetCount();for (int i = 0; i < n; i++){_variant_t v = (long)i;IDispatchPtr spDisp = m_spSHWinds->Item(v);SHDocVw::IWebBrowser2Ptr spBrowser(spDisp); //生成一个IE窗口的智能指针if (spBrowser){//获取IHTMLDocument2接口if (SUCCEEDED(spBrowser->get_Document( &spDispatch)))pDoc2 = spDispatch;if(pDoc2!=NULL){// AfxMessageBox("已经获取IHTMLDocument2");if (SUCCEEDED(pDoc2->get_forms(&pElementCol))){// AfxMessageBox("已经获取IHTMLElementCollection");long p=0;if(SUCCEEDED(pElementCol->get_length(&p)))if(p!=0){for(long i=0;i<=(p-1);i++){V_VT(&id) = VT_I4;V_I4(&id) = i;V_VT(&index) = VT_I4;V_I4(&index) = 0;if(SUCCEEDED(pElementCol->item(id,index, &spDispatch)))if(SUCCEEDED(spDispatch->QueryInterface(IID_IHTMLFormElement,(void**)&pFormElement))){// AfxMessageBox("已经获取IHTMLFormElement");long q=0;if(SUCCEEDED(pFormElement->get_length(&q)))for(long j=0;j<=(q-1);j++){V_VT(&id) = VT_I4;V_I4(&id) = j;V_VT(&index) = VT_I4;V_I4(&index) = 0;if(SUCCEEDED(pFormElement->item(id,index, &spDispatch)))if(SUCCEEDED(spDispatch->QueryInterface(IID_IHTMLInputTextElement,(void**)&pInputElement))){//AfxMessageBox("已经获取IHTMLInputTextElement");CComBSTR value;CComBSTR type;pInputElement->get_type(&type); //获取输入框类型(密码框还是文本框)CString strtype(type);strtype.MakeUpper();if(strtype.Find("TEXT")!=-1) //获取文本框的值{pInputElement->get_value(&value);CString str(value);if(!str.IsEmpty())m_ctrlIE.InsertItem(0, _bstr_t(value)+_bstr_t(" 【可能是用户名或其他需提交的内容】"));}else if(strtype.Find("PASSWORD")!=-1) //获取密码框的值{pInputElement->get_value(&value);CString str(value);if(!str.IsEmpty())m_ctrlIE.InsertItem(0, _bstr_t(value) + _bstr_t(" 【应该是密码】"));}}}}}}}}}}}注意:由于我也比较懒,本文框架是采用一篇名为《如何控制IE的行为》...在这里感谢原文作者,但是本文的主要代码是我写的,(其实自己写一个框架也太简单了,但是我还要上班啊 :( 请原谅!)最好不要向作者要技术支持!谢谢阅读!
如何提取网页中所有链接
作者:谭昕
下载本文示例源代码
见过“网际快车”的“使用网际快车下载全部链接”这个功能吗?想实现它,我们可以这样做:
IE有几个有用的接口,我们可以用它来提取网页所有链接。 一、基本原理
首先是用IHTMLDocument2的get_links,来获取IHTMLElementCollection接口,再通过IHTMLElementCollection来获取IHTMLAnchorElement,而IHTMLAnchorElement接口的get_href就是我们想要的,通过循环获取,我们就可以得到网页的所有链接了! 二、具体代码
{TCHAR HostName[2*MAX_PATH];CComPtr<IDispatch> spDispatch;CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> pDoc2;CComPtr<IHTMLElementCollection> pElementCol;CComPtr<IHTMLAnchorElement> pLoct;// TODO: Add your control notification handler code hereint n = m_LinksList.GetItemCount();//GetCount();for (int i = 0; i < n; i ++){IWebBrowser2 *pBrowser = (IWebBrowser2 *)m_LinksList.GetItemData(i);if (pBrowser){pBrowser->Release();}}m_LinksList.DeleteAllItems();m_LinksNum = 0;Log("**************************************************************");Log("\r\n");if (m_spSHWinds){int n = m_spSHWinds->GetCount();for (int i = 0; i < n; i++){_variant_t v = (long)i;IDispatchPtr spDisp = m_spSHWinds->Item(v);SHDocVw::IWebBrowser2Ptr spBrowser(spDisp); //生成一个IE窗口的智能指针if (spBrowser){if (SUCCEEDED(spBrowser->get_Document( &spDispatch)))pDoc2 = spDispatch;if(pDoc2!=NULL){if (SUCCEEDED(pDoc2->get_links(&pElementCol))){// AfxMessageBox("IHTMLElementCollection");long p=0;if(SUCCEEDED(pElementCol->get_length(&p)))if(p!=0){m_LinksNum = m_LinksNum+p;UpdateData(FALSE);for(long i=0;i<=(p-1);i++){BSTR String;_variant_t index = i;if(SUCCEEDED(pElementCol->item( index, index, &spDispatch)))if(SUCCEEDED(spDispatch->QueryInterface( IID_IHTMLAnchorElement,(void **) &pLoct)))pLoct->get_href(&String);ZeroMemory(HostName,2*MAX_PATH);lstrcpy(HostName,_bstr_t(String));m_LinksList.InsertItem(i,HostName);m_LinksList.SetCheck(i,TRUE);pLoct->get_hostname(&String);ZeroMemory(HostName,2*MAX_PATH);lstrcpy(HostName,_bstr_t(String));if(lstrlen(HostName)){m_LinksList.SetItemText(i,1,HostName);Log(HostName );Log("\r\n");}}}}}}}}}本程序在VC7+WINXP下编译通过,详细请看源代码!
定制浏览器
作者:冯明德
浏览器控件是个典型的Active控件,提供了大量的接口及自动化对象,可以灵活的加以控制,需要的时候,可以通过这些接口控制浏览器的行为,或提供相应的出接口定制浏览器。
一、概述
浏览器对象CLSID:
CLSID_WebBrowser
提供的主要接口
IWebBrowser2 浏览器的接口
当文档建立后,可以得到相应的文档接口,文档中各标记元素的接口。
在DHTML中,大量的对象和事件就是又这些接口提供和管理的。
IHTMLDocument2
IHTMLWindow2
IHTMLEventObj
IHTMLElement
....
浏览器还将调用宿主提供的接口,以发出事件或给用户提供定制机会。
出接口
DIID_DWebBrowserEvents2
DIID_HTMLDocumentEvents
DIID_HTMLWindowEvents
(ICustomDoc)
IDocHostUIHandler
二、事件的相应
除了使用MFC缺省的事件响应机制外,也可以自建事件接受器,来响应事件
也就是,在封装对象中提供DIID_DWebBrowserEvents2 接口,然后将此接口作为接受器连接到浏览器对象。
一种做法是
在派生类中,使用MFC建立接口方案提供一个DIID_DWebBrowserEvents2接口对象嵌套成员。
class CFMDBrowser : public CWebBrowser{...//事件接收器接口//DWebBrowserEvents//这是一个IDispatch分发接口BEGIN_INTERFACE_PART(BrowserEventSink,DWebBrowserEvents)STDMETHOD(GetTypeInfoCount)(UINT *pctinfo);STDMETHOD(GetTypeInfo)(UINT iTInfo,LCID lcid,ITypeInfo **ppTInfo);STDMETHOD(GetIDsOfNames)(REFIID riid,LPOLESTR *rgszNames,UINT cNames,LCID lcid,DISPID *rgDispId);STDMETHOD(Invoke)(DISPID dispIdMember,REFIID riid,LCID lcid,WORD wFlags,DISPPARAMS *pDispParams,VARIANT *pVarResult,EXCEPINFO *pExcepInfo,UINT *puArgErr);END_INTERFACE_PART(BrowserEventSink)DWORD m_dwEventSinkCookie;...}
这是一个接收器接口,无需添入到对象的接口表中。
(无需:BEGIN_INTERFACE_MAP、END_INTERFACE_MAP)
这是一个以分发接口(IDispatch)作为出接口的典型例子。在接口函数的实现中。Invoke负责又分发ID调用不同的虚拟函数。(事件函数作为虚拟函数,供派生类重载)
STDMETHODIMP CFMDBrowser::XBrowserEventSink::Invoke(DISPID dispIdMember,REFIID riid,LCID lcid,WORD wFlags,DISPPARAMS *pDispParams,VARIANT *pVarResult,EXCEPINFO *pExcepInfo,UINT *puArgErr){METHOD_PROLOGUE(CFMDBrowser,BrowserEventSink)//将事件分发到各虚拟函数//分发ID的定义见 exdispid.hswitch(dispIdMember){case DISPID_BEFORENAVIGATE:...HRESULT hr=pThis->OnBeforeNavigate(..) //事件对应的虚拟函数...break;case DISPID_NAVIGATECOMPLETE:...case ...case ...}
建立与浏览器的连接
得到IConnectionPointContainer接口,查找与DIID_DWebBrowserEvents对应的接收器,建立连接,记录连接的标号(m_dwEventSinkCookie);
BOOL CFMDBrowser::Connect(){IUnknown *p_Unk=GetControlUnknown();if(p_Unk==NULL)return FALSE;BOOL bOK=FALSE;//查找连接点对象IConnectionPointContainer *i_cpc=0;HRESULT hr=p_Unk->QueryInterface(IID_IConnectionPointContainer,(void **)(&i_cpc));if (SUCCEEDED(hr)){IConnectionPoint *i_cp=0;hr=i_cpc->FindConnectionPoint(DIID_DWebBrowserEvents,&i_cp);if (SUCCEEDED(hr)){hr=i_cp->Advise(&m_xBrowserEventSink,&m_dwEventSinkCookie);i_cp->Release();bOK=TRUE;}i_cpc->Release();}return bOK;}
结束时,断开与浏览器的连接
BOOL CFMDBrowser::DisConnect(){IUnknown *p_Unk=GetControlUnknown();if(p_Unk==NULL)return FALSE;BOOL bOK=FALSE;//查找连接点对象IConnectionPointContainer *i_cpc=0;HRESULT hr=p_Unk->QueryInterface(IID_IConnectionPointContainer,(void **)(&i_cpc));if (SUCCEEDED(hr)){IConnectionPoint *i_cp=0;hr=i_cpc->FindConnectionPoint(DIID_DWebBrowserEvents,&i_cp);if (SUCCEEDED(hr)){hr=i_cp->Unadvise(m_dwEventSinkCookie);i_cp->Release();bOK=TRUE;}i_cpc->Release();}return bOK;}
三、定制浏览器UI
浏览器提供了IDocHostUIHandler出接口,向用户查询界面特性
可以提供这个接口,与浏览器连接上,在其实现中,定制界面
1.建立接口
class CFMDBrowser : public CWebBrowser{...//IDocHostUIHandler接口,控制浏览器界面BEGIN_INTERFACE_PART(UIHandlerSink,IDocHostUIHandler)STDMETHOD(ShowContextMenu)(DWORD,POINT*,IUnknown*,IDispatch*);STDMETHOD(GetHostInfo)(DOCHOSTUIINFO*);STDMETHOD(ShowUI)(DWORD,IOleInPlaceActiveObject*,IOleCommandTarget*,IOleInPlaceFrame*,IOleInPlaceUIWindow*);STDMETHOD(HideUI)();STDMETHOD(UpdateUI)();STDMETHOD(EnableModeless)(INT);STDMETHOD(OnDocWindowActivate)(INT);STDMETHOD(OnFrameWindowActivate)(INT);STDMETHOD(ResizeBorder)(LPCRECT,IOleInPlaceUIWindow*,INT);STDMETHOD(TranslateAccelerator)(LPMSG,const GUID*,DWORD);STDMETHOD(GetOptionKeyPath)(LPOLESTR*,DWORD);STDMETHOD(GetDropTarget)(IDropTarget*,IDropTarget**);STDMETHOD(GetExternal)(IDispatch**);STDMETHOD(TranslateUrl)(DWORD,OLECHAR*,OLECHAR**);STDMETHOD(FilterDataObject)(IDataObject*,IDataObject**);END_INTERFACE_PART(UIHandlerSink)...}
无需添加接口映射
2.连接到浏览器
需要在NavigateComplete时间发生后,得到
ICustomDoc接口,由此接口的
SetUIHandler成员设置UI接口。
//设置界面接口IDispatch *i_dispatch=0;if (SUCCEEDED(i_dispatch=pThis->GetDocument())){IHTMLDocument2 *i_htmldoc2=0;if (SUCCEEDED(i_dispatch->QueryInterface(IID_IHTMLDocument2,(void **)(&i_htmldoc2)))){// force connection of IDocHostUIHandlerICustomDoc *i_customdoc=0;if (SUCCEEDED(i_htmldoc2->QueryInterface(IID_ICustomDoc,(void **)(&i_customdoc)))){i_customdoc->SetUIHandler(&(pThis->m_xUIHandlerSink));i_customdoc->Release();}}i_dispatch->Release();}
3.在接口的实现中,控制用户界面
例如更改右键菜单
在STDMETHOD(ShowContextMenu)(DWORD,POINT*,IUnknown*,IDispatch*);
的实现中:
HRESULT CFMDBrowser::ShowContextMenu(DWORD,POINT*,IUnknown*,IDispatch*){..建立自己的菜单return S_OK;}
采集网页选定部分全攻略
作者:龙仪
下载源代码
在 VCKBASE 混了这么久竟然没有写出一篇文章,想想很是惭愧,每当在这里看到一篇好文,这种感觉尤甚,总结我在程序员加油站中的一些技术点写了这个文章(虽然程序员加油站还要继续开发,但是由于时间关系不知道什么时候能完成),如果有时间我还会写一些文章的,我的写作水平可能很差,希望读者能够包涵。
程序原理:
一、在IE菜单中加入菜单项
在注册表HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt项下建立一个新项,项的名称即为出现在菜单中的标题
将新建项的默认值设定为一个URL地址,当用户点击菜单项后,IE就会调用URL指向的页面中的脚本。
二、如何控制菜单项在合适的时候显示
下面再介绍一下上面注册项中Contexts项的作用,通过该项可以制定菜单项在右键点击IE中的什么对象时出现,它可以为以下值的“或”组合
对象 | 值 |
缺省 | 0x1 |
图片 | 0x2 |
控件 | 0x4 |
表单域 | 0x8 |
选择文本 | 0x10 |
锚点 | 0x20 |
超链接 | 0x22 |
例如上面我们希望菜单项在用户点击图片或者超链接时出现,那么我们就将值设置为
dword:00000022
既在点击图片 或者 锚点时出现菜单。一个锚点是页面中描述一个超链接的对象。如果不设置Contexts 项,则菜单项会在点击任何对象时出现在右键菜单中。
注:
一二部分我引用了《如何在IE右键菜单中添加菜单项以及如何添加IE任务栏按钮》这篇文章的部分内容,详细内容请看:
http://www.csdn.net/develop/read_article.asp?id=3621
三、编辑点击菜单项执行的script脚本
这个脚本的文件名和1中的链接文件一致这个是我用的脚本:
<script language="VBScript">Sub OnContextMenu()set nc=CreateObject("Test2.testa.1")nc.GetHtmlText()end subCall OnContextMenu()</script>//看到网海拾贝中这样用<script language="VBScript">Sub OnContextMenu()NCWEBPAGE=1NCSELWEBPAGE=2NCSELTEXT=3NCALLTEXT=4NCIMAGE=5NCALLIMAGE=6NCALLLINK=7NCALLLINKTITLE=8NCSELSOURCECODE=9NCSOURCECODE=10On Error Resume Nextset nc=CreateObject("NcActive.NcCollect")if err<>0 thenMsgBox("网海拾贝没有正确安装")else//但是这个参数至今不知道如何在控件中得到call Nc.Gethtmldoc(NcSelText,external.menuArguments.document)end ifend subCall OnContextMenu()</script>四、在注册表中加入
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall
\\程序员加油站加入ArticlePath - 文档存盘路径(string),ArticleNumber -文档序号(dword)。
五、写一个ATL的DLL,就是上面脚本中调用的那个对象,提供一个接口GetHtmlText(),看到有些其他程序使用external.menuArguments.document做为参数,可是我没试验成功,无法直接获得其中的document所以只能用笨方法了,取得当前窗口,然后取得IE子窗口的句柄,然后取得document指针,取得选取的内容,然后保存网页,并下载图片。
下面就介绍一下ATL组件的制作,主要技术包括ATL编程,IE编程,注册表操作。
实现如下:
STDMETHODIMP Ctesta::GetHtmlText(){//保存网页内容的目录char chFilePath[MAX_PATH];DWORD Number = 0;CRegistry reg;reg.Open(HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\程序员加油站\\");BOOL ret = reg.ReadDWORD("ArticleNumber",&Number);if(!ret)return S_FALSE;//读取保存网页文件的目录ret = reg.ReadString("ArticlePath",chFilePath);if(!ret)return S_FALSE;//取得当前活动窗口的窗口句柄HWND hWnd = GetActiveWindow();CoInitialize( NULL );//显式装载 MSAA 判断是否被安装HINSTANCE hInst = ::LoadLibrary( _T("OLEACC.DLL") );if ( hInst != NULL ){if ( hWnd != NULL ){HWND hWndChild=NULL;// 取得当前窗口的IE子窗口指针::EnumChildWindows( hWnd, EnumChildProc, (LPARAM)&hWndChild );if ( hWndChild ){//定义IE文档CComPtr pHTMLDoc;LRESULT lRes;//由于WM_HTML_GETOBJECT非Windows标准消息,所以需要RegisterWindowMessageUINT nMsg = ::RegisterWindowMessage( _T("WM_HTML_GETOBJECT") );::SendMessageTimeout( hWndChild, nMsg, 0L, 0L, SMTO_ABORTIFHUNG, 1000, (DWORD*)&lRes );LPFNOBJECTFROMLRESULT pfObjectFromLresult = (LPFNOBJECTFROMLRESULT)::GetProcAddress( hInst, _T("ObjectFromLresult") );if ( pfObjectFromLresult != NULL ){HRESULT hr;//获取网页的IHTMLDocument2接口hr = (*pfObjectFromLresult)( lRes, IID_IHTMLDocument, 0, (void**)&pHTMLDoc );if ( SUCCEEDED(hr) ){CComPtr pSelObj;CComPtr pTxtRange;//根据IHTMLDocument2指针取得IHTMLSelectionObject接口指针pHTMLDoc->get_selection(&pSelObj);//再获得IHTMLTxtRange指针hr = pSelObj->createRange((IDispatch**)&pTxtRange);if(!CheckResult(hr,"pTxtRange"))return hr;//选择所有被选择的内容pTxtRange->select();BSTR bstrTxt,bstrTxt1;char strPath[MAX_PATH];char *strTxt = NULL;char*strTxt1 = NULL;//取得主站域名CComPtr pLocation;pHTMLDoc->get_location(&pLocation);pLocation->get_hostname(&bstrTxt1);strTxt1 = _com_util::ConvertBSTRToString(bstrTxt1);sprintf(strPath,"http://%s",strTxt1);SaveCharToFile(strTxt1,debugpath);//取得选中的内容pTxtRange->get_htmlText(&bstrTxt);strTxt = _com_util::ConvertBSTRToString(bstrTxt);//下载内容中的图片资源,并修改相应链接std::string webpage;char chSavePath[MAX_PATH];sprintf(chSavePath,"%s\\T%ld.htm",chFilePath,Number);CreateAllDirectory(chSavePath);//取得所有图片资源并保存网页if(CheckData(strTxt,strPath,chFilePath,Number,webpage) == FALSE)SaveCharToFile(strTxt,chSavePath,TRUE);elseSaveCharToFile(webpage.c_str(),chSavePath,TRUE);BOOL ret = reg.WriteDWORD("ArticleNumber",++Number);//释放内存if(strTxt1)delete[] strTxt1;if(strTxt)delete[] strTxt;SysFreeString(bstrTxt1); // 用完释放SysFreeString(bstrTxt); // 用完释放}}}} // else Internet Explorer is not running::FreeLibrary( hInst );} // else Active Accessibility is not installedCoUninitialize();return S_OK;}在取得选取网页内容之后,需要对HTML内容进行重新编辑以获得正确的显示,CheckData函数便是做这个工作的这个函数对HTML内容中图像信息的地址进行编辑,如果图像保存在本地则使用本地图像,如果图像没有下载保持原样。
BOOL CheckData(const LPCTSTR data,const LPCTSTR host,const LPCTSTR path,DWORD Number,std::string &outstring){char chImgPath[1024];//图像路径 !:由于有些图像路径可能会很长所以申请内存多一点char chImgSrc[MAX_PATH];//图像地址char chDownLoadPath[MAX_PATH];//下载图像文件路径char chWriteImgSrc[MAX_PATH];//图像文件路径memset(chImgPath,0,1024);memset(chImgSrc,0,MAX_PATH);memset(chDownLoadPath,0,MAX_PATH);memset(chWriteImgSrc,0,MAX_PATH);char dirname[20];//根目录名ltoa(Number,dirname,10);try{//查找是否有<img标记const char *p1 = FindString(data,"<img");const char *p2 = FindString(p1,"src=\"");const char *p3 = FindString(p2+5,"\"");const char *p4 = FindString(p1,">");if(p1 == NULL||p2 == NULL||p3 == NULL||p4 == NULL)return FALSE;//找到一个图像文件标记//拷贝图像链接之前的文字int n = p4-p1+1;strncpy(chImgPath,p1,n);outstring.append(data,p2-data+5);//提取图像链接n = p3-p2-5;strncpy(chImgSrc,p2+5,n);if(FindString(chImgSrc,"http://") == NULL){if(FindString(chImgSrc,".."))strcpy(chImgSrc,&chImgSrc[2]);sprintf(chDownLoadPath,"%s%s",host,chImgSrc);sprintf(chWriteImgSrc,"%s//%s%s",path,dirname,chImgSrc);}else{strcpy(chDownLoadPath,chImgSrc);const char *p5 = FindString(chImgSrc+7,"/");sprintf(chWriteImgSrc,"%s\\%s%s",path,dirname,&chImgSrc[p5-chImgSrc]);}char Output[MAX_PATH];sprintf(Output,"图像地址:%s\r\n存盘地址:%s\r\n主机地址:%s\r\n",chImgSrc,chWriteImgSrc,host);SaveCharToFile(Output,debugpath);n = strlen(chWriteImgSrc);for(int i=0;i<n;i++){if(chWriteImgSrc[i] == ''''/'''')chWriteImgSrc[i] = ''''\\'''';}//在下载之前先建立保存图像文件的路径CreateAllDirectory(chWriteImgSrc);//下载图像文件HRESULT hr = URLDownloadToFile( NULL, chDownLoadPath, chWriteImgSrc, 0, NULL);if( SUCCEEDED(hr))//{const char *p6 = FindString(chDownLoadPath+7,"/");sprintf(chWriteImgSrc,"%s%s",dirname,&chDownLoadPath[p6-chDownLoadPath]);//将存盘的路径保存进字符串}else{strcpy(chWriteImgSrc,chDownLoadPath);//没有下载成功将原始路径保存进}outstring.append(chWriteImgSrc);outstring.append(p3,p4-p3+1);BOOL ret = CheckData(p4+1,host,path,Number,outstring);if(!ret){outstring.append(p4+1);}}catch(...){return FALSE;}return TRUE;}
其他几个辅助函数这里简单介绍,详细内容请参看源代码
函数:void SaveCharToFile功能:将字符串保存为给定文件名const LPCTSTR data,//[IN] 给定待保存的数据const LPCTSTR saveFileName,//[IN]给定文件名BOOL flag = FALSE//[IN]追加写入还是覆盖标志函数:const char *FindString功能:在给定字符串中查找给定字符串const LPCTSTR source,//[IN] 给定源字符串的数据const LPCTSTR key)//[IN]待查字符串函数:void CreateAllDirectory功能:创建给定路径的所有未建目录const char* AllPath)//[IN] 需要创建的详细目录例如 输入为:"d:\a\b\c\d"则在D盘下创建相应a,b,c,d相应目录
好了,到此为止,我们已经做成一个取得网页选取内容的并保存在本地的程序,你可以举一反三,将它应用到更有趣更有用的领域。
这个程序是我研究了一周才写完的,其中取得domument的方法我觉得很笨,可我现在没有找到好办法,希望能够抛砖引玉,有更好的方法别忘了告诉我,我的Email:lgtest@sina.com欢迎来信讨论。
如何使用BHO定制你的Internet Explorer浏览器
原文出处:Browser Helper Objects: The Browser the Way You Want It
一、简介
有时,你可能需要一个定制版本的浏览器。在这种情况下,你可以自由地把一些新颖但又不标准的特征增加到一个浏览器上。结果,你最终有的只是一个新但不标准的浏览器。Web浏览器控件只是浏览器的分析引擎。这意味着仍然存在若干的与用户接口相关的工作等待你做――增加一个地址栏,工具栏,历史记录,状态栏,频道栏和收藏夹等。如此,要产生一个定制的浏览器,你可以进行两种类型的编程――一种象微软把Web浏览器控件转变成一个功能齐全的浏览器如Internet Explorer;一种是在现有的基础上加一些新的功能。如果有一个直接的方法定制现有的Internet Explorer该多好?BHO(Browser Helper Objects,我译为"浏览器帮助者对象",以下皆简称BHO)正是用来实现此目的的。
二、关于软件定制
以前,定制一个软件的行为主要是通过子类化方法实现的。 通过这种办法,你可以改变一个窗口的外表与行为。子类化虽然被认为是一种有点暴力方式――受害者根本不知道发生的事情――但它还是长时间以来的唯一的选择。
随着微软Win32 API的到来,进程间子类化不再被鼓励使用并愈发变得困难起来。当然,如果你是勇敢的--指针从未吓倒你,而最重要的是,如果你已经游刃于系统钩子之间,你可能觉得这一问题太简单了。 但是情形并不总是这样。暂放下这点不管,问题在于每一个进程运行在自己的地址空间中,而且打破进程边界略微有些不正确性。 另一方面, 你可能需要对定制进行更好的管理。更经常情况下,定制可能是程序本身强烈要求实现的。
在后者情况下,已安装的软件只需在既定的磁盘位置查询另外的组件模块,然后装载、设定初值,最后让它们自由地按照既定的设计工作。这正是Internet Explorer浏览器和它的BHO所要实现的。
三、什么是BHO?
从某种观点看,Internet Explorer同普通的Win32程序没有什么两样。借助于BHO,你可以写一个进程内COM对象,这个对象在每次启动时都要加载。这样的对象会在与浏览器相同的上下文中运行,并能对可用的窗口和模块执行任何行动。例如,一个BHO能够探测到典型的事件,如GoBack、GoForward、DocumentComplete等;另外BHO能够存取浏览器的菜单与工具栏并能做出修改,还能够产生新窗口来显示当前网页的一些额外信息,还能够安装钩子以监控一些消息和动作。简而言之, BHO的工作如我们打入浏览器领地的一位间谍(注意这是微软允许的合法工作)。
在进一步了解BHO细节之前,有几点我需要进一步阐述。首先,BHO对象依托于浏览器主窗口。实际上,这意味着一旦一个浏览器窗口产生,一个新的BHO对象实例就要生成。任何 BHO对象与浏览器实例的生命周期是一致的。其次, BHO仅存在于Internet Explorer 4.0及以后版本中。
如果你在使用Microsoft Windows? 98, Windows 2000, Windows 95, or Windows NT版本4.0 操作系统的话,也就一块运行了活动桌面外壳4.71,BHO也被 Windows资源管理器所支持。 BHO是一个COM进程内服务,注册于注册表中某一键下。在启动时,Internet Explorer查询那个键并把该键下的所有对象预以加载。
Internet Explorer浏览器初始化这一对象并要求某一接口功能。如果发现这一接口, Internet Explorer使用其提供的方法传递 IUnknown 指针到BHO对象。见图一:
图一 ie浏览器如何装入和初始化BHO对象,BHO场所(site)是用于实现通信的COM接口
浏览器可能在注册表中发现一系列的CLSID,并由此为每个CLSID建立一个进程中实例。结果是,这些对象被装载至浏览器上下文中并运行起来,好象它们是本地组件一样。但是,由于Internet Explorer的COM特性,即使被装入到它的进程空间中于事(你的野心实现)也不一定会有多大帮助。用另一说法, BHO的确能够做许多潜在的有用的事情,如子类化组成窗口或者安装线程局部钩子,但是它确实远离浏览器的核心活动。为了钩住浏览器的事件或者自动化浏览器,BHO需要建立一个私有的基于COM的通讯通道。为此,该BHO应该实现一个称为IObjectWithSite的接口。事实上,通过接口IobjectWithSite, Internet Explorer 可以传递它的IUnknown 接口。BHO反过来能够存储该接口并进一步查询更专门的接口,如IWebBrowser2、IDispatch和IConnectionPointContainer。
另外一种分析BHO对象的途径与Internet Explorer外壳扩展有关。我们知道,一个WINDOWS外壳扩展即是一个进程内的COM服务器,它在Windows资源管理器执行某种动作时装入内存――如显示上下文菜单。通过建立一个实现几个COM接口的COM模块,你就给上下文菜单加上一些项并能预以正确处理。一个外壳扩展必须以Windows资源管理器能够发现的方法注册。一个BHO对象遵循同样的模式――唯一的改变在于要实现的接口。然而,尽管实现方式有所不同,外壳扩展与 BHO 仍有许多共同的特点。如下表一:
表一 外壳扩展与 BHO相近特性比较
特性 | 外壳扩展 | BHO对象 |
加载者 | Windows资源管理器 | Internet Explorer(和外壳4.17及以上版本的Windows资源管理器) |
击活动作 | 在某类文档上的用户动作(即单击右键) | 打开浏览器窗口 |
何时卸载 | 参考计数达到0的几秒之后 | 导致它加载的窗口关闭时 |
实现形式 | COM进程中DLL | COM 进程中 DLL |
注册需求 | 常常是为一个COM服务器设置的入口处,另加的入口依赖于外壳类型及它要应用至的文档类型 | 常常是为一个COM服务器设置的入口处,另加一个把它申请为BHO的注册入口 |
接口需求 | 依赖于外壳扩展的类型 | IObjectWithSite |
如果你对SHELL扩展编程有兴趣的话,可以参考MSDN有关资料。
四、BHO的生存周期
前面已经说过,BHO不仅仅为Internet Explorer所支持。如果你在使用外壳 4.71或者更高版本,你的BHO对象也会被Windows资源管理器所加载。下表二展示了我们可以使用的不同版本的外壳产品情况,Windows外壳版本号存于库文件shell32.dll中。
表二 不同版本的Windows外壳对于BHO的支持情况
外壳版本 | 安装的产品 | BHO的支持情况 |
4.00 | Windows 95,Windows NT 4.0 带或不带 Internet Explorer 4.0 或更老版本。 注意没有安装外壳更新 | Internet Explorer 4.0 |
4.71 | Windows 95,Windows NT 4.0 带Internet Explorer 4.0 和活动桌面外壳更新 | Internet Explorer 与Windows 资源管理器 |
4.72 | Windows 98 | Internet Explorer与Windows 资源管理器 |
5.00 | Windows 2000 | Internet Explorer与Windows 资源管理器 |
BHO对象随着浏览器主窗口的显示而装入,随着浏览器主窗口的销毁而缷载。如果你打开多个浏览器窗口,多个BHO实例也一同产生。
无论浏览器以什么样的命令行启动,BHO对象都被加载。举例来说,即使你只是想要见到特定的 HTML 页或一个给定的文件夹,BHO对象也被加载。一般地,当 explorer.exe 或 iexplore.exe 运行的时候,BHO都要被考虑在内。如果你设置了"Open each folder in its own window"(对每一个文件夹以一个独立窗口打开)文件夹选项,那么你每次打开一个文件夹,BHO对象都要被加载。见图二。
图二 经过这样设置,你每次打开一个文件夹时,执行一个独立的explorer.exe实例,并装入已注册的BHO对象。
但是注意,这种情形仅适于当你从桌面上的"我的电脑"图标中打开文件夹的情况。在这种情况下,每次你移到另外一个文件夹时外壳都要调用explorer.exe。这种情况在你同时用两个窗格进行浏览时是不会发生的。事实上,当你改变文件夹时,外壳是不会启动浏览器的新的实例的而仅是简单创建嵌入视图对象的另外一个实例。奇怪的是,如果你在地址栏中输入一个新的名字来改变文件夹时,在同一个窗口中同样可以达到浏览之目的,无论Windows资源管理器视图是单个的还是双视图形式。
对于Internet Explorer的情形,事情要更简单一些。只有你显式地多次运行iexplore.exe浏览器时,你才有多个Internet Explorer的拷贝。当你从Internet Explorer中打开新的窗口时,每一个窗口在一个新的线程中被复制而不是创建一个新的进程,因此也就不需要重新载入BHO对象。
首先,BHO最有趣的地方是,它是极度动态的。每次Windows资源管理器或者Internet Explorer打开,装载器从注册表中读取已安装的BHO对象的CLSID然后处理它们。如果你在打开的浏览器多个实例中间编辑注册表的话,你可以随着多个浏览器拷贝的载入而装入多个不同的BHO。 这就是说,如果你选择从头创建一个新的属于自己的浏览器,那么你可以把它内嵌在一个Visual Basic或者MFC框架窗口中。同时你有相当的机会来灵活安排浏览程序。如果它们能满足你的需要的话,你可以依赖于Internet Explorer的强大的功能并且加上你想要的尽可能多的插件。
五、关于IObjectWithSite接口
从一个高起点来看,BHO即是一个DLL,它能够依附于Internet Explorer浏览器的一个新建的实例,在某些情况下也适用于Windows资源管理器。
一般地,一个场所(site)是一个中间对象,它位于容器对象与被包容对象之间。通过它,容器对象管理被包容对象的内容,也因此使得对象的内部功能可用。为此,容器方要实现接口IoleClientSite,被包容对象要实现接口IOleObject 。通过调用IOleObject提供的方法,容器对象使得被包容对象清楚地了解其HOST的环境。
一旦容器对象成为Internet Explorer(或是具有WEB能力的Windows资源管理器),被包容对象只需实现一个轻型的IObjectWithSite接口。该接口提供了以下方法:
表三 IObjectWithSite定义
方法 | 描述 |
HRESULT SetSite(IUnknown* pUnkSite) | 接收ie浏览器的IUnknown指针。典型实现是保存该指针以备将来使用。. |
HRESULT GetSite(REFIID riid, void** ppvSite) | 从通过SetSite()方法设置的场所中接收并返回指定的接口,典型实现是查询前面保存的接口指针以进一步取得指定的接口。 |
对BHO 的唯一严格的要求正在于必须实现这一个接口。 注意你应该避免在调用以上任何一个函数时返回E_NOTIMPL 。 要么你不实现这一接口,要么应保证在调用这些方法时进行正确地编码。
六、构造自己的BHO对象
一个BHO对象就是一个进程中服务器DLL,选用ATL创建它是再恰当不过的了。我们选择ATL的另外一个原因是因为它已经提供了缺省的而且提供了IObjectWithSite接口的足够好的实现。另外,在ATL COM 向导本地支持的已定义好的对象类型当中,有一个,就是Internet Explorer对象,这正是一个BHO应该具有的类型。一个 ATL Internet Explorer 对象,事实上是一个简单对象――也就是说,是一个支持IUnknown和自注册,还有接口IObjectWithSite的COM 服务器。如果你在ATL工程中添加一个这样的对象,并调用相应的类CViewSource,你将从向导中得到下列代码:
class ATL_NO_VTABLE CViewSource :public CComObjectRootEx<CComSingleThreadModel>,public CComCoClass<CViewSource, &CLSID_ViewSource>,public IObjectWithSiteImpl<CViewSource>,public IDispatchImpl<IViewSource, &IID_IViewSource,&LIBID_HTMLEDITLib>正如你所见,向导已经使类从接口IObjectWithSiteImpl继承,这是一个ATL模板类,它提供了接口IObjectWithSite的基本实现。一般情况下,没有必要重载成员函数GetSite()。取而代之的是, SetSite() 实现代码经常需要加以定制。ATL实际上仅仅把一个IUnknown接口指针存储在成员变量m_spUnkSite中。
在文章的剩余部分,我将讨论一个 BHO 的相当复杂而丰富的例子。该BHO对象将依附于Internet Explorer,并显示一个文本框来显示当前正浏览的网页源码。 该代码窗口将 随着你改变网页而自动更新,如果浏览器显示的不是一个HTML网页时,它将变灰。你对于原始HTML代码的任何改动立即反映在浏览器中。HTML (DHTML)使得这一看似魔术般的实现成为可能。该代码窗口可被隐藏和通过按动热键重现。 在可见情况下,它与Internet Explorer共享整个桌面空间,见图三。
![](https://i-blog.csdnimg.cn/blog_migrate/dd9890a999cd9e5582ad79ab7356fba0.png)
图三 BHO对象在使用中。它依附于Internet Explorer,并显示一个窗口来显示当前正浏览的网页源码。还允许你源码进行修改。
本例子的关键点在于存取Internet Explorer的浏览机制,其实它只不过是WebBrowser控件的一个实例而已。这个例子可以分解为以下五步来实现:
- 探测谁在装入这个对象,是Internet Explorer还是Windows资源管理器;
- 获取接口IWebBrowser2以实现Web浏览器对象;
- 捕捉Web浏览器的特定事件;
- 存取当前文档对象,确定它是一份HTML类型的文件;
- 管理对话框窗口以实现HTML源码的显示;
第一个步骤是在DllMain()中完成的。SetSite()是取得指向WebBrowser对象指针的适当位置。请详细分析以下步骤。
七、探测谁在调用这个对象
如前所述,一个BHO对象会被Internet Explorer或者Windows资源管理器(前提:外壳版本4.71或者更高)所加载。所以我专门设计了一个BHO来处理HTML网页,因此这个BHO与资源管理器毫无关系。如果一个Dll不想被调用者一起加载,只需在DllMain()中实现了探明谁在调用该对象后返回FALSE即可。参看下面代码:
if (dwReason == DLL_PROCESS_ATTACH){TCHAR pszLoader[MAX_PATH];//返回调用者模块的名称,第一个参数应为NULL,详见msdn。GetModuleFileName(NULL, pszLoader, MAX_PATH);_tcslwr(pszLoader);if (_tcsstr(pszLoader, _T("explorer.exe")))return FALSE;}一旦知道了当前进程是Windows资源管理器,可立即退出。
注意,再多加一些条件语句是危险的!事实上,另外一些进程试图装入该DLL时将被放弃。如果你做另外一个试验,比方说针对Internet Explorer的执行文件iexplorer.exe,这时第一个受害者就是regsvr32.exe(该程序用于自动注册对象)。
if (!_tcsstr(pszLoader, _T("iexplore.exe")))你不能够再次注册该DLL库了。 事实上,当 regsvr32.exe 试图装入DLL以激活函数DllRegisterServer()时,该调用将被放弃。
八、与Web浏览器取得联系
SetSite()方法正是BHO对象被初始化的地方,此外,在这个方法中你可以执行所有的仅仅允许发生一次的任务。当你用Internet Explorer打开一个URL时,你应该等待一系列的事件以确保要求的文档已完全下载并被初始化。唯有在此时,你才可以通过对象模型暴露的接口(如果存在的话)存取文档内容。这就是说你要取得一系列的指针。第一个就是指向IWebBrowser2(该接口用来生成WebBrowser对象)的指针。第二个指针与事件有关。该模块必须作为一个浏览器的事件侦听器来实现,目的是为接收下载以及与文档相关的事件。下面用ATL灵敏指针加以封装:
CComQIPtr< IWebBrowser2, &IID_IWebBrowser2> m_spWebBrowser2;CComQIPtr<IConnectionPointContainer,&IID_IConnectionPointContainer> m_spCPC;源代码部分如下所示:
HRESULT CViewSource::SetSite(IUnknown *pUnkSite){// 检索并存储 IWebBrowser2 指针m_spWebBrowser2 = pUnkSite;if (m_spWebBrowser2 == NULL)return E_INVALIDARG;//检索并存储 IConnectionPointerContainer指针m_spCPC = m_spWebBrowser2;if (m_spCPC == NULL)return E_POINTER;//检索并存储浏览器的句柄HWND. 并且安装一个键盘钩子备后用RetrieveBrowserWindow();// 为接受事件通知连接到容器return Connect();}为了取得IWebBrowser2接口指针,你可以进行查询。当然也可以在事件刚刚发生时查询IConnectionPointContainer。这里,SetSite()检索了浏览器的句柄HWND,并且在当前线程中安装了一个键盘钩子。HWND用于后面Internet Explorer窗口的移动或尺寸调整。这里的钩子用来实现热键功能,用户可以按动热键来显示/隐藏代码窗口。
九、从Internet Explorer浏览器取得事件
当你导向一个新的URL时,浏览器最需要完成的是两种事件:下载文档并为之准备HOST环境。也就是说,它必须初始化某对象并使该对象从外部可以利用。针对不同的文档类型,或者装入一个已注册的Microsoft ActiveX? 服务器来处理该文档(如Word对于.doc文件的处理)或者初始化一些内部组件来分析文档内容并生成和显示该文档。对于HTML网页就是这样,其内容由于DHTML对象作用而变得可用。当文档全部下载结束,DownloadComplete事件被激活。这并不是说,这样利用对象模型就可以安全地管理文档的内容了。事实上,DocumentComplete 事件仅指明一切已经结束,文档已准备好了 (注意DocumentComplete事件仅在你第一次存取URL时到达,如果你执行了刷新动作,你仅仅收到一个DocumentComplete事件)。
为了截获浏览器发出的事件, BHO需要通过IConnectionPoint 接口连接到浏览器上 并且实现传递接口IDispatch指针以处理各种事件。现在利用前面取得的IConnectionPointContainer指针来调用FindConnectionPoint方法――它返回一个指针指向连接点对象(正是通过这个连接点对象来取得要求的外向接口,此时是DIID_DWebBrowserEvent2)。 下列代码显示了连接点的发生情况:
HRESULT CViewSource::Connect(void){HRESULT hr;CComPtr<IConnectionPoint> spCP;//为Web浏览器事件而接收(receive)连接点hr = m_spCPC->FindConnectionPoint(DIID_DWebBrowserEvent2, &spCP);if (FAILED(hr))return hr;// 把事件处理器传递到容器。每次事件发生容器都将激活我们实现的IDispatch接口上的相应的函数。hr = spCP->Advise( reinterpret_cast<IDispatch*>(this), &m_dwCookie);return hr;}通过调用接口IConnectionPoint的Advise() 方法, BHO告诉浏览器它对它产生的事件很感兴趣。 由于COM事件处理机制,所有这些意味着BHO把IDispatch接口指针提供给浏览器。浏览器将回调IDispatch接口的Invoke() 方法,以事件的ID值作为第一参数:
HRESULT CViewSource::Invoke(DISPID dispidMember, REFIID riid,LCID lcid, WORD wFlags, DISPPARAMS* pDispParams,VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr){if (dispidMember == DISPID_DOCUMENTCOMPLETE) {OnDocumentComplete();m_bDocumentCompleted = true;}:}切记,当事件不再需要时,应该使之与浏览器分离。如果你忘记了做这件事情,BHO对象将被锁定,即使在你关闭浏览器窗口之后。很明显,实现分离的最佳时机是收到事件OnQuit时。
十、存取文档对象
此时,该BHO已经有一个参照指向Internet Explorer的Web浏览器控件并被连接到浏览器控件以接收所有它产生的事件。当网页被全部下载并正确初始化后,我们就可以通过DHTML文档模型存取它。Web浏览器的文档属性返回一个指向文档对象的IDispatch接口的指针:
CComPtr<IDispatch> pDisp;HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);get_Document() 方法取得的仅仅是一个接口指针。我们要进一步确定在IDispatch 指针背后存在一个HTML文档对象。用VB实现的话,可以用下面代码:
Dim doc As ObjectSet doc = WebBrowser1.DocumentIf TypeName(doc)="HTMLDocument" Then'' 获取文档内容并予以显示Else'' Disable the display dialogEnd If现在要了解一下get_Document()返回的IDispatch指针 。Internet Explorer不仅仅是一个HTML浏览器,而且还是一个ActiveX文档容器。 这样一来,难以保证当前浏览对象就是一个HTML文档。不过办法还是有的――你想,如果IDispatch指针真正指向一个HTML文档,查询IHTMLDocument2 接口一定成功。
IHTMLDocument2接口包装了DHTML对象模型用来展现HTML页面的所有功能。下面代码实现这些功能:
CComPtr<IDispatch> pDisp;HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> spHTML;spHTML = pDisp;if (spHTML) {// 获取文档内容并予以显示}else {// disable the Code Window controls}如果IHTMLDocument2接口查询失败,spHTML指针将是NULL。
现在考虑如何获得当前显示窗口的源代码。正如一个HTML页把它所有的内容封装在标签<BODY>中,DHTML对象模型要求你取得一个指向Body对象的指针:
CComPtr<IHTMLElement> m_pBody;hr = spHTML->get_body(&m_pBody);奇怪的是,DHTML对象模型不让你取得标签<BODY>之前的原始内容,如<HEAD>。其内容被处理并存于一些属性中,但你还是不能从HTML原始文件中提取这部分的RAW文本。这过,仅从BODY部分取得的内容足够了。为了取得包含在<BODY>…</BODY>间的HTML代码部分,可以把outerHTML属性内容读取到一个BSTR变量中:
BSTR bstrHTMLText;hr = m_pBody->get_outerHTML(&bstrHTMLText);在此基础上,在代码窗口中显示源码就是一种简单的事情了:生成一个窗口,进行字符的UNICODE至ANSI转化和设置编辑框控件的问题。下面代码实现这些功能:
HRESULT CViewSource::GetDocumentContent(){USES_CONVERSION;// 获取 WebBrowser的文档对象CComPtr<IDispatch> pDisp;HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);if (FAILED(hr))return hr;// 确保我们取得的是一个IHTMLDocument2接口指针//让我们查询一下 IHTMLDocument2 接口 (使用灵敏指针)CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> spHTML;spHTML = pDisp;// 抽取文档源代码if (spHTML){// 取得BODY 对象hr = spHTML->get_body(&m_pBody);if (FAILED(hr))return hr;// 取得HTML 文本BSTR bstrHTMLText;hr = m_pBody->get_outerHTML(&bstrHTMLText);if (FAILED(hr))return hr;// 进行文本的Unicode到 ANSI的转换LPTSTR psz = new TCHAR[SysStringLen(bstrHTMLText)];lstrcpy(psz, OLE2T(bstrHTMLText));// 文本进行相应的调整HWND hwnd = m_dlgCode.GetDlgItem(IDC_TEXT);EnableWindow(hwnd, true);hwnd = m_dlgCode.GetDlgItem(IDC_APPLY);EnableWindow(hwnd, true);// 设置代码窗口中的文本m_dlgCode.SetDlgItemText(IDC_TEXT, psz);delete [] psz;}else // 文档不是一个 HTML 页{m_dlgCode.SetDlgItemText(IDC_TEXT, "");HWND hwnd = m_dlgCode.GetDlgItem(IDC_TEXT);EnableWindow(hwnd, false);hwnd = m_dlgCode.GetDlgItem(IDC_APPLY);EnableWindow(hwnd, false);}return S_OK;}因为我要运行这段代码来响应DocumentComplete事件通知,每个新的页自动地而且敏捷地被处理。DHTML对象模型使你能够随意修改网页的结构,但这一变化在按F5刷新后全部复原。你还要处理一下DownloadComplete事件以刷新代码窗口 (注意, DownloadComplete 事件发生在 DocumentComplete事件之前)。你应该忽略网页的首次DownloadComplete事件,而是在执行刷新动作时才关注这一事件。布尔成员变量m_bDocumentCompleted正是用来区别这两种情形的。
十一、管理代码窗口
用来显示当前HTML页原始码的代码窗口涉及另外一个ATL 基本编程问题-对话框窗口,它位于ATL对象向导的"Miscellaneous"选项卡下。
我调整了代码窗口的大小来响应WM_INITDIALOG消息,使它占居桌面空间的下部区域,正好是在任务栏的上面。在浏览器启动时你可以选择显示或不显示这个窗口。缺省情况下是显示的,但这可以通过清除"Show window at startup"复选框项来实现。当然喜欢的话,你可以随时关闭。按键F12即可重新显示代码窗口。F12是通过在SetSite()中安装的键盘钩子实现的。启动环境存于WINDOWS注册表中,我选择外壳库文件shlwapi.dll中函数SHGetValue来实现注册表的读写操作。这同使用Reg开头的Win32函数操作相比,简单极了。请看:
DWORD dwType, dwVal;DWORD dwSize = sizeof(DWORD);SHGetValue(HKEY_CURRENT_USER, _T("Software\\MSDN\\BHO"), _T("ShowWindowAtStartup"), &dwType, &dwVal, &dwSize);这个DLL文件是同Internet Explorer 4.0 和活动桌面的诞生一起产生的,是WIN98及以后版本的标准组成,你可以放心使用。
十二、注册BHO对象
因为BHO 是一个COM 服务器,所以既应该作为COM 服务器注册又应该作为BHO对象注册。ATL向导自动生成.rgs文件,第一种情况的注册就免除了。下面的文件代码段是用来实现作为BHO对象注册的(CLSID为例中生成)。
HKLM {SOFTWARE {Microsoft {Windows {CurrentVersion {Explorer {''BHO'' {ForceRemove {1E1B2879-88FF-11D2-8D96-D7ACAC95951F}}}}}}}}注意ForceRemove一词能够实现在卸载对象时删除这一行相应的键值。BHO键下聚集了所有的BHO对象。对于这么多的一串家伙是从来不作缓冲调用的。这样以来,安装与测试BHO就是不费时的事情了。
十三、总结
本文描述了BHO对象,通过它你可以把自己的代码注入浏览器的地址空间中。你必须做的事情是写一个支持IObjectWithSite 接口的COM 服务器。在这一点上,你的BHO对象可以实现浏览器机制范围内的各种合法目的。本文所及示例涉及了COM事件,DHTML对象模型以及WEB浏览器编程接口。虽然内容稍宽一些,但它正显示了现实世界中的BHO对象的应用。如,你想知道浏览器在显示什么,那么您就需要了解接收事件并要熟悉WEB浏览器才行。
另外:Windows资源管理器也是与BHO对象交互的,这一点在编程时要特别注意。本文所附源程序为MSDN所带,在Windows2000/VC6下调试通过(编译通过后,重新启动IE即得到结果)。
ATL 实现定制的 IE 浏览器栏、工具栏和桌面工具栏
作者:杨老师
下载源代码
关键字:Band,Desk Band,Explorer Band,Tool Band,浏览器栏,工具栏,桌面工具栏
一、引言
最近,由于工作的要求,我需要在 IE 上做一些开发工作。于是在 MSDN 上翻阅了一些资料,根据 MSDN 上的说明我用 ATL 胜利完成了“资本家老板”分配的任务。
(并且在白天睡觉的过程中梦到了老板给我加工资啦......)
现在,我把 MSDN 上的原文资料,经过翻译整理并把一个 ATL 的实现奉贤给 VCKBASE 上的朋友们。
- 概念
- 原理
基本band 对象
必须实现的 COM 接口
IPersistStream
IObjectWithSite
IDeskBand、IDockingWindow、IOleWindow
选择实现的 COM 接口
Band 对象注册 - ATL 实现
二、概念
在翻译的过程中,有两个词汇非常不好理解。第一个词是 Band 对象,词典中翻译为“镶边、裙子边、带子、乐队......”我的英文水平有限,实在不知道应该翻译为什么词汇更合适。于是我毅然决然地决定:在如下的论述中,依然使用 band 这个词!(什么?没听明白?我的意思就是说,我不翻译这个词了)但到底 Band 对象应该如何理解那?请看图一:
图一
图一中画红圈的地方,分别称作“垂直的浏览器栏”、“水平的浏览器栏”、“工具栏”和“桌面工具栏”。这些“栏”,都可以在 IE 的“查看”菜单中或鼠标右键的上下文快捷方式菜单中显示或隐藏起来。这些界面窗口的实现,其实就是实现一种 COM 接口对象,而这个对象叫 band。这个概念实在是只能意会而无法言传的,我总不能在文章中把它翻译为“总是靠在 IE 主窗口边上的对象”吧?^_^
另外,还有一个词叫 site。这个很好翻译,叫“站点”!。呵呵,我敢打包票,如果你要能理解这个翻译在计算机类文章中的含义,那就只能恭喜你了,你的智慧太高了。(都是学计算机软件的人,做人的差距咋就这么大呢?)在本篇文章中,site 可以这样理解:IE 的主框架四周,就好比是“汽车站”,那些 band 对象,就好比是“汽车”。band 汽车总是可以停靠在“汽车站”上。所以,site 就是“站点”,它也是 COM 接口的对象(IObjectWithSite、IInputObjectSite)。
三、原理
3.1 基本 band 对象
Band 对象,从 Shell 4.71(IE 5.0) 开始提供支持。Band 是一个 COM 对象,必须放在一个容器中去使用,当然使用它们就好象使用普通窗口是一样的。IE 就是一个容器,桌面 Shell 也是一个容器,它们提供不同的函数功能,但基本的实现是相似的。
Band 对象分三种类型,浏览器栏 band(Explorer bands)、工具栏 band(Tool Bands)和桌面工具栏(Desk bands),而浏览器栏 band 又有两种表现形式:垂直和水平的。那么 IE 和 Shell 如何区分并加载这些 bands 对象呢?方法是:你要对不同的 band 对象,在注册表中注册不同的组件类型(CATID)。
Band 样式 | 组件类型 | CATID |
垂直的浏览器栏 | CATID_InfoBand | 00021493-0000-0000-C000-000000000046 |
水平的浏览器栏 | CATID_CommBand | 00021494-0000-0000-C000-000000000046 |
桌面的工具栏 | CATID_DeskBand | 00021492-0000-0000-C000-000000000046 |
IE 工具栏不使用组件类型注册,而是使用在注册进行 CLSID 的登记方式。详细情况见 3.3。
在例子程序中,实现了全部四个类型的 band 对象,垂直浏览器栏(CVerticalBar)显示了一个 HTML 文件,并且实现了对 IE 主窗口浏览网页的导航等功能;水平的浏览器栏(CHorizontalBar)是一个编辑窗,它同步显示当前网页的 BODY 源文件内容;IE 工具栏(CToolBar)最简单,只是添加了一个空的工具栏;桌面工具栏(CDeskBar)实现了一个单行编辑窗口,你可以在上面输入命令行或文件名称,回车后它会执行 Shell 的打开动作。
3.2 必须实现的 COM 接口
Band 对象是 IE 或 Shell 的进程内服务器,所以它被包装在 DLL 中。而作为 COM 对象,它必须要实现 IUnknown 和 IClassFactory 接口。(大家可以不同操心,因为我们用 ATL 写程序,这两个接口是不用我们自己写代码的。)另外,Band 对象还必须实现 IDeskBand、IObjectWithSite 和 IPersistStream 三个接口:
IPersistStream 是持续性接口的一种。当 IE 加载 band 对象的时候,它通过这个接口的 Load 方法传递属性值给对象,让其进行初始化;而当卸载前,IE 则调用这个接口的 Save 方法保存对象的属性。用 ATL 实现这个接口很简单:
class ATL_NO_VTABLE Cxxx :......public IPersistStreamInitImpl, // 添加继承......{public:BOOL m_bRequiresSave; // IPersistStreamInitImpl 所必须的变量......BEGIN_COM_MAP(CVerticalBar)......COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)COM_INTERFACE_ENTRY2(IPersistStream, IPersistStreamInit)COM_INTERFACE_ENTRY(IPersistStreamInit)......END_COM_MAP()BEGIN_PROP_MAP(Cxxx)...... // 添加需要持续性的属性END_PROP_MAP()上面的代码,其实实现的是 IPersistStreamInit 接口,不过没有关系,因为 IPersistStreamInit 派生自 IPersistStream,实例化了派生类,自然就实例化了基类。在例子程序中,我只在桌面工具栏对象中添加了持续性属性,用来保存和初始化“命令行”。另外 COM_INTERFACE_ENTRY2(A,B)表示的含义是:如果想查询A接口的指针,则提供B接口指针来代替。为什么可以这样那?因为B接口派生自A接口,那么B接口的前几个函数必然就是A接口的函数了,自然B接口的地址其实和A接口的地址是一样的了。
IObjectWithSite 是 IE 用来对插件进行管理和通讯用的一个接口。必须要实现这个接口的2个函数:SetSite() 和 GetSite()。当 IE 加载 band 对象和释放 band 对象的时候,都要调用 SetSite()函数,那么在这个函数里正好是写初始化和释放操作代码的地方:
STDMETHODIMP Cxxx::SetSite(IUnknown *pUnkSite){if( NULL == pUnkSite ) // 释放 band 的时候{// 如果加载的时候,保存了一些接口// 那么现在:释放它}else // 加载 band 的时候{m_hwndParent = NULL; // 装载 band 的父窗口(就是带有标题的那个框架窗口)// 这个窗口的句柄,是调用 IUnknown::QueryInterface() 得到 IOleWindow// 然后调用 IOleWindow::GetWindow() 而获得的。CComQIPtr< IOleWindow, &IID_IOleWindow > spOleWindow(pUnkSite);if( spOleWindow ) spOleWindow->GetWindow(&m_hwndParent);if( !m_hwndParent ) return E_FAIL;// 现在,正好是建立子窗口的时机。// 注意,子窗口建立的时候,不要使用 WS_VISIBLE 属性... ...// 在例子程序中,用 CAxWindow 实现了一个能包容ActiveX的容器窗口(垂直浏览器栏)// 在例子程序中,用 WIN API 函数 CreateWindow 实现了标准窗口(水平浏览器栏、工具栏)// 在例子程序中,用 CWindowImpl 实现了一个包容窗口(桌面工具栏)/*********************************************************/以下部分,根据 band 对象特有的功能,是可以选择实现的**********************************************************/// 如果子窗口实现了用户输入,那么必须实现 IInputObject 接口,// 而该接口是被 IE 的 IInputObjectSite 调用的,因此在你的对象// 中,应该保存 IInputObjectSite 的接口指针。// 在类的头文件中,定义:// CComQIPtr< IInputObjectSite, &IID_IInputObjectSite > m_spSite;m_spSite = pUnkSite; // 保存 IInputObjectSite 指针if( !m_spSite ) return E_FAIL;// 你需要控制 IE 的主框架吗?// 那么在类的头文件中,定义:// CComQIPtr< IWebBrowser2, &IID_IWebBrowser2 > m_spFrameWB;// 然后,先取得 IServiceProvider,再取得 IWebBrowser2CComQIPtr < IServiceProvider, &IID_IServiceProvider> spSP(pUnkSite);if( !spSP ) return E_FAIL;spSP->QueryService( SID_SWebBrowserApp, &m_spFrameWB );if( !m_spFrameWB) return E_FAIL;// 如果你取得了 IE 主框架的 IWebBrowser2 指针// 那么,当它发生了什么事情,你难道不想知道吗?// 定义:CComPtr m_spCP;CComQIPtr< IConnectionPointContainer,&IID_IConnectionPointContainer> spCPC( m_spFrameWB );if( spCPC ){spCPC->FindConnectionPoint( DIID_DWebBrowserEvents2, &m_spCP );if( m_spCP ){m_spCP->Advise( reinterpret_cast< IDispatch * >( this ), &m_dwCookie );}}// 咳~~~ 不说了,看源码去吧。这里能干的事情太多了... ...}return S_OK;}IDeskBand 是一个特殊的 band 对象接口,有一个方法函数:GetBarInfo();
IDockingWindow 是 IDeskBank 的基类,有3个方法函数:ShowDW()、CloseDW()、ResizeBorderDW();
IOleWindow 又是 IDockingWindow 的基类,有2个方法函数:GetWindow()、ContextSensitiveHelp();
首先声明 IDeskBand ,然后要实现 IDeskBand 接口的共6个函数,这些函数比较简单,不同类型的 band 对象,其实现方法也都基本一致:
class ATL_NO_VTABLE Cxxx :......public IDeskBand,......{......BEGIN_COM_MAP(Cxxx)......COM_INTERFACE_ENTRY_IID(IID_IDeskBand, IDeskBand)......END_COM_MAP()// IOleWindowSTDMETHODIMP Cxxx::GetWindow(HWND * phwnd){ // 取得 band 对象的窗口句柄// m_hWnd 是建立窗口时候保存的*phwnd = m_hWnd;return S_OK;}STDMETHODIMP Cxxx::ContextSensitiveHelp(BOOL fEnterMode){ // 上下文帮助,参考 IContextMenu 接口return E_NOTIMPL;}// IDockingWindowSTDMETHODIMP CVerticalBar::ShowDW(BOOL bShow){ // 显示或隐藏 band 窗口if( m_hWnd )::ShowWindow( m_hWnd, bShow ? SW_SHOW : SW_HIDE);return S_OK;}STDMETHODIMP CVerticalBar::CloseDW(DWORD dwReserved){ // 销毁 band 窗口if( ::IsWindow( m_hWnd ) )::DestroyWindow( m_hWnd );m_hWnd = NULL;return S_OK;}STDMETHODIMP CVerticalBar::ResizeBorderDW(LPCRECT prcBorder, IUnknown* punkToolbarSite, BOOL fReserved){ // 当框架窗口的边框大小改变时return E_NOTIMPL;}// IDeskBandSTDMETHODIMP CVerticalBar::GetBandInfo(DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO* pdbi){// 取得 band 的基本信息,你需要填写 pdbi 参数作为返回if( NULL == pdbi ) return E_INVALIDARG;// 如果将来需要调用 IOleCommandTarget::Exec() 则需要保存这2个参数m_dwBandID = dwBandID;m_dwViewMode = dwViewMode;if(pdbi->dwMask & DBIM_MINSIZE){ // 最小尺寸pdbi->ptMinSize.x = 10;pdbi->ptMinSize.y = 10;}if(pdbi->dwMask & DBIM_MAXSIZE){ // 最大尺寸 (-1 表示 4G)pdbi->ptMaxSize.x = -1;pdbi->ptMaxSize.y = -1;}if(pdbi->dwMask & DBIM_INTEGRAL){pdbi->ptIntegral.x = 1;pdbi->ptIntegral.y = 1;}if(pdbi->dwMask & DBIM_ACTUAL){pdbi->ptActual.x = 0;pdbi->ptActual.y = 0;}if(pdbi->dwMask & DBIM_TITLE){ // 窗口标题wcscpy(pdbi->wszTitle,L"窗口标题");}if(pdbi->dwMask & DBIM_MODEFLAGS){pdbi->dwModeFlags = DBIMF_VARIABLEHEIGHT;}if(pdbi->dwMask & DBIM_BKCOLOR){ // 如果使用默认的背景色,则移除该标志pdbi->dwMask &= ~DBIM_BKCOLOR;}return S_OK;}3.3 选择实现的 COM 接口
有两个接口不是必须实现的,但也许很有用:IInputObject 和 IContextMenu。如果 band 对象需要接收用户的输入,那么必须实现 IInputObject 接口。IE 实现了 IInputObjectSite 接口,当容器中有多个输入窗口时,它调用 IInputObject 接口方法去负责管理用户的输入焦点。
在浏览器栏中需要实现3个函数:UIActivateIO()、HasFocusIO()、TranslateAcceleratorIO()。
当浏览器栏激活或失去活性的时候,IE 调用 UIActivateIO 函数,当激活的时候,浏览器栏一般调用 SetFocus 去设置它自己窗口的焦点。当 IE 需要判断哪个窗口有焦点的时候,它调用 HasFocusIO 。当浏览器栏的窗口或其子窗口有输入焦点时,则应返回 S_OK,否则返回 S_FALSE。TranslateAcceleratorIO 允许对象处理加速键,例子程序中没有实现,所以直接返回 S_FALSE。
STDMETHODIMP CExplorerBar::UIActivateIO(BOOL fActivate, LPMSG pMsg){if(fActivate)SetFocus(m_hWnd);return S_OK;}STDMETHODIMP CExplorerBar::HasFocusIO(void){if(m_bFocus)return S_OK;return S_FALSE;}STDMETHODIMP CExplorerBar::TranslateAcceleratorIO(LPMSG pMsg){return S_FALSE;}Band 对象能够通过包容器的 IOleCommandTarget::Exec() 调用执行命令。而 IOleCommandTarget 接口指针,则可以通过调用包容器的 IInputOjbectSite::QueryInterface(IID_IOleCommandTarget,...) 函数得到。CGID_DeskBand 是命令组,当一个 band 对象的 GetBandInfo 被调用的时候,包容器通过 dwBandID 参数指定一个 ID 给 band 对象,对象要保存住这个ID,以便调用 IOleCommandTarget::Exec()的时候使用。ID 的命令有:
- DBID_BANDINFOCHANGED
Band 的信息变化。设置参数 pvaIn 为 band ID, 该 ID 就是最近一次调用 GetBandInfo 所得到的值,容器会调用 band 对象的 GetBandInfo 函数来更新请求信息。 - DBID_MAXIMIZEBAND
最大化 band。设置参数 pvaIn 为 band ID,该 ID 就是最近一次调用 ?GetBandInfo ?所得到的值。 - DBID_SHOWONLY
打开或关闭容器中其它的 bands。 设置参数 pvaIn 为VT_UNKNOWN 类型,它可以是如下的值:
值 描述 pUnk band 对象的 IUnknown 指针,其它的桌面 bands 将被隐藏 0 隐藏所有的桌面 bands 1 显示所有的桌面 bands - DBID_PUSHCHEVRON
在菜单项左边显示“v”的选择标志。容器发送一个 RB_PUSHCHEVRON 消息,当 band 对象接收到通知消息 RBN_CHEVRONPUSHED 提示它显示一个"v"的标志。设置 IOleCommandTarget::Exec 函数中 nCmdExecOpt 参数为 band ID,该 ID 是最近一次调用 GetBandInfo ?所得到的值,设置 IOleCommandTarget::Exec 函数中 pvaIn 参数为 VT_I4 类型,这是应用程序定义的一个值,它通过通知消息 RBN_CHEVRONPUSHED 中lAppValue 回传给 band 对象。
3.4 Band 对象注册
Band 对象必须注册为一个 OLE 进程内的服务器,并且支持 apartment 线程公寓。注册表中默认键的值是表示菜单的文字。对于浏览器栏,它加到 IE 菜单的“查看\浏览器栏”中;对于工具栏 band ,它加到 IE 菜单的“查看\工具栏”中;对于桌面 band, 它加到系统任务栏的快捷菜单中。在菜单资源中,可以使用“&”指明加速键。
通常,一个基本的 band 对象的注册表项目是:
HKEY_CLASSES_ROOT
CLSID
{你的 band 对象的 CLSID}
(Default) = 菜单的文字
InProcServer32
(Default) = DLL 的全路径文件名
ThreadingModel= Apartment
工具栏 bands 还必须把它们的 CLSID 注册到 IE 的注册表中。
在 HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer\Toolbar 下给出 CLSID 作为键名,而其键值是被忽略的。
HKEY_LOCAL_MACHINE
Software
Microsoft
Internet Explorer
Toolbar
{你的 band 对象的 CLSID}
还有几个可选的注册表项目(例子程序并不是这样实现的)。比如,你想让浏览器栏显示 HTML 的话,必须要如下设置注册表:
HKEY_CLASSES_ROOT
CLSID
{你的 Band 对象的 CLSID}
Instance
CLSID
(Default) = {4D5C8C2A-D075-11D0-B416-00C04FB90376}
同时,如果要指定一个本地的 HTML 文件,那么要如下设置:
HKEY_CLASSES_ROOT
CLSID
{你的 Band 对象的 CLSID}
Instance
InitPropertyBag
Url
另外,还可以指定浏览器栏的宽和高,当然,它是依赖于这个栏是纵向还是横向的。其实这个项目无所谓,因为当用户调整了浏览器栏的大小后,会自动保存在注册表中的。
HKEY_CURRENT_USER
Software
Microsoft
Internet Explorer
Explorer Bars
{你的 Band 对象的 CLSID}
BarSize
BarSize 键的类型必须是 REG_BINARY 类型,它有8个字节。左起前4个字节,是用16进制表示的像素宽度或高度,后4个字节保留,你应该设置为0。下面是一个可以在浏览器栏上显示 HTML 文件的全部注册表项目的例子,默认宽度为291(0x123)个像素点:
HKEY_CLASSES_ROOT
CLSID
{你的 Band 对象的 CLSID}
(Default) = 菜单文字
InProcServer32
(Default) = DLL 的全路径文件名
ThreadingModel= Apartment
Instance
CLSID
(Default) = {4D5C8C2A-D075-11D0-B416-00C04FB90376}
InitPropertyBag
Url= 你的 HTML 文件名
HKEY_CURRENT_USER
Software
Microsoft
Internet Explorer
Explorer Bars
{你的 Band 对象的 CLSID}
BarSize= 23 01 00 00 00 00 00 00
对于注册表的设置,用 ATL 实现其实是异常简单的。打开工程的 xxx.rgs 文件,并手工编辑一下就可以了。 下面这个文件源码,是例子程序中 IE 工具栏的注册表样式,HKLM 是需要手工添加的,因为它不使用组件类型方式注册。而对于其它类型的 band 对象只要在类声明中添加:
BEGIN_CATEGORY_MAP(Cxxx) // 向注册表中注册 COM 类型IMPLEMENTED_CATEGORY(CATID_InfoBand) // 垂直样式的浏览器栏END_CATEGORY_MAP()IE 工具栏类型 band 对象的“.rgs”文件
HKCR // 这个项目是 ATL 帮你生成的,你只要手工修改“菜单上的文字”就可以了{Bands.ToolBar.1 = s ''ToolBar Class''{CLSID = s ''{ 你的 CLSID }''}Bands.ToolBar = s ''ToolBar Class''{CLSID = s ''{ 你的 CLSID }''CurVer = s ''Bands.ToolBar.1''}NoRemove CLSID{ForceRemove { 你的 CLSID } = s ''用在菜单上的文字(&T)''{ProgID = s ''Bands.ToolBar.1''VersionIndependentProgID = s ''Bands.ToolBar''ForceRemove ''Programmable''InprocServer32 = s ''%MODULE%''{val ThreadingModel = s ''Apartment''}''TypeLib'' = s ''{xxxx-xxxx-xxxxxxxxxxxxxxx}''}}}HKLM // 这个项目是手工添加的IE工具栏所特有的{Software{Microsoft{''Internet Explorer''{NoRemove Toolbar{ForceRemove val { 你的 CLSID } = s ''随便给个说明性文字串''}}}}}四、 ATL 实现
下载代码后(VC 6.0 工程),请参照前面的说明仔细阅读,代码中也有一些关键点的注释。如果想运行,则可以用 regsvr32.exe 进行注册,然后打开 IE 浏览器或资源浏览器就可以看到效果了。如果想自己实践一下,可以按照如下的步骤构造工程:
4.1 建立一个 ATL DLL 工程
4.2 添加 New ATL Object...,选择 Internet Explorer Object,选这个类型的目的是让向导给我们添加 IObjectWithSite 的支持。如果你使用的是 .net 环境,则不要忘记选择支持这个接口。
![](https://i-blog.csdnimg.cn/blog_migrate/994e3a5e67d47091545f2f47d6fe61e1.png)
4.3 输入对象名称,比如我想建立一个垂直的浏览器栏,不妨叫它 VerBar
![](https://i-blog.csdnimg.cn/blog_migrate/98b41795dac0c85c0b721f4a6262327b.png)
4.4 线程模型必须选择 Apartment,接口类型的选择无所谓,看你想不想支持 IDispatch 接口功能了。在例子程序中的垂直浏览器栏中,由于想更简单的操纵 IE 和从 IE 中接受事件(连接点),选择 Dual 是必要的。聚合选项,你只要别选择 Only 就可以了。
![](https://i-blog.csdnimg.cn/blog_migrate/9b05743ed532e4adc7e0a30418e4843c.png)
4.5 展现你无穷的智慧,开始输入程序吧。如果是 Debug 方式编译,可能会出现一个连接错误,报告找不到_AtlAxCreateControl,那么你要在菜单 Project\Settings...\Link 中增加对 Atl.lib 的连接。或者使用 #pragma comment ( lib, "atl" )加入连接库。
4.6 如果想调试代码,在菜单 Project\Settings...\Debug 中输入 IE 的路径名称,比如:“C:\Program Files\Internet Explorer\IEXPLORE.EXE”,然后就可以跟踪断点调试了。 编译和调试桌面工具栏的 band 对象,是非常麻烦的,因为计算机启动时自动运行 Shell,而 Shell 就会加载活动的桌面对象。
五、结束语
好了,到这里,就到这里了。祝大家学习快乐^_^
IE 控件一些高级使用方法
作者:杨老师
下载源代码
本文介绍如下内容
1、如何显示内存中的 HTML 网页;
2、如何屏蔽掉鼠标右键的上下文菜单;
3、如何扩展 HTML 中的脚本(external)对象;
4、如何显示 HTML 样式的对话窗;
5、如何执行 HTML 脚本;
一、如何显示内存中的 HTML 网页
或者因为网页保密的考虑;或者因为软件分发的考虑,有的时候就需要让 IE 或 IE 浏览器控件显示内存或资源中的 HTML 网页。在 MFC 中,CHtmlView::LoadFromResource() 可以显示程序资源中的 HTML 内容。我们都知道MFC的 CHtmlView 其实是对 IWebBrowser2 的一个包装,但是在 IWebBrowser2 中却没有类似的方法。那么它是如何实现的那?步骤如下:
1、首先通过 IWebBrowser2::Navigate2() 显示一个网页,其目的是产生有效的对象,从而得到 IHTMLDocument2 接口;
2、IWebBrowser2::get_Document() 得到 IHTMLDocument2 接口指针;
3、IHTMLDocument2::QueryInterface() 得到 IPersistStreamInit 接口指针;
4、IPersistStreamInit::InitNew() 初始化接口对象;
5、IPersistStreamInit::Load() 装载内存中的 HTML 数据流(IStream *);
内存指针转换为流的方法是:
I、 GlobalAlloc() 申请内存;
II、 复制 HTML 字符串内容到上述的内存中;
III、 CreateStreamFromHGlobal() 转换内存为 IStream 指针;
原理性代码如下:
// 显示一个空白网页m_ie.Navigate2( &CComVariant(_T("about:blank")),NULL,NULL,NULL,NULL);// 得到 IHTMLDocument2 指针CComPtr< IDispatch > spDoc(m_ie.GetDocument());// 得到 IPersistStreamInit 指针CComQIPtr< IPersistStreamInit, &IID_IPersistStreamInit > spPSI( spDoc );// 申请内存,复制 HTML 字符串LPTSTR lpMem = (LPTSTR)::GlobalAlloc( GPTR, ::lstrlen( lpHtml )+1 );lstrcpy( lpMem, " xxx xxx" ); // 转换内存为流对象指针 CComPtr< IStream > spStream; CreateStreamOnHGlobal( lpMem, TRUE, &spStream ); // 初始化后,装载显示 spPSI->InitNew(); spPSI->Load( spStream );
图一、IE控件显示内存中的 HTML 文件 图二、HTML对话窗
IE 所能支持的数据传输协议,除了大家所熟悉的 http、ftp、file......还有一个协议是 res ,它表示浏览显示文件中的 HTML 资源。你可以在 IE 的地址栏上直接输入这样格式的 URL:"res://文件名/资源名"。
把 HTML 文件加入到程序资源的方法比较简单,在资源卡片中,鼠标右键弹出菜单,执行 Import...(引入),选择指定的 HTML 文件,然后给一个资源名称即可。(在这里,最方便的资源名称用字符串比较好,如果使用整数ID,那么将来在使用的时候是这样的格式:res://文件名/#101,这里假设 101 是资源的ID号。真麻烦!我不太喜欢这样的方式。)对于图片文件等其它的附件,则需要手工编辑资源 RC 文件(用 IDE 环境引入,它会试图用文本方式打开一个2进制文件,多数情况下会“死机”)。下图是事例程序引入资源后的样式:
图三、HTML 资源的引入
手工编辑 RC 文件的部分是:
...... / // // HTML // HTML_TOWORD HTML DISCARDABLE "res\\ToWord.htm" // 这两个是HTML文件,可以引入 HTML_DLG HTML DISCARDABLE "res\\html_dlg.htm" ~SEND_R1_C1.GIF HTML DISCARDABLE "res\\~Send_r1_c1.gif" // 下面的是GIF文件,需要手工加入 ~SEND_R1_C2.GIF HTML DISCARDABLE "res\\~Send_r1_c2.gif" LOGO.GIF HTML DISCARDABLE "res\\Logo.gif" SEND_R1_C1.GIF HTML DISCARDABLE "res\\Send_r1_c1.gif" SEND_R1_C2.GIF HTML DISCARDABLE "res\\Send_r1_c2.gif" SPACER.GIF HTML DISCARDABLE "res\\spacer.gif" #endif // Chinese (P.R.C.) resources / ......
二、屏蔽 IE 控件的上下文菜单
屏蔽或自定义 IE 控件的上下文菜单,其实就是需要实现 IDocHostUIHandler 接口中的 ShowContextMenu 方法。如果使用 ATL 编写程序,我认为实现是比较简单的(也许是我使用 ATL 写 COM 比 MFC 熟悉一些的因素吧)。事例程序由于用 MFC 书写,真是搞的我头晕眼花,翻箱倒柜终于找到了微软书写的演示代码,于是我就直接复制过来使用了。(换句话说,读者在阅读这部分代码的时候,如果有问题可不要问我。你直接打电话去咨询 Microsoft 哈。)
三、扩展 HTML 脚本中的 external 对象
从 CCmdTarget 派生一个自动化对象(新建C++类的时候,注意别忘了选择 Automation)。在这个类里,你可以使用 ClassWizard 的 Automation 卡片,添加自定义的方法和属性。而在 HTML 的脚本程序中,就可以使用 window.external 进行调用了。用这个方法,实现了对 HTML 脚本功能的扩充。在 HTML 脚本和自动化对象之间要建立起关系,则需要实现 IDocHostUIHandler::GetExternal() 方法。
四、显示 HTML 样式的对话窗
这节内容是本文的重点。
用户的界面设计经历了若干个发展阶段。最早的程序设计,可以说没有用户界面;然后发展出一些简单的与用户交互的界面(控制台界面,全屏文本界面);再然后发展出了图形界面。其实我们现在的商业程序设计中,界面的处理代码占用了很大的篇幅。为了使界面的处理变得简单、通用、易修改维护,人们制作了很多的界面程序库。说实在话,大多数的界面程序库由于封装的不好,一是不灵活,二是经常需要修改它内部的 BUG,重用的效果并不理想。通用的换肤软件也只能实现对标准的窗口类进行皮肤美化,对自定义的窗口类还是需要自己写钩子。咳......
现在,我们已经有一个非常好的方法进行界面设计了,那就是使用 HTML(使用 Visual Studio.net 的程序员,一定对 .net 的界面很喜欢吧?!.net 开发环境,无处不在使用 HTML 方式的界面)。即使是一个完全地道的本地软件(非B/S软件),也可以使用本地 ASP 方式,HTA 方式进行程序设计。软件用户非常喜欢这样的程序,因为他早就熟悉并掌握了浏览器的操作,另外,对于程序员来说,也非常喜欢这种方式,因为不会再为换肤,不同用户不同的界面特化而伤脑筋了。
微软将要在下一代的程序设计中使用 XML 来描述用户界面。这种设计方式将会解放你、我这样的程序员,把咱们的工作量全部都转化到美工师那里去了:) 借 vckbase 的平台,现在呼吁大家,尽快学习和掌握 HTML、XML 的设计和脚本编程,并能熟练地对它们与 C++ 对象的交互进行编程。可以预计在未来的两三年内,拥有这样水平的程序员,一定会开始吃香饽饽了,嘿嘿......
下面,就如何显示一个 HTML 对话窗,开始我们未来软件方式的编程吧。
我们要调用 MSHTML.DLL 中的一个函数 ShowHTMLDialog(Ex) 来完成 HTML 对话窗的显示和数据交互。这个函数的声明是:
HRESULT ShowHTMLDialogEx( HWND hwndParent, IMoniker *pMk, DWORD dwDialogFlags, VARIANT *pvarArgIn, WCHAR *pchOptions, VARIANT *pvarArgOut );
hwndParent | 对话窗的父窗口句柄 | 这个太简单了,不多罗嗦。 |
pMk | URL的命名接口指针 | 表示在对话窗中显示哪个URL的页面。但它不是以简单的URL字符串方式提供的。它使用了moniker(命名)接口指针。 根据URL得到IMoniker *很简单,调用CreateURLMoniker()。唯一要注意的是,这个函数需要连接 Urlmon.lib库。 |
dwDialogFlags | 对话窗类型 | 可以组合 HTMLDLG_NOUI、HTMLDLG_MODAL、HTMLDLG_MODELESS、HTMLDLG_PRINT_TEMPLATE、HTMLDLG_VERIFY。 示例程序使用的是模式对话窗。HTMLDLG_NOUI 在下一节中介绍。 |
pvarArgIn | 对话窗的输入参数 | 一个传入对话窗的VARIANT变量,对话窗脚本中使用 window.dialogArguments 可以取得。 |
pchOptions | 对话窗样式 | 用字符串表示的对话窗样式。参考 IHTMLWindow2::showModalDialog()函数。 比如:"dialogHeight:100px dialogWidth:200px"表示200点宽,100点高。如果你不想在程序中指定,也可以在HTML中<html style=....>描述。 |
pvarArgOut | 对话窗输出参数 | 对话窗的VARIANT返回值,对话窗脚本中使用 window.returnValue 可以赋值。 |
五、执行 HTML 脚本
关于调用脚本的方法,我在 vckbase 发表了好几篇文章(鬼知道我为什么对脚本这么有兴趣)。ShowHTMLDialogEx()函数中,如果类型参数给出 HTMLDLG_NOUI,则表示并不真正显示一个对话窗,而是加载指定的 HTML 并执行其中的脚本。示例程序的该脚本中,执行一连串的动作,完成了把上一个 HTML 对话窗中用户输入的文本,发送到 MS WORD 中去。以此上下串联起来,演示了本文章中所讨论的所有功能。下面我把脚本和注释给朋友们展现一下:
On Error Resume Next Set wordapp=CreateObject("Word.Application") ''''运行 MS WORD if err<>0 then MsgBox("没有安装 MS WORD") else wordapp.visible = true ''''显示WORD界面 wordapp.Documents.Add "",false, 0 ''''新增一个空文档 wordapp.Selection.TypeText window.dialogArguments ''''键入传递进来的文本 end if window.close '''' 关闭
六、结束语
好好学习、天天向上。做合格的社会主义计算机软件事业接班人。嘿嘿......
浏览器的定制与扩展
作者:李汉鹏
下载源代码
本文分如下章节:
- 前言
- 在MFC中使用浏览器
- 怎样扩展或定制浏览器
- 定制鼠标右键弹出出菜单
- 实现脚本扩展(很重要的external接口)
- C++代码中如何调用网页脚本中的函数
- 定制消息框的标题
- 怎样定制、修改浏览器向Web服务器发送的HTTP请求头
- 怎样修改浏览器标识
- 去掉讨厌的异常警告
- 怎样处理浏览器内的拖放
- 怎样禁止网页元素的选取
- 其它
前言
由于本人在开发中经常要在程序中嵌入浏览器,为了符合自己的需求经常要对浏览器进行扩展和定制, 解决这些问题需在网上找资料和学习的过程,我想可能很多开发者或许会遇到同样的问题,特写此文,以供大家参考。
在MFC中使用浏览器
在MFC中微软为我们提供了CHtmlView、CDHtmlDialog类让我们的程序很方便的嵌入浏览器和进行浏览器的二次开发,这比直 接使用WebBrowser控件要方便很多,所以本文中讨论的浏览器的问题都是针对CHtmlView来讨论的。文中将提到一个类CLhpHtmlView, 它是CHtmlView的派生类,文中提及的扩展或定制都将在CLhpHtmlView类(或派生类)上实现。
怎样扩展或定制浏览器
浏览器定义了一些扩展接口(如IDocHostUIHandler可以定制浏览器界面有关的行为),以便开发者进行定制和扩展。浏览 器会在需要的时候向他的控制站点查询这些接口,在控制站点里实现相应的接口就可以进行相应的扩展。在MFC7.01类 库中,CHtmlView使用的控制站点是CHtmlControlSite的,在CHtmlControlSite类中 只实现了接口IDocHostUIHandler, 而要实现更多的扩展接口,必须用自定义的控制站类来取代CHtmlControlSite,在下文中提及的类CDocHostSite即为自定义 的控制站类。
关于接口的介绍请参考:
http://dev.csdn.net/develop/article/48/48483.shtm
如何使自定义的控制站点来替换默认的控制站点呢?在MFC7.0中只需重载CHtmlView的虚函数CreateControlSite即可:
BOOL CLhpHtmlView::CreateControlSite(COleControlContainer * pContainer,COleControlSite ** ppSite, UINT /*nID*/, REFCLSID /*clsid*/){*ppSite = new CDocHostSite(pContainer, this);// 创建自己的控制站点实例return (*ppSite) ? TRUE : FALSE;}VC6.0要替换控制站要复杂的多,这里就不讨论了,如需要6.0版本的请给我发邮件到yourshine@21cn.com。
定制鼠标右键弹出出菜单
要定制浏览器的鼠标右键弹出菜单,必须在自定义的控制站点类中实现IDocHostUIHandler2接口,并且IE的 版本是5.5或以上。在接口IDocHostUIHandler2的ShowContextMenu方法中调用浏览器类的OnShowContextMenu虚函数,我们 在浏览器类的派生类重载此虚函数即可实现右键菜单的定制,参见代码
HRESULT CDocHostSite::XDocHostUIHandler::ShowContextMenu(DWORD dwID,POINT * ppt,IUnknown * pcmdtReserved,IDispatch * pdispReserved){METHOD_PROLOGUE(CDocHostSite, DocHostUIHandler);return pThis->m_pView->OnShowContextMenu( dwID, ppt, pcmdtReserved,pdispReserved );}HRESULT CLhpHtmlView::OnShowContextMenu(DWORD dwID,LPPOINT ppt,LPUNKNOWN pcmdtReserved,LPDISPATCH pdispReserved){HRESULT result = S_FALSE;switch(m_ContextMenuMode){case NoContextMenu: // 无菜单result=S_OK;break;case DefaultMenu: // 默认菜单break;case TextSelectionOnly: // 仅文本选择菜单if(!(dwID == CONTEXT_MENU_TEXTSELECT || dwID == CONTEXT_MENU_CONTROL))result=S_OK;break;case CustomMenu: // 自定义菜单if(dwID!=CONTEXT_MENU_TEXTSELECT)result=OnShowCustomContextMenu(ppt,pcmdtReserved,pdispReserved);break;}return result;}在CLhpHtmlView中定义的枚举类型CONTEXT_MENU_MODE举出了定制右键弹出菜单的四种类型
enum CONTEXT_MENU_MODE // 上下文菜单{NoContextMenu, // 无菜单DefaultMenu, // 默认菜单TextSelectionOnly, // 仅文本选择菜单CustomMenu // 自定义菜单};通过CLhpHtmlView的函数SetContextMenuMode来设置右键菜单的类型。如果设定的右键弹出菜单是“自定义菜单”类型, 我们只要在CLhpHtmlView的派生类中重载OnShowCustomContextMenu虚函数即可,如下代码 CDemoView是CLhpHtmlView的派生类
HRESULT CDemoView::OnShowCustomContextMenu(LPPOINT ppt, LPUNKNOWN pcmdtReserved,LPDISPATCH pdispReserved){if ((ppt==NULL)||(pcmdtReserved==NULL)||(pcmdtReserved==NULL))return S_OK;HRESULT hr=0;IOleWindow *oleWnd=NULL;hr=pcmdtReserved->QueryInterface(IID_IOleWindow, (void**)&oleWnd);if((hr != S_OK)||(oleWnd == NULL))return S_OK;HWND hwnd=NULL;hr=oleWnd->GetWindow(&hwnd);if((hr!=S_OK)||(hwnd==NULL)){oleWnd->Release();return S_OK;}IHTMLElementPtr pElem=NULL;hr = pdispReserved->QueryInterface(IID_IHTMLElement, (void**)&pElem);if(hr != S_OK){oleWnd->Release();return S_OK;}IHTMLElementPtr pParentElem=NULL;_bstr_t tagID;BOOL go=TRUE;pElem->get_id(&tagID.GetBSTR());while(go && tagID.length()==0){hr=pElem->get_parentElement(&pParentElem);if(hr==S_OK && pParentElem!=NULL){pElem->Release();pElem=pParentElem;pElem->get_id(&tagID.GetBSTR());}elsego=FALSE;};if(tagID.length()==0)tagID="no id";CMenu Menu,SubMenu;Menu.CreatePopupMenu();CString strTagID = ToStr(tagID);if(strTagID == "red")Menu.AppendMenu(MF_BYPOSITION, ID_RED, "您点击的是红色");else if(strTagID == "green")Menu.AppendMenu(MF_BYPOSITION, ID_GREEN, "您点击的是绿色");else if(strTagID == "blue")Menu.AppendMenu(MF_BYPOSITION, ID_BLUE, "您点击的是蓝色");elseMenu.AppendMenu(MF_BYPOSITION, ID_NONE, "你点了也白点,请在指定的地方点击");int MenuID=Menu.TrackPopupMenu(TPM_RETURNCMD|TPM_LEFTALIGN|TPM_RIGHTBUTTON,ppt->x, ppt->y, this);switch(MenuID){case ID_RED:MessageBox("红色");break;case ID_GREEN:MessageBox("红色");break;case ID_BLUE:MessageBox("红色");break;case ID_NONE:MessageBox("haha");break;}oleWnd->Release();pElem->Release();return S_OK;}
实现脚本扩展(很重要的external接口)
在你嵌入了浏览器的工程中,如果网页的脚本中能调用C++代码,那将是一件很惬意的事情,要实现这种交互,就必须实现脚本扩展。实现脚本扩展就是在程序中实现一个IDispatch接口,通过CHtmlView类的OnGetExternal虚函数返回此接口指针,这样就可以在脚本中通过window.external.XXX(关键字window可以省略)来 引用接口暴露的方法或属性(XXX为方法或属性名)。在MFC中从CCmdTarget派生的类都可以实现自动化,而不必在MFC工程中引入繁杂的ATL。从CCmdTarget派生的类实现自动化接口的时候不要忘了在构造函数中调用EnableAutomation函数。
要使虚函数OnGetExternal发挥作用必须在 自定义的控制站点类中实现IDocHostUIHandler,在接口IDocHostUIHandler的GetExternal方法中调用浏览器类的OnGetExternal虚函数,我们在浏览器类的派生类重载OnGetExternal虚函数, 通过参数lppDispatch返回一个IDispatch指针,这样脚本中引用window.external时就是引用的返回的接口,参见代码
HRESULT CDocHostSite::XDocHostUIHandler::GetExternal(IDispatch ** ppDispatch){METHOD_PROLOGUE(CDocHostSite, DocHostUIHandler);return pThis->m_pView->OnGetExternal( ppDispatch );}CLhpHtmlView::CLhpHtmlView(BOOL isview){......EnableAutomation();// 允许自动化}HRESULT CLhpHtmlView::OnGetExternal(LPDISPATCH *lppDispatch){*lppDispatch = GetIDispatch(TRUE);// 返回自身的IDispatch接口return S_OK;}请注意上面代码中,在OnGetExternal返回的是自身IDispatch接口, 这样就不比为脚本扩展而另外写一个从CCmdTarget派生的新类, CLhpHtmlView本身就是从CCmdTarget派生,直接在上面实现接口就是。
下用具体示例来说明怎样实现脚本扩展
示例会在网页上点击一个按钮而使整个窗口发生抖动
从CLhpHtmlView派生一个类CDemoView,在类中实现IDispatch, 并通过IDispatch暴露方法WobbleWnd
---------------------------------------------------------------------------文件 DemoView.h---------------------------------------------------------------------------.......class CDemoView : public CLhpHtmlView{......DECLARE_DISPATCH_MAP() // 构建dispatch映射表以暴露方法或属性......void WobbleWnd();// 抖动窗口};---------------------------------------------------------------------------文件 DemoView.cpp---------------------------------------------------------------------------......// 把成员函数映射到Dispatch映射表中,暴露方法给脚本BEGIN_DISPATCH_MAP(CDemoView, CLhpHtmlView)DISP_FUNCTION(CDemoView, "WobbleWnd", WobbleWnd, VT_EMPTY, VTS_NONE)END_DISPATCH_MAP()......void CDemoView::WobbleWnd(){// 在这里实现抖动窗口......}---------------------------------------------------------------------------文件 Demo.htm---------------------------------------------------------------------------...... οnclick="external.WobbleWnd()" ......这里我要介绍一下DISP_FUNCTION宏,它的作用是将一个函数映射到Dispatch映射表中,我们看
DISP_FUNCTION(CDemoView, "WobbleWnd", WobbleWnd, VT_EMPTY, VTS_NONE)
CDemoView是宿主类名, "WobbleWnd"是暴露给外面的名字(脚本调用时使用的名字), VT_EMPTY是返回值得类型为空,VTS_NONE说明此方法没有参数,如果要映射的函数有返回值和参数该 如何映射,通过下面举例来说明
DISP_FUNCTION(CCalendarView,"TestFunc",TestFunc,VT_BOOL,VTS_BSTR VTS_I4 VTS_I4)BOOL TestFunc(LPCSTR param1, int param2, int param3){.....}参数表VTS_BSTR VTS_I4 VTS_I4是用空格分隔,他们的类型映射请参考MSDN,这要提醒的是不要把VTS_BSTR与CString对应,而应与LPCSTR对应。
C++代码中如何调用网页脚本中的函数
IHTMLDocument2::scripts属性表示HTML文档中所有脚本对象。使用脚本对象的IDispatch接口的GetIDsOfNames方法可以得到脚本函数的 DispID,得到DispID后,使用IDispatch的Invoke函数可以调用对应的脚本函数。CLhpHtmlView提供了方便的调用JavaScript的函数,请参考CLhpHtmlView中有关键字“JScript”的代码。
定制消息框的标题
我们在脚本中调用alert弹出消息框时,消息框的标题是微软预定义的“Microsoft Internet Explorer”,如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/e23d7586b2e57012871bc39d8e2cfcbf.png)
在自定义的控制站点类中实现IDocHostShowUI接口,在接口的ShowMessage方法中调用浏览器的OnShowMessage,我们重载 OnShowMessage虚函数即可定制消息框的标题,实现代码如下:
// 窗口标题"Microsoft Internet Explorer"的资源标识#define IDS_MESSAGE_BOX_TITLE 2213HRESULT CLhpHtmlView::OnShowMessage(HWND hwnd,LPOLESTR lpstrText,LPOLESTR lpstrCaption,DWORD dwType,LPOLESTR lpstrHelpFile,DWORD dwHelpContext,LRESULT * plResult){//载入Shdoclc.dll 和IE消息框标题字符串HINSTANCE hinstSHDOCLC = LoadLibrary(TEXT("SHDOCLC.DLL"));if (hinstSHDOCLC == NULL)return S_FALSE;CString strBuf,strCaption(lpstrCaption);strBuf.LoadString(hinstSHDOCLC, IDS_MESSAGE_BOX_TITLE);// 比较IE消息框标题字符串和lpstrCaption// 如果相同,用自定义标题替换if(strBuf==lpstrCaption)strCaption = m_DefaultMsgBoxTitle;// 创建自己的消息框并且显示*plResult = MessageBox(CString(lpstrText), strCaption, dwType);//卸载Shdoclc.dll并且返回FreeLibrary(hinstSHDOCLC);return S_OK;}从代码中可以看到通过设定m_DefaultMsgBoxTitle的值来改变消息宽的标题,修改此值是同过SetDefaultMsgBoxTitle来实现
void CLhpHtmlView::SetDefaultMsgBoxTitle(CString strTitle){m_DefaultMsgBoxTitle=strTitle;}
怎样定制、修改浏览器向Web服务器发送的HTTP请求头
在集成了WebBrowser控件的应用中,Web服务器有时可能希望客户端(浏览器)发送的HTTP请求中附带一些额外的信息或自定义的 HTTP头字段,这样就必须在浏览器中控制向Web服务器发送的HTTP请求。 下面是捕获的一个普通的用浏览器发送的HTTP请求头:
GET /text7.htm HTTP/1.0Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, \application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*Referer: http://localhostAccept-Language: en-usUser-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; Poco 0.31; LHP Browser 1.01; \.NET CLR 1.1.4322)Host: localhostConnection: Keep-AliveCHtmlView的void Navigate2(LPCTSTR lpszURL,DWORD dwFlags = 0,LPCTSTR lpszTargetFrameName = NULL,LPCTSTR lpszHeaders = NULL,LPVOID lpvPostData = NULL,DWORD dwPostDataLen = 0);函数参数lpszHeaders可以指定HTTP请求头,示例如下:
Navigate2(_T("http://localhost"),NULL,NULL, "MyDefineField: TestValue");我们捕获的HTTP头如下:
![](https://i-blog.csdnimg.cn/blog_migrate/1441bf55d0fb790fbe4bb33c7c28bd41.png)
怎样修改浏览器标识
在HTTP请求头中User-Agent字段表明了浏览器的版本以及操作系统的版本等信息。WEB服务器经常需要知道用户请求页面时是来自IE还是来自自己的客户端中的WebBrowser控件, 以便分开处理,而WebBrowser控件向WEB服务器发送的浏览器标识(User-Agent字段)跟用IE发送的是一样的,怎样区分自己的浏览器和IE呢? 微软没有提供现成的方法,要自己想法解决。 前面讨论的定制HTTP请求头就是为这一节准备的。 思路是这样的: 在自己的浏览器里处理每一个U页面请求,把请求头User-Agent改成自己想要的。 在CHtmlView的OnBeforeNavigate2虚函数里来修改HTTP请求是再好不过了,
#define WM_NVTO (WM_USER+1000)class NvToParam{public:CString URL;DWORD Flags;CString TargetFrameName;CByteArray PostedData;CString Headers;};void CDemoView::OnBeforeNavigate2(LPCTSTR lpszURL,DWORD nFlags,LPCTSTR lpszTargetFrameName,CByteArray& baPostedData,LPCTSTR lpszHeaders,BOOL* pbCancel){CString strHeaders(lpszHeaders);if(strHeaders.Find("User-Agent:LHPBrowser 1.0") < 0)// 检查头里有没有自定义的User-Agent串{*pbCancel = TRUE;// 没有,取消这次导航if(!strHeaders.IsEmpty())strHeaders += "\r\n";strHeaders += "User-Agent:LHPBrowser 1.0";// 加上自定义的User-Agent串NvToParam* pNvTo = new NvToParam;pNvTo->URL = lpszURL;pNvTo->Flags = nFlags;pNvTo->TargetFrameName = lpszTargetFrameName;baPostedData.Copy(pNvTo->PostedData);pNvTo->Headers = strHeaders;// 发送一个自定义的导航消息,并把参数发过去PostMessage(WM_NVTO,(WPARAM)pNvTo);return;}CHtmlView::OnBeforeNavigate2(lpszURL,nFlags,lpszTargetFrameName,baPostedData,lpszHeaders,pbCancel);}LRESULT CDemoView::OnNvTo(WPARAM wParam, LPARAM lParam){NvToParam* pNvTo = (NvToParam*)wParam;Navigate2((LPCTSTR)pNvTo->URL,pNvTo->Flags,pNvTo->PostedData,(LPCTSTR)pNvTo->TargetFrameName,(LPCTSTR)pNvTo->Headers);delete pNvTo;return 1;}在OnBeforeNavigate2中如果发现没有自定义的User-Agent串,就加上这个串,并取消本次导航,再Post一个消息(一定要POST,让OnBeforeNavigate2跳出以后再进行导航 ),在消息中再次导航,再次导航时请求头已经有了自己的标识,所以能正常的导航。
去掉讨厌的异常警告
在程序中使用了CHtmlView以后,我们在调整窗口大小的时候经常会看到输出窗口输出的异常警告: ReusingBrowser.exe 中的 0x77e53887 处最可能的异常: Microsoft C++ exception: COleException @ 0x0012e348 。
Warning: constructing COleException, scode = DISP_E_MEMBERNOTFOUND($80020003).这是由于CHtmlView在处理WM_SIZE消息时的一点小问题引起的,采用如下代码处理WM_SIZE消息就不会有此警告了
void CLhpHtmlView::OnSize(UINT nType, int cx, int cy){CFormView::OnSize(nType, cx, cy);if (::IsWindow(m_wndBrowser.m_hWnd)){CRect rect;GetClientRect(rect);// 就这一句与CHtmlView的不同::AdjustWindowRectEx(rect, GetStyle(), FALSE, WS_EX_CLIENTEDGE);m_wndBrowser.SetWindowPos(NULL,rect.left,rect.top,rect.Width(),rect.Height(),SWP_NOACTIVATE | SWP_NOZORDER);}}
怎样处理浏览器内的拖放
有时可能有这样的需求,我们希望在资源管理器里托一个文件到浏览器而做出相应的处理,甚至是将文件拖到某一个网页元素上来做出相应的处理,而浏览器默认的处理拖放文件操作是将文件打开,但WebBrowser控件给了我们一个自己处理拖放的机会。 那就是在自定义的控制站点类中实现IDocHostUIHandler,在接口IDocHostUIHandler的GetDropTarget方法中调用 浏览器类的OnGetDropTarget虚函数。要处理网页内的拖放,必需在OnGetDropTarget函数中返回一个自己定义的IDropTarget接口指针, 所以我们自己写一个类CMyOleDropTarget从COleDropTarget类派生,并且在实现IDropTarget接口,此类的代码在这就不列出了,请下载演示 程序,参考文件MyOleDropTarget.h和MyOleDropTarget.cpp。我们看CLhpHtmlView中OnGetDropTarget的代码
HRESULT CLhpHtmlView::OnGetDropTarget(LPDROPTARGET pDropTarget, LPDROPTARGET* ppDropTarget ){m_DropTarget.SetIEDropTarget(pDropTarget);LPDROPTARGET pMyDropTarget;pMyDropTarget = (LPDROPTARGET)m_DropTarget.GetInterface(&IID_IDropTarget);if(pMyDropTarget){*ppDropTarget = pMyDropTarget;pMyDropTarget->AddRef();return S_OK;}return S_FALSE;}m_DropTarget即为自定义的处理拖放的对象。这样就能通过在从CLhpHtmlView派生的类中重载OnDragEnter、OnDragOver、 OnDrop、OnDragLeave虚函数来处理拖放了。在这里顺带讲一下视图是怎样处理拖放的。 要使视图处理拖放,首先在视图里添加一个COleDropTarget(或派生类)成员变量,如CLhpHtmlView中的“CMyOleDropTarget m_DropTarget;”,再在 视图创建时调用COleDropTarget对象的Register,即把视图与COleDropTarget对象关联起来,如CLhpHtmlView中的“m_DropTarget.Register(this);”,再对拖放 触发的事件进行相应的处理, OnDragEnter 把某对象拖入到视图时触发,在此检测拖入的对象是不是视图想接受的对象,如是返回“DROPEFFECT_MOVE”表示接受此对象,如
if(pDataObject->IsDataAvailable(CF_HDROP))// 被拖对象是文件吗?return DROPEFFECT_MOVE;OnDragOver 被拖对象在视图上移动,同OnDragEnter一样检测拖入对象,如果要接受此对象返回“DROPEFFECT_MOVE”。 OnDrop 拖着被拖对象在视图上放开鼠标,在这里对拖入对象做出处理; OnDragLeave 拖着被拖对象离开视图。 C++的代码写好了,但事情还没完,还必须在网页里用脚本对拖放事件进行处理, 即页面里哪个元素要接受拖放对象哪个元素就要处理ondragenter、ondragover、ondrop,代码其实很简单,让事件的返回值为false即可,这样 C++的代码才有机会处理拖放事件,代码如下:
......<td οndragenter="event.returnValue = false" οndragοver="event.returnValue = false" \οndrοp="event.returnValue = false">......如果要使整个视图都接受拖放,则在Body元素中处理此三个事件。 注意:别忘了让工程对OLE的支持即在初始化应用程序时调用AfxOleInit()。
怎样禁止网页元素的选取
用网页做界面时多数情况下是不希望网页上的元素是能够被鼠标选中的, 要使网页元素不能被选中做法是:给浏览器的“宿主信息标记”加上DOCHOSTUIFLAG_DIALOG标记。
“宿主信息标记”用N个标记位来控制浏览器的许多性质,如:
- 禁用浏览器的3D的边缘;
- 禁止滚动条;
- 禁用脚本;
- 定义双击处理的方式;
- 禁用浏览器的自动完成功能;
...... 更多详情请参考MSDN的DOCHOSTUIFLAG帮助。
怎样修改“宿主信息标记”?
在CDocHostSite中实现IDocHostUIHandler, 在GetHostInfo方法中调用浏览器的OnGetHostInfo虚函数,在虚函数OnGetHostInfo中便可指定“宿主信息标记”,如:
HRESULT CLhpHtmlView::OnGetHostInfo(DOCHOSTUIINFO * pInfo){pInfo->cbSize = sizeof(DOCHOSTUIINFO);pInfo->dwFlags = DOCHOSTUIFLAG_DIALOG |DOCHOSTUIFLAG_THEME |DOCHOSTUIFLAG_NO3DBORDER |DOCHOSTUIFLAG_SCROLL_NO;pInfo->dwDoubleClick = DOCHOSTUIDBLCLK_DEFAULT;return S_OK;}用脚本也可实现: 在Head中加入脚本:
document.onselectstart=new Function(''return false'');或者
<body onselectstart="return false">。
其它
在CLhpHtmlView中还提供了几个函数, 修改网页元素的内容:
BOOL PutElementHtml(CString ElemID,CString Html);取表单元素的值:
BOOL GetElementValue(CString ElemID,CString& Value);设置表单元素的值:
BOOL PutElementValue(CString ElemID,CString Value);给表单元素设置焦点:
void ElementSetFocus(CString EleName);
VC++开发BHO插件——定制你的浏览器
关键字 BHO Browser COM ATL 网址过滤
原作者姓名 陆其明
文章原始出处 http://hqtech.nease.net
读者评分 18 评分次数 4
正文
在Windows操作系统上,我们最常见的浏览器有两种:文件浏览器(exploer.exe,应用于文件系统)和Internet浏览器(iexplore.exe,应用于互联网资源)。由于这两个浏览器功能强大,而且又与Windows操作系统捆绑销售,最终也就成为了浏览器的标准。但有时候,为了给浏览器加入一些新的特性,我们往往会重新设计一个自己的浏览器。新的浏览器模仿标准浏览器的大部分功能,同时加入新特性。这种做法最直观,但实际上也是相对于微软的重复劳动,且工作量比较大。其实,使用BHO插件,一切都变得很简单。
BHO(Browser Help Objects),是实现了特定接口的COM组件。开发好的BHO插件在注册表特定的位置注册好后,每当微软的浏览器启动,BHO实例就会被创建。在浏览器工作的工程中,BHO会接收到很多事件,比如浏览器浏览新的地址、前进或后退、生成新的窗口、浏览器退出等等;BHO可以在这些事件的响应中实现与浏览器的交互。
下面,我们首先来介绍一下BHO的工作原理。上面我们已经提到,BHO是COM组件,而且一定实现了IObjectWithSite接口。这些组件除了在注册表中注册为COM Server外,还必须将它们的CLSID在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\ CurrentVersion\Explorer\Browser Helper Objects下注册为子键。微软在设计浏览器的时候,已经给这些组件预留了空间。每当浏览器启动时,浏览器会首先在上述注册表位置查看是否有注册的BHO CLSID;如果有则分别创建一个实例,并对BHO实例进行初始化,建立交互连接。(注:BHO实例只有在创建它的浏览器窗口销毁时才被释放。)下图演示了BHO的创建过程:
成功创建的BHO,不仅可以得到各种标准的浏览器操作事件,并做出响应;还可以定制浏览器的菜单、工具条等界面元素;更或者可以安装钩子函数,监视浏览器的一举一动。值得注意的是,使用BHO插件,Internet浏览器要求在4.0以上版本;如果是文件浏览器,操作系统要求是Windows 95/98/2000或Window NT 4.0以上版本,并且Shell的版本在4.71以上。下面是支持BHO特性的系统一览表:
Shell版本 操作系统版本 支持BHO
4.00 Windows 95 and Windows NT 4.0(IE版本为 4.0) 仅IE4.0
4.71 Windows 95 and Windows NT 4.0(IE版本为 4.0) IE和文件浏览器
4.72 Windows 98 IE和文件浏览器
5.00 Windows 2000 IE和文件浏览器
接下去,笔者就来介绍一下如何开发BHO插件,开发环境为VC6.0(使用ATL),安装Platform SDK中的Internet Development SDK。首先,启动VC的ATL COM AppWizard,生成一个项目名为BhoPlugin,其余均采用默认设置。接着,我们就来分步详细阐述。
第一步,增加一个ATL Object到该项目中。VC菜单Insert->New ATL Object…,在弹出的对话框中选择“Internet Explorer Object”,输入COM类名(在Short Name后输入EyeOnIE,其它各项会自动生成)。完成后,我们可以看到CEyeOnIE类有一个基类IObjectWithSiteImpl,这个就是实现IObjectWithSite接口的模版类。
第二步,实现IObjectWithSite的接口方法。在这之前,我们要先定义几个成员变量:CComQIPtr<IWebBrowser2, &IID_IWebBrowser2> mWebBrowser2,(需要加入#include "ExDisp.h"),用以保存浏览器组件的指针;DWORD mCookie,用以保存与浏览器的连接ID。IObjectWithSite有两个接口方法:SetSite和GetSite。我们只需重载SetSite就行了。在EyeOnIE.h中增加函数声明STDMETHOD(SetSite)(IUnknown *pUnkSite),在EyeOnIE.cpp实现如下:
STDMETHODIMP CEyeOnIE::SetSite(IUnknown *pUnkSite)
{
USES_CONVERSION;
if (pUnkSite)
{
mWebBrowser2 = pUnkSite;
if (mWebBrowser2)
{
return RegisterEventHandler(TRUE);
}
}
return E_FAIL;
}
HRESULT CEyeOnIE::RegisterEventHandler(BOOL inAdvise)
{
CComPtr<IConnectionPoint> spCP;
// Receives the connection point for WebBrowser events
CComQIPtr<IConnectionPointContainer, &IID_IConnectionPointContainer> spCPC(mWebBrowser2);
HRESULT hr = spCPC->FindConnectionPoint(DIID_DWebBrowserEvents2, &spCP);
if (FAILED(hr))
return hr;
if (inAdvise)
{
// Pass the event handlers to the container
hr = spCP->Advise(reinterpret_cast<IDispatch*>(this), &mCookie);
}
else
{
spCP->Unadvise(mCookie);
}
return hr;
}
我们可以看到,SetSite的参数实际上指向的是浏览器组件。在SetSite实现中,我们首先保存浏览器组件指针,然后将该BHO向浏览器注册为事件处理器。
第三步,实现IDispatch接口方法。事件处理也就在IDispatch::Invoke中实现(各个事件的ID在ExDispID.h中定义)。BHO可能会接收到很多事件,但我们只需要响应我们感兴趣的那一部分。首先在EyeOnIE.h中增加该函数的声明,在EyeOnIE.cpp的实现中,笔者试着响应浏览器浏览一个地址之前发出的事件DISPID_BEFORENAVIGATE2,以此来实现简单的网址过滤功能,代码参考如下:
STDMETHODIMP CEyeOnIE::Invoke(DISPID dispidMember,REFIID riid, LCID lcid,
WORD wFlags, DISPPARAMS * pDispParams,
VARIANT * pvarResult,EXCEPINFO * pexcepinfo,
UINT * puArgErr)
{
USES_CONVERSION;
if (!pDispParams)
return E_INVALIDARG;
switch (dispidMember)
{
//
// The parameters for this DISPID are as follows:
// [0]: Cancel flag - VT_BYREF|VT_BOOL
// [1]: HTTP headers - VT_BYREF|VT_VARIANT
// [2]: Address of HTTP POST data - VT_BYREF|VT_VARIANT
// [3]: Target frame name - VT_BYREF|VT_VARIANT
// [4]: Option flags - VT_BYREF|VT_VARIANT
// [5]: URL to navigate to - VT_BYREF|VT_VARIANT
// [6]: An object that evaluates to the top-level or frame
// WebBrowser object corresponding to the event.
//
case DISPID_BEFORENAVIGATE2:
{
LPOLESTR lpURL = NULL;
mWebBrowser2->get_LocationURL(&lpURL);
char * strurl;
if (pDispParams->cArgs >= 5 && pDispParams->rgvarg[5].vt == (VT_BYREF|VT_VARIANT))
{
CComVariant varURL(*pDispParams->rgvarg[5].pvarVal);
varURL.ChangeType(VT_BSTR);
strurl = OLE2A(varURL.bstrVal);
}
if (strstr(strurl, "girl.com"))
{
*pDispParams->rgvarg[0].pboolVal = TRUE;
::MessageBox(NULL, _T("该网页已被禁止!"),_T("Warning"),MB_ICONSTOP);
return S_OK;
}
break;
}
case DISPID_NAVIGATECOMPLETE2:
break;
case DISPID_DOCUMENTCOMPLETE:
break;
case DISPID_DOWNLOADBEGIN:
break;
case DISPID_DOWNLOADCOMPLETE:
break;
case DISPID_NEWWINDOW2:
break;
case DISPID_QUIT:
RegisterEventHandler(FALSE);
break;
default:
break;
}
return S_OK;
}
我们看到,当用户浏览的新地址包含"girl.com"字符的时候,浏览器就会弹出一个警告对话框,并且停止进一步的动作。另外值得注意的是,在DISPID_QUIT事件(浏览器将要退出)的响应中,我们将BHO事件处理器进行了注销。
第四步,因为BHO可能会被文件浏览器加载。如果我们不想这样,我们就要在DllMain中对加载者进行判断,参考如下:
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
// Check who's loading us.
// If it's Explorer then "no thanks" and exit...
TCHAR pszLoader[MAX_PATH];
GetModuleFileName(NULL, pszLoader, MAX_PATH);
_tcslwr(pszLoader);
if (_tcsstr(pszLoader, _T("explorer.exe")))
return FALSE;
_Module.Init(ObjectMap, hInstance, &LIBID_BHOPLUGINLib);
DisableThreadLibraryCalls(hInstance);
}
else if (dwReason == DLL_PROCESS_DETACH)
_Module.Term();
return TRUE; // ok
}
最后,别忘了修改注册表文件,追加BHO的注册信息。在EyeOnIE.rgs文件的下面增加如下代码:
HKLM
{
SOFTWARE
{
Microsoft
{
Windows
{
CurrentVersion
{
Explorer
{
'Browser Helper Objects'
{
{6E28339B-7A2A-47B6-AEB2-46BA53782379}
}
}
}
}
}
}
}
注意,{6E28339B-7A2A-47B6-AEB2-46BA53782379}是笔者这个BHO的CLSID,如果你自己开发BHO,这里应该正确填写你的CLSID。
好了,一个简单的BHO开发完成了。(可以到本人的个人主页 http://hqtech.nease.net/ 下载实例源代码。)BHO插件可以实现的功能还有很多,比如网页内容分析、IE界面定制等等。作为总结,笔者还要提醒读者一点的是,如果不想让BHO起作用了,可以注销该插件,如下格式:regsvr32 /u yourpath\yourbho.dll,或者直接在注册表中将“Browser Helper Objects”目录下注册的CLSID删掉。
正文完
内存调试技巧:C 语言最大难点揭秘
本文将带您了解一些良好的和内存相关的编码实践,以将内存错误保持在控制范围内。内存错误是 C 和 C++ 编程的祸根:它们很普遍,认识其严重性已有二十多年,但始终没有彻底解决,它们可能严重影响应用程序,并且很少有开发团队对其制定明确的管理计划。但好消息是,它们并不怎么神秘。引言
C 和 C++ 程序中的内存错误非常有害:它们很常见,并且可能导致严重的后果。来自计算机应急响应小组(请参见参考资料)和供应商的许多最严重的安全公告都是由简单的内存错误造成的。自从 70 年代末期以来,C 程序员就一直讨论此类错误,但其影响在 2007 年仍然很大。更糟的是,如果按我的思路考虑,当今的许多 C 和 C++ 程序员可能都会认为内存错误是不可控制而又神秘的顽症,它们只能纠正,无法预防。
但事实并非如此。本文将让您在短时间内理解与良好内存相关的编码的所有本质:
正确的内存管理的重要性
存在内存错误的 C 和 C++ 程序会导致各种问题。如果它们泄漏内存,则运行速度会逐渐变慢,并最终停止运行;如果覆盖内存,则会变得非常脆弱,很容易受到恶意用户的攻击。从 1988 年著名的莫里斯蠕虫攻击到有关 Flash Player 和其他关键的零售级程序的最新安全警报都与缓冲区溢出有关:“大多数计算机安全漏洞都是缓冲区溢出”,Rodney Bates 在 2004 年写道。
在可以使用 C 或 C++ 的地方,也广泛支持使用其他许多通用语言(如 Java?、Ruby、Haskell、C#、Perl、Smalltalk 等),每种语言都有众多的爱好者和各自的优点。但是,从计算角度来看,每种编程语言优于 C 或 C++ 的主要优点都与便于内存管理密切相关。与内存相关的编程是如此重要,而在实践中正确应用又是如此困难,以致于它支配着面向对象编程语言、功能性编程语言、高级编程语言、声明性编程语言和另外一些编程语言的所有其他变量或理论。
与少数其他类型的常见错误一样,内存错误还是一种隐性危害:它们很难再现,症状通常不能在相应的源代码中找到。例如,无论何时何地发生内存泄漏,都可能表现为应用程序完全无法接受,同时内存泄漏不是显而易见。
因此,出于所有这些原因,需要特别关注 C 和 C++ 编程的内存问题。让我们看一看如何解决这些问题,先不谈是哪种语言。
内存错误的类别
首先,不要失去信心。有很多办法可以对付内存问题。我们先列出所有可能存在的实际问题:
1.内存泄漏
2.错误分配,包括大量增加 free()释放的内存和未初始化的引用
3.悬空指针
4.数组边界违规
这是所有类型。即使迁移到 C++ 面向对象的语言,这些类型也不会有明显变化;无论数据是简单类型还是 C 语言的 struct或 C++ 的类,C 和 C++ 中内存管理和引用的模型在原理上都是相同的。以下内容绝大部分是“纯 C”语言,对于扩展到 C++ 主要留作练习使用。
内存泄漏在分配资源时会发生内存泄漏,但是它从不回收。下面是一个可能出错的模型(请参见清单 1):
清单 1. 简单的潜在堆内存丢失和缓冲区覆盖
以下是引用片段: void f1(char *explanation) { char p1; p1 = malloc(100); (void) sprintf(p1, "The f1 error occurred because of '%s'.", explanation); local_log(p1); } |
您看到问题了吗?除非 local_log()对 free()释放的内存具有不寻常的响应能力,否则每次对 f1的调用都会泄漏 100 字节。在记忆棒增量分发数兆字节内存时,一次泄漏是微不足道的,但是连续操作数小时后,即使如此小的泄漏也会削弱应用程序。
在实际的 C 和 C++ 编程中,这不足以影响您对 malloc()或 new的使用,本部分开头的句子提到了“资源”不是仅指“内存”,因为还有类似以下内容的示例(请参见清单 2)。FILE句柄可能与内存块不同,但是必须对它们给予同等关注:
清单 2. 来自资源错误管理的潜在堆内存丢失
以下是引用片段: int getkey(char *filename) { FILE *fp; int key; fp = fopen(filename, "r"); fscanf(fp, "%d", &key); return key; } |
fopen的语义需要补充性的 fclose。在没有 fclose()的情况下,C 标准不能指定发生的情况时,很可能是内存泄漏。其他资源(如信号量、网络句柄、数据库连接等)同样值得考虑。
内存错误分配
错误分配的管理不是很困难。下面是一个示例(请参见清单 3):
清单 3. 未初始化的指针
以下是引用片段: void f2(int datum) { int *p2; /* Uh-oh! No one has initialized p2. */ *p2 = datum; ... } |
关于此类错误的好消息是,它们一般具有显著结果。在 AIX 下,对未初始化指针的分配通常会立即导致 segmentation fault错误。它的好处是任何此类错误都会被快速地检测到;与花费数月时间才能确定且难以再现的错误相比,检测此类错误的代价要小得多。
在此错误类型中存在多个变种。free()释放的内存比 malloc()更频繁(请参见清单 4):
清单 4. 两个错误的内存释放
以下是引用片段: /* Allocate once, free twice. */ void f3() { char *p; p = malloc(10); ... free(p); ... free(p); } /* Allocate zero times, free once. */ void f4() { char *p; /* Note that p remains uninitialized here. */ free(p); } |
这些错误通常也不太严重。尽管 C 标准在这些情形中没有定义具体行为,但典型的实现将忽略错误,或者快速而明确地对它们进行标记;总之,这些都是安全情形。
悬空指针
悬空指针比较棘手。当程序员在内存资源释放后使用资源时会发生悬空指针(请参见清单 5):
清单 5. 悬空指针
以下是引用片段: void f8() { struct x *xp; xp = (struct x *) malloc(sizeof (struct x)); xp.q = 13; ... free(xp); ... /* Problem! There's no guarantee that the memory block to which xp points hasn't been overwritten. */ return xp.q; } |
传统的“调试”难以隔离悬空指针。由于下面两个明显原因,它们很难再现:
即使影响提前释放内存范围的代码已本地化,内存的使用仍然可能取决于应用程序甚至(在极端情况下)不同进程中的其他执行位置。
悬空指针可能发生在以微妙方式使用内存的代码中。结果是,即使内存在释放后立即被覆盖,并且新指向的值不同于预期值,也很难识别出新值是错误值。悬空指针不断威胁着 C 或 C++ 程序的运行状态。
数组边界违规
数组边界违规十分危险,它是内存错误管理的最后一个主要类别。回头看一下清单 1;如果 explanation的长度超过 80,则会发生什么情况?回答:难以预料,但是它可能与良好情形相差甚远。特别是,C 复制一个字符串,该字符串不适于为它分配的 100 个字符。在任何常规实现中,“超过的”字符会覆盖内存中的其他数据。内存中数据分配的布局非常复杂并且难以再现,所以任何症状都不可能追溯到源代码级别的具体错误。这些错误通常会导致数百万美元的损失。
内存编程的策略
勤奋和自律可以让这些错误造成的影响降至最低限度。下面我们介绍一下您可以采用的几个特定步骤;我在各种组织中处理它们的经验是,至少可以按一定的数量级持续减少内存错误。
编码风格编码风格是最重要的,我还从没有看到过其他任何作者对此加以强调。影响资源(特别是内存)的函数和方法需要显式地解释本身。下面是有关标头、注释或名称的一些示例(请参见清单 6)。
清单 6. 识别资源的源代码示例
以下是引用片段: /******** * ... * * Note that any function invoking protected_file_read() * assumes responsibility eventually to fclose() its * return value, UNLESS that value is NULL. * ********/ FILE *protected_file_read(char *filename) { FILE *fp; fp = fopen(filename, "r"); if (fp) { ... } else { ... } return fp; } /******* * ... * * Note that the return value of get_message points to a * fixed memory location. Do NOT free() it; remember to * make a copy if it must be retained ... * ********/ char *get_message() { static char this_buffer[400]; ... (void) sprintf(this_buffer, ...); return this_buffer; } /******** * ... * While this function uses heap memory, and so * temporarily might expand the over-all memory * footprint, it properly cleans up after itself. * ********/ int f6(char *item1) { my_class c1; int result; ... c1 = new my_class(item1); ... result = c1.x; delete c1; return result; } /******** * ... * Note that f8() is documented to return a value * which needs to be returned to heap; as f7 thinly * wraps f8, any code which invokes f7() must be * careful to free() the return value. * ********/ int *f7() { int *p; p = f8(...); ... return p; } |
使这些格式元素成为您日常工作的一部分。可以使用各种方法解决内存问题:
专用库
语言
软件工具
硬件检查器在这整个领域中,我始终认为最有用并且投资回报率最大的是考虑改进源代码的风格。它不需要昂贵的代价或严格的形式;可以始终取消与内存无关的段的注释,但影响内存的定义当然需要显式注释。添加几个简单的单词可使内存结果更清楚,并且内存编程会得到改进。
我没有做受控实验来验证此风格的效果。如果您的经历与我一样,您将发现没有说明资源影响的策略简直无法忍受。这样做很简单,但带来的好处太多了。
检测检测是编码标准的补充。二者各有裨益,但结合使用效果特别好。机灵的 C 或 C++ 专业人员甚至可以浏览不熟悉的源代码,并以极低的成本检测内存问题。通过少量的实践和适当的文本搜索,您能够快速验证平衡的 *alloc()和 free()或者 new和 delete的源主体。人工查看此类内容通常会出现像清单 7中一样的问题。
清单 7. 棘手的内存泄漏
以下是引用片段: static char *important_pointer = NULL; void f9() { if (!important_pointer) important_pointer = malloc(IMPORTANT_SIZE); ... if (condition) /* Ooops! We just lost the reference important_pointer already held. */ important_pointer = malloc(DIFFERENT_SIZE); ... } |
如果 condition为真,简单使用自动运行时工具不能检测发生的内存泄漏。仔细进行源分析可以从此类条件推理出证实正确的结论。我重复一下我写的关于风格的内容:尽管大量发布的内存问题描述都强调工具和语言,对于我来说,最大的收获来自“软的”以开发人员为中心的流程变更。您在风格和检测上所做的任何改进都可以帮助您理解由自动化工具产生的诊断。
静态的自动语法分析
当然,并不是只有人类才能读取源代码。您还应使静态语法分析成为开发流程的一部分。静态语法分析是 lint、严格编译和几种商业产品执行的内容:扫描编译器接受的源文本和目标项,但这可能是错误的症状。
希望让您的代码无 lint。尽管 lint已过时,并有一定的局限性,但是,没有使用它(或其较高级的后代)的许多程序员犯了很大的错误。通常情况下,您能够编写忽略 lint的优秀的专业质量代码,但努力这样做的结果通常会发生重大错误。其中一些错误影响内存的正确性。与让客户首先发现内存错误的代价相比,即使对这种类别的产品支付最昂贵的许可费也失去了意义。清除源代码。现在,即使 lint标记的编码可能向您提供所需的功能,但很可能存在更简单的方法,该方法可满足 lint,并且比较强键又可移植。
内存库
补救方法的最后两个类别与前三个明显不同。前者是轻量级的;一个人可以容易地理解并实现它们。另一方面,内存库和工具通常具有较高的许可费用,对部分开发人员来说,它们需要进一步完善和调整。有效地使用库和工具的程序员是理解轻量级的静态方法的人员。可用的库和工具给人的印象很深:其作为组的质量很高。但是,即使最优秀的编程人员也可能会被忽略内存管理基本原则的非常任性的编程人员搅乱。据我观察,普通的编程人员在尝试利用内存库和工具进行隔离工作时也只能感到灰心。
由于这些原因,我们催促 C 和 C++ 程序员为解决内存问题先了解一下自己的源。在这完成之后,才去考虑库。
使用几个库能够编写常规的 C 或 C++ 代码,并保证改进内存管理。Jonathan Bartlett 在 developerWorks 的 2004 评论专栏中介绍了主要的候选项,可以在下面的参考资料部分获得。库可以解决多种不同的内存问题,以致于直接对它们进行比较是非常困难的;这方面的常见主题包括垃圾收集、智能指针和智能容器。大体上说,库可以自动进行较多的内存管理,这样程序员可以犯更少的错误。
我对内存库有各种感受。他们在努力工作,但我看到他们在项目中获得的成功比预期要小,尤其在 C 方面。我尚未对这些令人失望的结果进行仔细分析。例如,业绩应该与相应的手动内存管理一样好,但是这是一个灰色区域——尤其在垃圾收集库处理速度缓慢的情况下。通过这方面的实践得出的最明确的结论是,与 C 关注的代码组相比,C++ 似乎可以较好地接受智能指针。
内存工具
开发真正基于 C 的应用程序的开发团队需要运行时内存工具作为其开发策略的一部分。已介绍的技术很有价值,而且不可或缺。在您亲自尝试使用内存工具之前,其质量和功能您可能还不了解。
本文主要讨论了基于软件的内存工具。还有硬件内存调试器;在非常特殊的情况下(主要是在使用不支持其他工具的专用主机时)才考虑它们。
市场上的软件内存工具包括专有工具(如 IBM Rational Purify 和 Electric Fence)和其他开放源代码工具。其中有许多可以很好地与 AIX 和其他操作系统一起使用。
所有内存工具的功能基本相同:构建可执行文件的特定版本(很像在编译时通过使用 -g标记生成的调试版本)、练习相关应用程序和研究由工具自动生成的报告。请考虑如清单 8所示的程序。
清单 8. 示例错误
以下是引用片段: int main() { char p[5]; strcpy(p, "Hello, world."); puts(p); } |
此程序可以在许多环境中“运行”,它编译、执行并将“Hello, world.\n”打印到屏幕。使用内存工具运行相同应用程序会在第四行产生一个数组边界违规的报告。在了解软件错误(将十四个字符复制到了只能容纳五个字符的空间中)方面,这种方法比在客户处查找错误症状的花费小得多。这是内存工具的功劳。
结束语
作为一名成熟的 C 或 C++ 程序员,您认识到内存问题值得特别关注。通过制订一些计划和实践,可以找到控制内存错误的方法。学习内存使用的正确模式,快速发现可能发生的错误,使本文介绍的技术成为您日常工作的一部分。您可以在开始时就消除应用程序中的症状,否则可能要花费数天或数周时间来调试。
C++中使用BHO来屏蔽特定网站
BHO(浏览器辅助对象)是一种简单的ATL COM对象,而Internet Explorer会在每次运行时都加载它;换句话来说,即每个Internet Explorer的实例都会加载它。BHO运行在Internet Explorer的地址空间内,能对可访问的对象(如窗口、模块等等)执行任何操作,且因为它依附于浏览器的主窗口,所以其生命期与浏览器实例的生命期一致。
如果在系统中打开了活动桌面,BHO也能随同Windows Explorer一起启动。如果不想在Windows Explorer中运行BHO,可将如下代码添加到DLLMain中:
TCHAR strLoader[MAX_PATH]; ::GetModuleFileName (NULL, strLoader, MAX_PATH); if(stricmp("explorer.exe", strLoader) == 0) return FALSE; |
BHO的COM Server必须实现IObjectWithSite,以便对象可以挂钩到浏览器事件,Internet Explorer会依靠IObjectWithSite将一个指针传递到它的IUnknown接口,所以,只需实现IObjectWithSite的SetSite方法就行了,如下所示:
STDMETHODIMP CBhoApp::SetSite(IUnknown *pUnkSite) { //获取并存储IWebBrowser2指针 m_spWebBrowser2 = pUnkSite; if (m_spWebBrowser2 == NULL) return E_INVALIDARG; //获取并存储IConnectionPointerContainer指针 m_spCPC = m_spWebBrowser2; if (m_spCPC == NULL) return E_POINTER; //连接到宿主程序以接收事件通知 return Connect(); } |
以下是Connect函数比较简单的实现:
HRESULT CBhoApp::Connect() { HRESULT hr; CComPtr<IConnectionPoint> spCP; //获取访问WebBrowser事件的连接指针 hr = m_spCPC->FindConnectionPoint(DIID_DWebBrowserEvents2, &spCP); if (FAILED(hr)) return hr; //把事件处理程序传递给宿主程序Each time an event //每次有事件产生时,宿主程序都会调用我们实现的IDispatch接口的函数 hr = spCP->Advise(reinterpret_cast<IDispatch*>(this),&m_dwCookie); return hr; } |
通过调用Advise方法,告之浏览器BHO想要接受事件通知,这意味着BHO会把指向IDispatch的指针提供给浏览器(这是由于要进行组件事件处理),接下来,浏览器会调用IDispatch的Invoke方法,并传递给它一个事件的ID作为参数,因此,BHO必须实现Invoke方法以处理所发生的事件。
STDMETHODIMP CBhoApp::Invoke(DISPID dispidMember,REFIID riid, LCID lcid,WORD wFlags, DISPPARAMS *pDispParams,VARIANT *pvarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) { //在使用ATL字符串转换宏(此处用的是OLE2T)防止编译错误时,必须先调用这个宏 USES_CONVERSION; if(dispidMember == DISPID_BEFORENAVIGATE2) { BSTR bstrUrlName; HRESULT hr = m_spWebBrowser2->get_LocationURL(&bstrUrlName); if(FAILED(hr)) return hr; LPTSTR psz = new TCHAR[SysStringLen(bstrUrlName)]; lstrcpy(psz, OLE2T(bstrUrlName)); //此处,直接比较www.xyz.com,也可从一个要屏蔽的网站列表中进行比较。 if(stricmp("http://www.xyz.com/",psz) == 0) { VARIANT vFlags = ,vTargetFrameName = ; //如果不想显示"空白页",也可重定向至某个表明此网站已被屏蔽的页面。 m_spWebBrowser2->Navigate(SysAllocString(L"about:blank"),&vFlags,&vTargetFrameName, NULL,NULL); m_spWebBrowser2->put_Visible(VARIANT_TRUE); return S_FALSE; } return S_OK; } else if(dispidMember == DISPID_NAVIGATECOMPLETE2) //检查以防止页面的post导航加载(post-navigation loading) { BSTR bstrUrlName; HRESULT hr = m_spWebBrowser2->get_LocationURL(&bstrUrlName); if(FAILED(hr)) return hr; //把文本从Unicode转换为ANSI LPTSTR psz = new TCHAR[SysStringLen(bstrUrlName)]; lstrcpy(psz, OLE2T(bstrUrlName)); ::OutputDebugString("In Navigate Complete"); ::OutputDebugString(psz); if(stricmp("http://www.xyz.com/",psz) == 0) { VARIANT vFlags = ,vTargetFrameName = ; m_spWebBrowser2->Navigate(SysAllocString(L"about:blank"), &vFlags,&vTargetFrameName, NULL,NULL); m_spWebBrowser2->put_Visible(VARIANT_TRUE); } return S_OK; } return S_FALSE; } |
还需修改工程的.rgs文件,依据所定格式添加以下注册表项:
HKLM SOFTWAREMicrosoftWindowsCurrentVersionExplorerBrowser Helper Objects ForceRemove |
在编译完成后,使用regsvr32注册组件,如果想卸载,只需在regsvr32后带上/u。
显示内存中的HTML内容
IE WebControl控件提供了显示HTML的功能,但是只能够显示URL或是文档。很多朋友都询问关于显示内存中HTML内容的方法,前几天我在网上找到一段代码,我在这里进行一些讲解。
思路:
先用WebControl显示一个空的HTML文档(对于WebControl的用法有了解),然后利用接口IID_IHTMLDocument2,来重新配置Body标记内的InnerHTML属性。(需要对DHTML有了解)
实现方法:
从CHTMLView派生新类CMoreHtmlView。
显示空的HTML文档,能够直接利用Navigate2方法,您能够指定一个默认的HTML文档,但是WebControl还能够显示定义在资源中的内容,根据Res Protocol的定义,显示使用资源中的数据时用下面的表示方法:
res://sFile[/sType]/sID
sFile:表示包含资源的文档
sType:资源的类型,例如RT_HTML或您自己定义的资源类型,省略时为RT_HTML,
sID:资源ID
实例代码
void CMoreHtmlView::NavigateMemory()
{
//get application name
CString sAppName=AfxGetAppName();
CString sResourseID;
//get resourse ID of Empty Html
sResourseID.Format("%d", IDR_EMPTY_HTML);
CString sNavigatePath;
//compile
sNavigatePath="res://"+sAppName+".exe/"+sResourseID;
Navigate2( sNavigatePath); //装入资源中的HTML文档 此行实现功能
//Activate memory mode
m_bMemoryMode=TRUE;
}
修改Body标记内InnerHTML属性。
实例代码
BOOL CMoreHtmlView::PutBodyContent(LPSTR lpstrContent)
{
//store body content
if( lpstrContent) m_lpstrBodyContent=lpstrContent;
//check if HtmlDocument initialized
if( m_pHtmlDoc2) //m_pHtmlDoc2 指向接口 IID_IHTMLDocument2
{
HRESULT hr = S_OK;
IHTMLElement *pBodyElement;
//get body element
hr=m_pHtmlDoc2->get_body( &pBodyElement); //得到Body标记
//put content to body element
_bstr_t pbBody( m_lpstrBodyContent);
hr=pBodyElement->put_innerHTML( pbBody); //配置HTML标记内内容
if( hr==S_FALSE) return FALSE;
else return TRUE;
}
else return FALSE;
}
怎样得到IHTMLDocument2
void CMoreHtmlView::OnDocumentComplete(LPCTSTR lpszURL)
{
// show html on first loading of document
if(m_bMemoryMode)
{
LPDISPATCH lpDispatch;
lpDispatch=GetHtmlDocument();
ASSERT(lpDispatch);
//get html document from IDispatch
HRESULT hr=lpDispatch->QueryInterface(IID_IHTMLDocument2, (void**)&m_pHtmlDoc2); //此行实现功能
if( SUCCEEDED(hr)) ShowMemoryHtml();
}
CHtmlView::OnDocumentComplete(lpszURL);
}
示范代码的阅读:
上面提到的三点是基本的方法,但是要阅读我找到的这份代码需要做一点特别的说明。关于IHTMLDocument2的获得是在文档装入结束后才查询此接口。