课外现代文阅读及答案:IE内核浏览器开发笔记

来源:百度文库 编辑:偶看新闻 时间:2024/05/06 14:01:05

开发一个浏览器,或者浏览器插件,或者浏览器上的工具条,并不是很难的事情,因为微软已经考虑到了这一些需求,因此给我们提供了一些接口和方法来实现这些想法,只是这些接口比较难懂。
开发浏览器插件,一般使用的是BHO技术,主要的流程就是在注册表里告诉IE,我写了一个BHO的插件,是符合COM规范的,你在用户打开IE的时候,把我也加载上,做为你这个IE进程的附属品(所以每一个IE进程会加载一个独立的插件,他们之间如果要通信就是进程间通信)。开发类似google的工具栏也是一样的流程。当IE把你的插件和工具栏加载了之后,你要做的就是根据COM的规范,去取得加载你的那个IE进程的接口(interface),这样你就可以查询IE的一些状态和一些你想要的值(例如当前的网页地址等)。如果你想监控IE的行为,那么通过在IE提供的一个接口里面进行注册,IE就会在一些事件(例如navigate到一个新的页面,页面开始下载,页面加载完毕等)发生时,通知你来进行一定的处理。具体的资料文档可以看我在本文最后附的文档。
开发一个浏览器,则主要是依靠webbrower这个控件。微软的ATL这种技术(也是遵循COM规范的),可以把一些常用的东东按照COM规范封装起来,提供给MFC程序或者其他WINDOWS程序使用,封装好的ATL中文叫做控件。而webbrower这个控件,则封装了整个IE的功能,使得开发者可以在自己的程序中嵌入这个控件,制作出自己的浏览器,我们平时所使用的遨游等基于IE内核的浏览器,就是这样开发出来的。
开发浏览器这种windows程序,会需要用到一些库,否则你就要从windows的SDK开始开发程序,工作量会比较大。微软给我们提供的类库主要是MFC(集成在VC里面),使用MFC可以很简单的完成程序框架搭建,消息映射,对话框界面编辑,通用控件的使用等很多工作,并且是可视化的。但是由于MFC过于庞大和复杂,学习起来也很花时间,并且还有一些其他不能忍受的缺点(这是WTL说的),所以微软的内部人员自己开发了一个轻量级的库叫WTL,希望替代MFC。WTL是个很奇怪的东西,没有得到微软的官方支持,但是也没有得到官方反对,官方提供他的下载。而且在很多开源社区,都是拿WTL进行开发。但是对于我来讲,由于WTL中文资料过少,并且缺乏MSDN这样的东西,使用起来不是很方便,我还是选择了MFC作为我的开发库。WTL本身应该是很好用的,而且在WTL的自带sample里面,就有一个例子恰巧是一个简单的多标签浏览器,感兴趣的可以去看看,在这个基础上开发自己的浏览器也是个不错的选择。

选择了MFC之后,对MFC提供的框架也要做个选择。MFC提供 对话框,MDI,SDI 三种程序框架。对话框上面各种东西都要自己添加,适合那些简单的程序,放弃掉,剩下的就是MDI和SDI程序。
选择MDI和SDI后,MFC会为你自动生成一些代码,主要的只有这么几个:CMainFrame,CChildFrame,CDocument和CView,其中SDI没有CChildFrame。
这四个类的含义是:
CMainFrame:程序的主窗口,菜单,工具条,状态栏等都在这个窗口实现。在这个主窗口里面可以包含多个子窗口,其中只有一个子窗口处于active状态,处于active状态的子窗口会被在最上面显示并能够接收消息。
CView:用于显示的子窗口区域。注意这个类只是一个显示区域,不能单独做为子窗口,每个VIEW必须依附一个FRAME才能显示(这个FRAME可以是MianFrame也可以是ChildFrame)。
CChildFrame:这个是MDI程序独有的,MDI程序中,每个MianFrame包含多个ChildFrame,ChildFrame再包含VIEW(每个ChildFrame可以包含多个VIEW)。
CDocument:这个是用于数据存放的一个类,在我们开发浏览器的程序中几乎没有用,因为浏览器显示的数据都是从网络下载的,不是在硬盘上的,而且用户也一般不去修改它,只是显示,因此只需要VIEW就可以了。
这四个类的关系是:MainFrame只有一个,Document可以有多个,每个Document可以有多种显示形式(VIEW)。ChildFrame纯粹是为了把VIEW打包显示所做的一个类,可以不要,让MainFrame来负责VIEW的显示。

