查看文章 |
一、引言 最近,由于工作的要求,我需要在 IE 上做一些开发工作。于是在 MSDN 上翻阅了一些资料,根据 MSDN 上的说明我用 ATL 胜利完成了“资本家老板”分配的任务。 (并且在白天睡觉的过程中梦到了老板给我加工资啦......) 现在,我把 MSDN 上的原文资料,经过翻译整理并把一个 ATL 的实现奉贤给 VCKBASE 上的朋友们。
二、概念
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 的命令有:
3.4 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 环境,则不要忘记选择支持这个接口。 ![]() 4.3 输入对象名称,比如我想建立一个垂直的浏览器栏,不妨叫它 VerBar ![]() 4.4 线程模型必须选择 Apartment,接口类型的选择无所谓,看你想不想支持 IDispatch 接口功能了。在例子程序中的垂直浏览器栏中,由于想更简单的操纵 IE 和从 IE 中接受事件(连接点),选择 Dual 是必要的。聚合选项,你只要别选择 Only 就可以了。 ![]() 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 就会加载活动的桌面对象。 五、结束语 好了,到这里,就到这里了。祝大家学习快乐^_^ |



