简介 IE的面板实际就是嵌入到IE浏览器中的子窗体。面板有两种显示模式,一种是垂直显示在浏览器的左侧,一种是水平显示在浏览器的下方。IE浏览器内置提供了很多的标准面板,如收藏夹和搜索面板这些是垂直的面板,而每日提示和讨论面板则是水平的面板。
此外我们还可以通过实现COM组件来添加我们自己的浏览器面板到IE中。当我们的面板COM组件注册后,它会被添加到浏览器的查看菜单的浏览器栏的子菜单下。当用户选中该菜单项后,该面板就会显示在浏览器中。 实现面板COM组件 接下来我们将实现类似于IE内置的收藏夹的一个面板,不过它不是用来收藏网址的,而是用来运行程序的。下面是它的示意图: 同所有的COM组件一样,面板组件首先要实现IUnknown 和IClassFactory接口,不过Borland提供了TComObject类,这个类已经实现了前面两个接口,我们因此就不需要再重造轮子了,只要从TComObject类继承就可以了。 首先选择菜单File | New | ActiveX | Com Object命令,创建一个TDelphiBand的COM组件框架 然后我们就需要实现IDeskBand,IObjectWithSite和IPersistStreamInit或者IPersistStream接口了。其中IPersistStreamInit和IPersistStream接口可以用来保存面板的各种状态和信息,IPersistStreamInit和IPersistStream接口相比就是多了一个InitNew的方法,这个方法可以用来初始化面板组件的状态。在本文中,由于不涉及信息的保存,因此这个接口的方法的实现大部分都是直接返回E_NOTIMPL表示该方法没有被实现。除了GetClassID方法,当系统调用这个方法的时候,我们需要返回COM组件的GUID标识符。 function TDelphiBand.GetClassID(out classID: TCLSID): HResult; begin classID := CLSID_DelphiBand; Result := S_OK; end; IObjectWithSite接口,顾名思义这个接口就是一个嵌入在COM容器中的组件需要实现的接口,当用户选中一个浏览器面板的时候,IE这个COM容器会调用面板组件的IObjectWithSite的SetSite方法,这个方法传递过来的punkSite参数就是COM容器的IUnknown接口,通过这个接口我们可以获得IE提供的各个COM接口,进而可以调用IE提供的所有功能。这个方法的一般实现是: 1、 如果方法传过来的punkSite参数为nil,表示面板组件正在被隐藏释放。这时方法的返回结果必须为S_OK。 2、 如果punkSite参数不为nil,则通过punkSite获得我们所需要的COM接口(如IE的IWebBrowser接口),并将其保存起来。 下面是SetSite方法的实现代码: function TDelphiBand.SetSite(const pUnkSite: IUnknown): HResult; begin if Assigned(pUnkSite) then begin //获得IInputObjectSite接口,并保存起来,后面会用它通知浏览器 //面板焦点的变化情况 Site := pUnkSite as IInputObjectSite; //通过调用GetWindow来获得父窗体的句柄,并保存 (pUnkSite as IOleWindow).GetWindow(ParentWnd); //获得IE的IWebBrowser2接口 (Site as IServiceProvider).QueryService(IWebbrowserApp, IWebbrowser2, IE); end; Result := S_OK; end;
IObjectWithSite还有一个GetSite方法也需要实现,方法的定义如下: function GetSite(const riid: TIID; out site: IUnknown): HResult; stdcall; 其中我们需要返回在SetSite方法中保存的Site接口,然后从site接口获得riid参数所需要的接口,如果Site接口为nil,方法的返回的HResult需要设定为E_Fail,表示调用失败。如果Site接口支持riid指定的接口的话,我们就返回S_OK表示成功,如果Site接口不为nil,但是也不支持riid的接口的话,我们就返回E_NOINTERFACE,表示无法获得riid对应的接口。代码实现如下: function TDelphiBand.GetSite(const riid: TIID; out site: IUnknown): HResult; begin if Assigned(Site) then Result := Site.QueryInterface(riid, site) else Result := E_FAIL; end;
接下来的IDeskBand接口就是我们面板组件所要实现的核心接口了。它包括下面这些方法。 function GetBandInfo(dwBandID, dwViewMode: DWORD; var pdbi: TDeskBandInfo): HResult; stdcall; function ShowDW(fShow: BOOL): HResult; stdcall; function CloseDW(dwReserved: DWORD): HResult; stdcall; function ResizeBorderDW(var prcBorder: TRect; punkToolbarSite: IUnknown; fReserved: BOOL): HResult; stdcall; function GetWindow(out wnd: HWnd): HResult; stdcall; function ContextSensitiveHelp(fEnterMode: BOOL): HResult; stdcall; 其中ContextSensitiveHelp方法是用来提供上下文帮助支持的,未来简单起见,这里我们不提供上下文支持,所以简单返回E_NOTIMPL就可以了。而ResizeBorderDW 方法系统也从来不会调用,因此我们也返回E_NOTIMPL。
获得面板的显示信息
对于GetWindow方法来说,我们需要返回要嵌入到IE中的子窗体的句柄,以便容器用来显示。下面是GetWindow方法的实现: function TDelphiBand.GetWindow(out wnd: HWnd): HResult; begin //如果还没有创建面板窗体,则创建 begin //父窗体是先前在SetSite方法中获得的父窗体 BandForm := TBandForm.CreateParented(ParentWnd); //将IE赋值给面板界面的相应变量 BandForm.IE:=IE; End; //返回子窗体的句柄 Wnd := Bandform.Handle; SavedWndProc := Bandform.WindowProc; //替换默认的窗体消息过程 Bandform.WindowProc := BandWndProc; Result := S_OK; end;
此外,因为面板对象是以子窗体的形式显示,它还必须实现窗口消息过程。这里我们使用BandWndProc过程替换了窗体默认的消息处理过程,BandWndProc的实现如下:
procedure TDelphiBand.BandWndProc(var Message: TMessage); begin //当我们的窗体的子控件,如编辑框获得焦点后,系统会发WM_ParentNotify消息给 //我们的窗体,这时要通知浏览器我们面板获得了焦点,这样IE才会将快捷键发送给 //我们的窗体 if (Message.Msg = WM_PARENTNOTIFY) then begin Hasfocus:=True; FocusChange(True); end; //然后调用保存的原有窗体消息过程来处理 SavedWndProc(Message); end;
FocuseChanged方法用来通知浏览器焦点的变化情况,实现如下: procedure TDelphiBand.FocusChange(bHasFocus: Boolean); begin //调用先前保存的IInputObjectSite的OnFocusChangeIS方法通知浏览器 //焦点变化。 if (Site <> nil) then Site.OnFocusChangeIS(Self, bHasFocus); end;
系统获得子窗体的同时,因为IE本身对子窗体的显示区域是有一定限制的,所以它还需要知道面板窗体的某些属性,如最大显示尺寸、最小显示尺寸、改变大小时的尺寸变化幅度等信息,这些信息它是通过调用面板组件的IDeskBand接口的GetBandInfo方法来获得的。下面是GetBandInfo方法的声明: function GetBandInfo(dwBandID, dwViewMode: DWORD; var pdbi: TDeskBandInfo):HResult; 其中dwBandID参数是面板的标识符,这个标识符是COM容器赋予的,我们需要保存这个ID。dwViewMode表示面板的显示模式:
pdbi参数则是一个TDESKBANDINFO 结构,结构定义如下: DESKBANDINFO = packed record dwMask: DWORD; ptMinSize: TPointL; ptMaxSize: TPointL; ptIntegral: TPointL; ptActual: TPointL; wszTitle: array[0..255] of WideChar; dwModeFlags: DWORD; crBkgnd: COLORREF; end; 下表是关于结构中各个成员的详细说明:
当系统调用GetBandInfo方法后,我们只要根据系统请求返回相应的面板信息就可以了,代码实现如下: function TDelphiBand.GetBandInfo(dwBandID, dwViewMode: DWORD; var pdbi: TDeskBandInfo): HResult; begin //保存面板ID,以便后面使用 BandId := dwBandID; //如果请求最小尺寸,则窗体的最小宽度是窗体设计时的宽度 if (pdbi.dwMask or DBIM_MINSIZE) <> 0 then begin pdbi.ptMinSize.y := Bandform.width; pdbi.ptMinSize.x := -1; end; //最大尺寸无限制 if (pdbi.dwMask or DBIM_MAXSIZE) <> 0 then begin pdbi.ptMaxSize.x := -1; pdbi.ptMaxSize.y := -1; end; //大小改变幅度都为1 if (pdbi.dwMask or DBIM_INTEGRAL) <> 0 then begin pdbi.ptIntegral.x := 1; pdbi.ptIntegral.y := 1; end; //默认尺寸为窗体设计尺寸,注意我们这里的面板是垂直面板,因此x对应的高度 //y对应的是宽度 if (pdbi.dwMask or DBIM_ACTUAL) <> 0 then begin pdbi.ptActual.x := Bandform.Height; pdbi.ptActual.y := bandform.Width; end; //面板的宽度可调 if (pdbi.dwMask or DBIM_MODEFLAGS) <> 0 then begin pdbi.dwModeFlags := DBIMF_VARIABLEHEIGHT; end; //忽略背景色的设置,因为Delphi的窗体背景色是VCL自己画的,不使用 //Windows默认颜色 if (pdbi.dwMask or DBIM_BKCOLOR) <> 0 then begin pdbi.dwMask := pdbi.dwMask and (not DBIM_BKCOLOR); end; //返回面板的标题,注意返回的字符串必须是Unicode的 if (Pdbi.dwMask and DBIM_TITLE) = DBIM_TITLE then begin FillChar(pdbi.wszTitle, SizeOf(Caption) + 1, ' '); StringToWideChar(Caption, @pdbi.wszTitle, Length(Caption) + 1); end; Result := NOERROR; end;
当面板需要显示或隐藏的时候系统会调用IDeskBand接口的ShowDW方法通知面板改变窗体的显示状态,方法的fShow布尔参数表示需要显示还是隐藏窗体,代码实现如下:
function TDelphiBand.ShowDW(fShow: BOOL): HResult; begin Hasfocus:=fShow; FocusChange(fShow); Result := S_OK; end;
注意当显示或隐藏的时候,我们还需要调用FocusChange方法通知浏览器焦点发生了变化。 当IE需要关闭面板时,会调用IDeskBand的CloseDW方法,这时我们需要销毁面板窗体,代码实现如下: function TDelphiBand.CloseDW(dwReserved: DWORD): HResult; begin if BandForm <> nil then BandForm.Destroy;//为什么不是调用Free? Result := S_OK; end;
用户输入支持
实现了上面三个接口方法的的面板还是仅仅能够用来显示信息,如果我们需要在面板中提供一些编辑框,允许用户输入Email信息的话,那么还必须实现IInputObject接口。IInputObject接口有下面三个方法: function UIActivateIO(fActivate: BOOL; var lpMsg: TMsg): HResult; stdcall; function HasFocusIO: HResult; stdcall; function TranslateAcceleratorIO(var lpMsg: TMsg): HResult; stdcall;
Internet浏览器会在面板被激活和失活时调用UIActivateIO方法通知面板对象,这时面板应该调用SetFocus获得焦点。方法的fActivate表示是激活还是失活的状态,代码实现如下:
function TDelphiBand.UIActivateIO(fActivate: BOOL; var lpMsg: TMsg): HResult; begin Hasfocus:=fActivate; if HasFocus then Bandform.SetFocus; Result := S_OK; end;
当浏览器想要知道目前是哪个子窗体获得了焦点的时候,它会调用HasFocusIO方法,如果我们的面板获得了焦点,那么就应该返回S_OK,否则返回S_False。代码实现如下: function TDelphiBand.HasFocusIO: HResult; begin Result:=Integer(not HasFocus);//S_OK对应的常数为0,所以要not 一下 end;
最后一个TranslateAcceleratorIO方法允许面板处理键盘快捷键消息,代码实现如下: function TDelphiBand.TranslateAcceleratorIO(var lpMsg: TMsg): HResult; begin if (lpMsg.WParam <> VK_TAB) then begin TranslateMessage(lpMSg); DispatchMessage(lpMsg); Result := S_OK; end else Result := S_FALSE; end;
右键菜单支持 另外如果我们还想在面板中显示一个右键菜单以便用户能够快速的调用一些命令的话,如显示一个关于窗口等。我们还需要实现IContextMenu接口。当IE需要返回右键菜单项的时候,它会调用面板的IContextMenu接口的QueryContextMenu方法,这里我们向菜单添加了一个关于菜单,下面是实现代码:
function TDelphiBand.QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult; begin //添加关于菜单 InsertMenu(Menu, indexMenu, MF_STRING or MF_BYPOSITION, idCmdFirst , 'About...'); //返回添加的菜单数目 Result := 1; end;
当菜单处于高亮状态时,IE会调用菜单接口的GetCommandString方法获得上下文相关帮助,并显示在浏览器的状态条上,这里我们简单返回NOERROR,表示没有相关帮助就可以了。 function TDelphiBand.GetCommandString(idCmd, uType: UINT; pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): HResult; begin Result := NOERROR; end; 最后,当用户点击了菜单后,IE会调用菜单接口的InvokeCommand来实现菜单命令,我们只要简单地显示一个关于对话框就可以了。 function TDelphiBand.InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; begin //如果是被程序调用,则退出 if (HiWord(Integer(lpici.lpVerb)) <> 0) or (LoWord(lpici.lpVerb) > 0) then begin Result := E_FAIL; Exit; end; case LoWord(lpici.lpVerb) of 0: Showmessage('IE浏览器面板组件演示程序1.0'); end; Result := NO_ERROR; end; 程序界面 COM组件基本完成之后,就该设计我们的界面的功能了。现在浏览很多网站时都要填写登录界面或者其它一些注册信息,非常烦琐,所以下面我就要写一个自动填表的工具。界面非常简单,先放一个TCoolbar,然后放上一个列表框和一个保存按钮和一个填表按钮,自动填表的过程如下: 1. 当用户填写完表单信息后,点击保存按钮,COM组件会从页面上获取填写的表单信息,并将其保存到一个配置文件中。 2. 等用户下回再访问该页面时,选中被保存的表单,然后点击加载,COM组件就会自动填写上次保存的信息。 3. 考虑到很多网站的都要求输入密码,为了让用户在提交时,能看清填写的被*号遮蔽的密码,向导在加载表单时,会截获提交按钮的事件,在用户点击提交按钮时,会显示一个消息框,显示填写的密码内容。 下面是保存表单的代码:
保存表单内容时,首先通过IE获得页面的DOM接口IHtmlDocument,然后通过
COM组件的注册 实现完COM组件的功能后,剩下的就是注册到系统中以便IE加载了,面板COM组件首先要把自己的GUID写到注册表中HKEY_CLASS_ROOT下,下面是具体的注册表项: HKEY_CLASSES_ROOT CLSID {面板组件的CLSID GUID} (Default) = 显示在查看|浏览器栏的菜单文本 InProcServer32 (Default) = DLL路径 ThreadingModel= Apartment
同时面板还要在IE的注册表项下填写下面信息: HKEY_LOCAL_MACHINE Software Microsoft Internet Explorer Toolbar {面板组件的CLSID GUID} 最后,对于IE垂直面板来说,我们还要注册COM组件的类别: HKEY_CLASSES_ROOT\CLSID\<面板GUID>\ Implemented Categories\{00021493-0000-0000-C000-000000000046} 要注意的是从IE5开始,注册一个新的面板,需要删除下面的注册表项,迫使IE重建缓存来识别新的面板。 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Discardable\PostSetup\ Component Categories\{00021493-0000-0000-C000-000000000046}\Enum 下面是DeskBand类工厂的实现代码: TDelphiBandFactory = class(TComObjectFactory) private procedure AddKeys; procedure RemoveKeys; public procedure UpdateRegistry(Register: Boolean); override; end; procedure TDelphiBandFactory.UpdateRegistry(Register: Boolean); begin inherited UpdateRegistry(Register); if Register then AddKeys else RemoveKeys; end; procedure TDelphiBandFactory.AddKeys; var S: string; begin S := GUIDToString(CLSID_DelphiBand); with TRegistry.Create do try //删除注册表项,迫使IE重建缓存,发现新面板 DeleteKey('Software\Microsoft\Windows\CurrentVersion\Explorer\Discardable\ PostSetup\Component Categories\' + VerticalBand + '\Enum'); RootKey := HKEY_CLASSES_ROOT; if OpenKey('CLSID\' + S, True) then begin WriteString('', '&Delphi Band'); CloseKey; end; if OpenKey('CLSID\' + S + '\InProcServer32', True) then begin WriteString('ThreadingModel', 'Apartment'); CloseKey; end; if OpenKey('CLSID\' + S + '\Implemented Categories\' + VerticalBand, True) then CloseKey; finally Free; end; end; procedure TDelphiBandFactory.RemoveKeys; var S: string; begin S := GUIDToString(CLSID_DelphiBand); with TRegistry.Create do try RootKey := HKEY_CLASSES_ROOT; DeleteKey('CLSID\' + S + '\Implemented Categories\' + VerticalBand); DeleteKey('CLSID\' + S + '\InProcServer32'); DeleteKey('CLSID\' + S); Closekey; finally Free; end; end; initialization TDelphiBandFactory.Create(ComServer, TDelphiBand, CLSID_DelphiBand, '', Caption, ciMultiInstance); end. 注册组件之后,启动IE,打开随书光盘中的LoginForm.html,然后启动面板程序,执行加载和保存功能,界面示意图:
最后 实际上面板对象不仅仅可以应用在IE中,外壳的任务条的快捷方式工具条实际上也是面板组件,也可以采用本文提到的类似的技术来实现,除了要填写的注册表项内容不同外,其它大同小异,因此这里就不赘述了,有兴趣的朋友直接察看MSDN就可以了。
参考文献: 1、 Peter Larsen写的IE面板组件例子的代码。 2、 MSDN。 |
|