这样我们开发自己的浏览器的基本架构就出来了,webbrower这个控件可以把它封装在VIEW里面,也就是每个VIEW封装一个,用于显示一个HTML页面,多个VIEW就是多个页面,然后我们在MainFrame上添加一个标签控制栏,控制这些VIEW的切换,新增和删除就可以了。由于VIEW中封装的webbrower直接根据URL从网络下载要显示的HTML数据,因此这些VIEW不需要对应到某个Document,这样CDocument这个类我们就不需要了。
实际上,在VIEW里面封装webbrower控件的工作,微软也已经帮我们做好了,微软封装好的这个VIEW叫做CHTMLVIEW,我们可以直接拿来使用。

MDI和SDI实际上我一开始都尝试过,有没有ChildFrame对程序确实区别不大,但是由于MDI中的多了一层ChildFrame,使得程序复杂了一些。并且ChildFrame默认不是全屏的,所以在切换每个ChildFrame时,都会需要把当前的ChildFrame弄成全屏。这个工作对于我相当难,不管是一开始修改ChildFrame的style,重载ChildFrame的绘制函数,使用MDIMAXFRAME,对ChildFrame发送最大化消息等,总有可能在某些情况下ChildFrame变成非全屏的。而且从全屏变为非全屏会引起闪烁,所以我放弃了,使用简单的SDI没有什么不好的。


以下是我收集整理的关于插件和工具条开发的一些资料:
一:COM的基础概念
COM:微软的进程间通用接口,主要概念为interface.
CLSID:对于每个组件类,都需要分配一个唯一表示它的代码,就是ID,为了避免冲突,微软使用GUID作为CLSID,有生成GUID的函数,主要是根据当时的时间、机器地址等信息动态产生,理论上可保证全球唯一。
IID:接口的ID。
DLL:一个容器,里面可能包含多个com对象(多个CLSID),每个COM对象可能有多个IID

由于COM/DCOM系统组件之间通讯是和位置无关的,也即一个使用组件功能的客户程序在编写时不需要考虑组件的位置,组件的定位和通讯由系统完成。
客户端程序:
  (1) 调用CoInitializeEx初始化。
  因为程序的很多辅助功能是由库函数和操作系统中的各种服务自动完成的,如组件的定位和加载,并且这些工作很复杂,所以程序需要首先作一些初始化。
  (2)调用CoCreateInstance创建对象。
  第1个参数CLSID_InsideCOM是一个128位的标识-类标识符(CLSID),在程序中定义为 {0×10000002,0×0000, 0×0000,{0×00,0×00,0×00,0×00,0×00,0×00,0×00,0×01}},今后这样128位的标识在表示时将省略0x并用"-"代替 ",";第4个参数IID_IUnknown也是一个128位的标识-接口标识符(IID);第5个参数(void**)&pUnknown是一个指针,在返回时它指向一个接口实例的指针。
  CoCreateInstance()是一个库函数,从语义上说它创建对应类标识(CLSID)的一个COM/DCOM对象实例,并获得该对象的一个接口实例指针。对于进程内组件一个COM/DCOM对象是在一个DLL中实现的,在Windows系统注册表中维护着CLSID和DLL文件路径的对应关系,CoCreateInstance首先查找注册表,然后加载对应的DLL程序,调用该DLL的DllGetClaseObject引出函数(任何作为组件的DLL都必须提供该函数)以及其他的一些操作创建一个对象实例。
  一个COM/DCOM对象对于客户程序唯一可见的是它所包含的一组接口,每一个接口都由128位的IID标识,在整个COM/DCOM系统中都是唯一的(包括分布在不同机器上的COM/DCOM系统)。任何类型的COM/DCOM对象都必须支持IID_IUnknown(标识为00000000-0000-0000-C000-000000000046)接口。不同语言编写的客户程序中基本都有某种机制来标识接口指针,COM/DCOM用对象的IID_IUnknown接口指针的值来区分对象而没有独立的对象引用,任何对象在生命期内返回给客户程序的IID_IUnknown接口指针值必须是相同的。注意COM/DCOM对其他的接口指针值没有如上的要求。
  COM/DCOM规定IID_IUnknown接口由以下三个函数组成:
  QueryInterface(const IID iid, void **ppv);
  AddRef( );
  Release( );
  而且其他任何接口也必须包含这三个函数。其中AddRef和Release是用来控制对象生存周期的。 QueryInter- face则达到通过接口标识查询对象实现的接口,COM/DCOM规定通过对象的任何接口的QueryInterface函数可以获得同一个对象的其余接口指针。
  (3)pUnknown->QueryInterface(IID_ISum, (void**)&pSum);获得接口标识符为IID_Isum的另一个接口实例指针pSum。
  (4)hr = pSum->Sum(2, 3, &sum);通过接口实例指针pSum,调用接口的成员函数Sum。从编程模式的角度来看客户程序向组件程序发送功能请求在源程序中最终体现为调用接口的一个成员函数,并且实际上不论对于进程内组件还是对于进程外组件都是同样的。对于进程内组件这一调用就是通过调用同一个进程中的函数实现的,但是必须强调即使是进程内的调用遵循的仍然是二进制的规范,也即客户程序中的pSum指向的内存格式必须满足COM/DCOM的规范,至于这一规范是怎样的将在后文中讲述。对于一个确定的接口(确定值的IID),它的进程内组件不论用什么程序设计语言实现,生成的目标DLL返回给客户程序的接口指针所指向的内存格式都是一样的。
  (5)CoUninitialize();调用清理函数。
  通过以上对客户程序的分析,可见客户程序的编程模式为(a)创建对应CLSID的对象实例;(b)获得对象的初始接口指针;(c)通过接口指针的QueryInterfase函数查询其它接口指针;(d)通过接口指针调用接口的函数。(e)通过接口的AddRef()和Release()控制对象的生命期。客户程序和进程内组件程序遵循的二进制规范则体现在(a)128位的类表识CLSID和接口表识IID;(b)组件程序必须是一个合法的DLL,并且引出若干标准的函数如DellGetClassObject。(c)组件程序返回给客户程序的接口指针所指向的内存必须满足COM/DCOM规范。
  组件程序:
  现在分析一下组件程序的编写,了解对象是如何实现的。首先察看DllGetClassObject(const CLSID clsid, const IID id, (void **)ppv)函数,当客户程序加载该DLL后将首先调用该引出函数。该函数是一个进程内组件提供其服务的最基本的入口,也是进程内组件所遵循的二进制规范的一部分。DllGetClassObject的功能是根据CLSID判断本组件是否支持该类型的对象,一个组件可以支持多种类型的对象。DllGetClassObject根据CLSID生成对应的类厂对象,并根据输入参数const IID id将类厂对象的对应接口指针通过ppv返回给客户程序。这里的引入了类厂这个在客户程序中未提及的新概念。根据COM/DCOM规范,组件程序必须为自己支持的每个CLSID提供类厂对象,由类厂对象负责创建对应类型的COM/DCOM对象实例。类厂对象提供通常称为IID_IClassFactory的接口(其值为000001-0000-0000-C000-000000000046),客户程序通过调用该接口的CreateInstance(Iunknown *pUnknown-Outer, const IID iid, void **ppv)函数真正创建对象实例并获得对象的第一个接口指针。由此可见客户程序调用CoCreateInstance库函数实际完成了两个步骤的工作,它首先请求组件创建类厂对象,然后又通过类厂对象创建对应CLSID的对象实例。
  上面组件程序的实例中,在DllGetClassObject函数中通过new Cfactory创建了类厂对象,在CFactory:: CreateInstance 中通过new CInsideCOM创建类对象。COM/DCOM对象是以C++对象的形式实现的,接口指针是以C++中的对象指针的形式返回的。对于进程内组件,组件和客户程序在编写时是分别进行的,而在运行时处于同一个地址空间内又以指针的方式进行交互,那么交互的二进制兼容自然是基于内存格式的。
  在了解进程内组件的编写后,读者最大的疑惑必然是如何确保客户程序和组件程序在彼此独立的编写的过程中(甚至使用不同的语言)如何确保二进制兼容的。如前所述对于进程内组件二进制兼容包含三个方面的内容,128位标识符的识别以及确保组件DLL程序的合法性是很容易做到的,而如何确保在基于内存的交互时接口指针所指向的内存格式符合规范则显得有些复杂。下一节将介绍IDL语言,它是解决以上问题的重要手段。

ATL:轻量级模板机制,相比MFC的庞大,很轻便。
WTL:是在ATL之上的轻量级扩展编程库。

MFC:对API的包装,实现了MVC分离,方便界面开发。DLL庞大。
二:IE编程的方法和接口
2.1 基础知识(BHO和BANDS)
BHO:是一种IE提供的接口,可以跟随每个IE进程启动。

IE工具条:
Tool Bands(工具条栏)
IE-〉查看-〉工具栏
说明:该功能是在ie5.0中被加上的,和我们平常用的ToolBar一样。ie的ToolBar其实是个Rebar
Control,它相当于一个容器,它可以有一个或者多个子Band,它可以动态的改变子band的位置和大小。
Explorer Bars(浏览器栏)
IE->查看-〉浏览器栏中打开一个你想要的浏览器栏
说明: 该功能是在ie4.0中被加上的,他是ie的一个子窗口而已,被用于显示一些信息。他分为两种,竖向的(例如收藏夹)和纵向的(例如httpwatch)。
Desk Bands(桌面工具条栏)
右键任务栏-〉工具栏-〉选择你要显示的Desk Band
说明:该功能是和ie无关的,它主要是被显示在任务栏上便于用户操作,MediaPlayer在xp下就有个Desk Band,大家应该也都用过了。大家可以去看看msdn:ms-help://MS.VSCC.2003/MS.MSDNQTR.2006JAN.1033/shellcc/platform/shell/programmersguide/shell_adv/bands.htm

2.2 BAND和IE的互联机制
2.1.1 BAND需要实现的基本接口和如何注册
Band对象的基础知识
虽然这三种band看起来和一般的window窗口差不多,但是他是要宿主于容器之内的,原因很简单它不过是个com的组件而已。就像Explorer Bars被包含在IE浏览器中,DeskBands被包含Shell中。这几个Band的实现基本上一样,不同的只是他们要注册的位置和信息不同而已。
除了IUnknown和IClassFactory(类厂接口,不用我们实现,atl给我们写好了)外,所有的band对象都要实现下面三个接口:
IDeskBand(用于显示、隐藏Band对象)
IObjectWithSite(得到IE实例的IUnknown指针,这样就可以queryinterface其他的例如IWebBrowser2)
IPersistStream
在注册Explorer Bars和Desk Bands时必须注册为适当的组件类别为了确定该组件的类型和所在的容器,ToolBand不同于他们,它不需用注册类别因为它只有一种而且容器也不用指定。
Explorer Bars有两种类别:
竖向的 —-> CATID_InfoBand
横向的 —-> CATID_CommBand
DeskBand的类别: –〉CATID_DeskBand

如果band对象需要接受用户的健盘输入的话,那么必须实现IInputObject接口。
如果Explorer Bar或者Desk Band需要支持快捷菜单的话,那么必须实现IContextMenu接口,Tool band不支持该功能。
因为Band对象实现了一个子窗体,所以它也应该能处理window消息,Band对象能通过容器的IOleCommandTarget接口去向容器Send消息,该接口可以通过容器的IInputObjectSit结构QueryInterface到,你接着可以通过IOleCommandTarget::Exec发送消息。

Band的注册说明:
每个Com都有一个Clsid,但是我们的band如果被安装了那么会在相应的容器里面有个自己的菜单的以便来
控制显示或者隐藏自己,这个菜单的字符串一般都会被写到注册表中的:
HKEY_CLASSES_ROOT
CLSID
{你的Band对象的CLSID}
(Default) = 菜单的字符串
InProcServer32
(Default) = DLL的全路径
ThreadingModel= Apartment
IE的Tool Band除了要注册上面的信息外,还要在HKEY_LOCAL_MACHINESoftwareMicrosoftInternet ExplorerToolbar
下面添加我们ToolBand的clasid的key这样ie在启动的时候才能认为它是个ie的toolBar插件。
HKEY_LOCAL_MACHINE
Software
Microsoft
Internet Explorer
Toolbar
{你的Band对象的CLSID}
IE的Explorer Bar也同样,就是key不一样:
HKEY_CURRENT_USER
Software
Microsoft
Internet Explorer
Explorer Bars
{你的Band对象的CLSID}
BarSize

2.1.2 BAND如何得到IE的IUnknown

  微软并未在MSDN中说明如何将Google Toolbar这类IE的工具条插件嵌入自己的应用程序,但其基于COM的设计方法实际上给予了我们这个能力(创建嵌入式的工具条的方法并不是本文的重点,此处略去,有兴趣的朋友可以参考MSDN)。我们知道,除了IUnknown接口外,Bands和Bars(以下简称Band对象)还需要实现三个接口:IObjectWithSite,IPersistStream和IDeskBand。当用户选择工具条或面板时,其容器(如IE的外壳框架)就会调用Band对象的IObjectWithSite::SetSite方法(该方法仅需要一个IUnknown类型的指针),将自己实现的IUnknown指针传递给Band对象。这就是整个插件开始真正激活的入口,也是我们的着手点。
  MSDN中说到,一般来说,Band对象对于SetSite方法的实现需要完成以下几件事:
如果当前Band对象持有另外的Site指针,则首先释放该指针。
如果容器向SetSite方法传入的是一个空指针,则表示要删除该Band对象,此时SetSite返回S_OK即可。
如果容器传入的不是空指针,则需要设置新的Site:
对此IUnknown指针所指的新Site调用QueryInterface查询得到其IOleWindow接口。
调用得到的IOleWindow接口的GetWindow方法获取父窗口的句柄(此窗口即是Band对象的栖身之处)并保存下来。如果以后不会再用到IOleWindow接口的话就对其调用Release。
现在可以创建Band对象的窗口了,当然,要以第2步得到的窗口为父窗口来创建,并且该窗口目前只能以不可见状态存在。
如果Band对象实现了IInputObject接口,即需要接收键盘输入,则还需要向容器传来的Site查询(QueryInterface)IInputObjectSite接口,此接口指针也需要保存下来。
上述步骤完成后即可返回S_OK,否则应返回OLE-defined的error code告知容器什么地方出了错。

  显然,就我们要讨论的问题而言,只需换个角度(编写IE外壳的的角度)来考虑即可。首先,我们需要一个IUnknown接口(即Band对象所需的Site),其次需要一个IInputObjectSite接口,用以和Band对象的IInputObject接口交互,处理输入焦点转移的情况。接下来就可以通过Band对象的IDeskBand接口来显示、隐藏Band对象了(注意IDeskBand接口派生自IDockingWindow接口,后者又派生自IOleWindow接口)。

2.1.3 BAND得到IE的IUnknown之后,如何监控和控制IE的行为
如何对IE的行为进行监控:
IConnectionPointContainer接口:
至少需要实现IUnknown和IDispatch接口。当事件触发时,事件接收者的Invoke方法会被调用
//现在我们返回讨论Invoke.
//该方法有8个参数, 但我们将仅仅讨论其中的两个: dispidMember 和pDispParams. (其余的参见MSDN中的IDispatch::Invoke.)
//dispidMember 参数将告诉你哪一个事件被激发.
//如果客户应用程序接收来自Internet Explorer的事件, dispidMember 参数的值应当是DISPIDs 列表中的某个.
//pDispParams 输入参数是指向容器结构的指针, 存储事件激发时的其他项.
//传递到事件句柄的参数存储在pDispParams->rgvarg ,逆序存放.

//举例来说, Internet Explorer 激发NavigateComplete2 事件如下所示:
//NavigateComplete2(pDisp, URL)
//当 Invoke 被调用, pDispParams->cArgs 将包含两个值,
//URL 参数在 pDispParams->rgvarg[0] 以及pDisp 参数存储在 pDispParams->rgvarg[1].
//这些就是COM次序传递参数给Invoke 方法的方式.

2.1.4 BAND如何进行显示以及和用户互动
1:使用控件在band窗口里面再创建子窗口
2:使用消息映射接收用户行为触发的各种消息

WTL的消息映射:
要求继承自两个类(窗口类和消息类)

消息大部分是发给父窗口的?但是如果子窗口重写了某一个消息的处理函数,父窗口就会传递给子窗口进行处理,子窗口处理完毕后,可以使用宏 来让父窗口继续处理消息或者不再继续处理消息。

WTL使用宏替代函数来处理自己窗口的消息
CHAIN_MSG_MAP,如果有继承,那么子类处理完后可以交给父类继续处理
2.1.5 BAND如何存取注册表信息
2.1.6 BAND如何存取COOKIES
2.3 BAND的打包安装和卸载
2.3.1 BAND安装过程中的工作内容
1:需要能把自己注册成IE的工具条,并能在下次IE打开时自动显示。包括注册COM,注册工具条,修改注册表自动显示。
2:能把一个初始配置文件放在用户选择的目录,并需要在注册表写入配置文件的位置,以便IE打开时加载配置文件。
具体做法为打包程序在注册表中写用户的安装目录,工具条自己去读。
配置文件做成XML格式,便于维护。