编写可复用性更好的C++代码——Band对象和COMToys(四)

2024-01-14 11:38

本文主要是介绍编写可复用性更好的C++代码——Band对象和COMToys(四),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

编译/赵湘宁

原著:Paul Dilascia

MSJ November 1999 & December 1999

关键字:Bands 对象,Desk Bands,Info/Comm Bands,Explorer Bar,Tool Bands。

本文假设你熟悉C++,COM,IE。

下载本文源代码: MyBands.zip (128KB)
                TestEditSrch.zip (75KB)


第一部分:Band 对象介绍
第二部分:BandObj的类层次和MyBands服务程序的注册
第三部分:深入Band内部,揭开Band的面纱


第四部分 Band对象使用中遇到的一些问题

    前面所描述的是一些Band对象的基本操作和处理,这些基本操作理论上都是按照你设计好的思路实现了所要的功能。但在实际的编程过程中,并不是每一件事情都按照它应该的方式进行。下面将告诉你有关Band对象使用中可能遇到的问题。
    文档中说桌面Band必须实现IPersistStream接口来存储持久性数据。到底是什么数据呢?根据跟踪诊断显示,只有桌面Band 会查询IPersistStream接口,而Explorer是不会查询IPersistStream接口的,但要查询IPersistStreamInit接口。那么IPersistStream有什么用呢?又一则隐藏在微软例子代码中的注释是这样说的:"如果用户要拖拽桌面Band离开任务栏到桌面上,则IPersistStream是必须要实现的接口。"如第一部分中的图二所示,如果你不想要这个特性,就不用实现IPersistStream接口。
    TRACE还显示Windows查询IOleCommandTarget 和IDiscardableBrowserProperty接口。后面的这个接口是我特别喜爱的接口之一,它没有函数!有人会问怎么可能一个接口不带函数呢?IDiscardableBrowserProperty只是用来通知IE:"如果你访问另一个页面,便可以丢弃我的页面数据。它们是可消耗的。"有关IDiscardableBrowserProperty的详细信息参见MSDN的另一篇文章:" Discardable Properties for Your Web Pages in Internet Explorer 4.0"。
    最后,TRACE揭示Windows还要查询另一个神秘的接口,它就是——{EA5F2D61-E008-11CF-99CB-00C04FD64497},这个ID未出现在任何文档里面,源文件或注册表当中。如果有那位仁兄知道有关这个接口的信息的话,请把它公诸于世。
    当你第一次创建桌面Band时,可能会遇到Windows不识别它的麻烦。但对于浏览栏(info和comm类型的Band),只需重新启动一次IE或资源管理器即可。对于Windows不识别桌面Band的问题,只有一个解决方法就是用Ctrl-Alt-Del杀掉资源管理器进程或重启机器。但最近我在新安装的第二版Windows 98中测试时,发现我的桌面Band神秘地停止工作。不管我注册多少次DLL和杀掉资源管理器进程都没用,我的桌面Band拒绝出现。同样的问题还发生在Windows 2000中。
    经历了无数困惑和坎坷之后,我几乎绝望了。这时我想起求助于MS的老大,他们可能会解决这个问题。得到的答复是叫我参考微软知识库的有关文章--Q214842,这篇文章解开了这个秘密--"Windows 2000(第二版Windows 98也是同样的问题)只在它感应到有一个运行的安装程序或者注册表中没有提供缓冲(cache)位置的时候才刷新种类缓冲。" Windows"能感应"到安装程序?。它是超人吗?本文下面要揭示有两种方法使得资源管理器重新建立种类缓冲:从setup.exe或者install.exe程序安装(这就是所谓的超人力量,不过如此!)或删除下面的这个注册表键:
HKEY_CLASSES_ROOT/Component Categories/ {00021492-0000-0000-C000-000000000046}/Enum,它是一个缓冲。({00021492...}是指CATID_DeskBand)。我修改了BandObj.rgs文件内容,总是删除这个键值。问题解决了。但是仍然要重启外壳让Windows产生新的种类缓冲。
    接下来是一串关联的问题,它们都是由一个相同的bug导致的。这个问题就是:无论我什么时候编译源代码,总是出现"不能用写方式打开MyBands.dll"。很明显,只要资源管理器在运行,它就总是保持Band对象处于活动状态。IE也一样:当用户隐藏或显示Band时,IE调用IDockingWindow::HideDW 和ShowDW。IE不会释放这个对象,直到它退出。对于桌面Band来说,这意味着你必须要再一次用Ctrl-Alt-Del来杀掉它,然后重新建立,很讨厌,但那是没有办法的事。
    下面简单说说调试问题,一般外壳扩展的调试可以参照精华区中一篇文章:" 如何调试Windows外壳扩展"所讲的办法(窍门是用Ctrl+Alt+Shift关闭外壳,然后以explorer.exe作为调试进程运行调试器),但是,在调试桌面Band时。这个方法不灵,原因是最后总是锁住DLL。然后又得重启机器,真是糟透了,我真是厌烦 debugging 调试。幸好用TRACE也能跟踪,这是我喜欢的一种调试工具。但偶尔要借助于系统级的调试器。
    当我第一次实现MyBands时,在处理记忆用户选中了哪一个搜索引擎的地方不成功。那是因为我在对象的析构函数中存储相应状态——但不知什么原因析构函数没有被调用,就像我前面说过的,Windows没有销毁对象,它只是关闭了窗口。所以我将存储代码移到PostNcDestroy中才解决了问题。但是,如果即便用户关闭了它,对象仍然存活的话,那就没有必要存储状态设置,因为它们仍然还在内存中。不是吗?
    所有这些小问题都是因为一个大问题导致的,通过TRACE的输出显示:每次你隐藏或显示某个桌面Band时,资源管理器都创建一个新的Band对象。它决不会释放旧的那一个!分析一下就知道了,如果CMyDeskBand是916个字节,并且我有128MB RAM内存,在Windows内存耗尽之前,我能隐藏或显示多少次对象呢?微软已经承认了他们这个愚蠢的错误并答应不久就解决它。
    挑完了毛病,再回到Band的种类问题上,我在开始的时候谈到了有三种类型的Band对象。其实还有第四种:那就是工具栏Band。为了搞清楚这些术语的意思,我的头已经被弄得蒙嚓镲了,工具栏Band位于IE 的Rebar中。IE5.0中的"电台"就是工具栏Band的一个例子。(你不知道IE现在有一个无线电接收装置吗?小意思啦,IE6.0可能会有HDTV呢!)
    编写工具栏Band很容易。只要对info/comm类型的Band进行专门注册就可以了;将Band类ID(GUID)作为串添加到HKEY_LOCAL_MACHINE/Software/ Microsoft/Internet Explorer/Toolbar下面。我已经修改了BandObj.rgs文件来添加这个值,文件名字叫ExplrBar.rgs。然后将这个文件作为资源添加到工程的MyBands.rc文件中。

IDR_COMMBAND REGISTRY DISCARDABLE "ExplrBar.rgs"

    在使用工具栏Band时有一个问题。当你在资源管理器中单击右键显示这个Band的时候,它有一个"电台"菜单项。实际上,它是第一个注册的工具栏Band的名字。这是一个已知的bug——参见微软知识库文章Q231621。Windows 2000好像已经解决了这个问题。

再谈MyBands

  我想,现在你一定有点厌恶Band对象了,并希望它从此从你的生命中消失掉。真是个好主意!对于我来说,反正我已经把所有关于Band对象的东西全都告诉你了。在我走之前,请让我再说说MyBands——实际实现Band的部分。它很简单,但有些东西值得一提。
   首先,CMyCommBand和CMyInfoBand都是从一个公共基类CMyIEBand派生而来的。CMyIEBand实现了一个公共特性:即当你在Band上单击鼠标时,它给浏览器发送VC知识库主页和新浪主页。为了实现这个功能,此Band对象需要IWebBrowser2接口,以便对象能调用IWebBrowser2::Navigate函数,让Web浏览器知道访问什么地方。显然,这需要一点诀窍,所以我首先想到编写:
CComQIPtriwb = m_pSite;     
    它当然要用QueryInterface来查询IWebBrowser2接口,但QueryInterface的调用以E_NOINTERFACE错误和一个空指针而告终。容器不实现IWebBrowser2接口。那么到哪里去获得这个接口呢?请看下面这些你必须掌握的编程代码:
CComQIPtrsp = m_pSite;
m_spWebBrowser2 = NULL;
if (sp) {
sp->QueryService(IID_IWebBrowserApp,
IID_IWebBrowser2, 
(void**)&m_spWebBrowser2);
}      
    IServiceProvider是一个用于COM对象的通用方法,通过它来提供它所知道的由其它对象实现的接口。初看起来它有点像QueryInterface,但又不完全是那样:容器本身不实现IWebBrowser2接口,但是容器知道如何去获得它的一个对象。
    接下来,让我们研究一下CMyDeskBand(Web搜索框)。这里有趣的地方是编辑框控制,它在CEditSearch类中实现,为了在用户按下"Enter"(回车)键后接受到WM_CHAR消息,我添加了一个WM_GETDLGCODE消息处理器,其返回是DLGC_WANTALLKEYS。看看吧,所有Windows3.1的知识在这里仍然有用武之地。当用户按下"Enter"键时,CEditSearch截获它,然后OnChar调用DoSearch函数利用用户的输入信息来建立一个URL。这里需要用"+"来替代空格(spaces),所以如果你用Yahoo搜索"beanie baby sex"的话,则生成的URL将是:http://ink.yahoo.com/bin/query?p=beanie+baby+sex&z= 2&hc=0&hs=0,CEditSearch将传递这个串到IWebBrowser2::Navigate。CEditSearch中内建了几个搜索引擎,但你可以添加更多的搜索引擎进去,方法是编辑MyBands.ini文件。获得这个文件中URL最简单的方法是到你最喜欢的门户站点,用"MYSEARCH"作为搜索关键字,并将结果URL从地址栏贝到MyBands.ini文件中。
    说到 MyBands 所使用的这个INI文件——其实在存储简单的应用程序配置信息时,注册表显得非常笨重,所以我写了一个小类:CIniFile来避免应用程序自动使用注册表。
BOOL CMyBandsDll::InitInstance()
{
// SetRegistryKey(_T("VCKBASE")); // 屏蔽掉注册表!
CIniFile::Use(this, CIniFile::LocalDir); // 使用INI文件!
}      
    它告诉应用程序将INI文件放到程序或DLL所在的目录,不是Windows目录。如果你不调用SetRegistryKey 的话,MFC将默认使用INI文件,但它将这个文件放在Windows目录中。我喜欢将配置文件与使用它们的程序放在同一个目录中,这样你可以删除整个目录而不用关心会有额外的垃圾泛滥。如果你愿意的话,CIniFile可以让你选择使用Windows目录,或者指定一个不同的文件名。方法是将应用程序的m_pszRegistryKey设置为NULL并且将m_pszProfileName设置为INI文件的名字。当然,如果你使用INI文件,你就不能自动获得多个用户使用的机器上由注册表实现的用户指定设置。所以说你看着办吧,是控告我还是用SetRegistryKey。
    正如第三部分的图十二所示,让编辑框的上下文菜单正常工作是要费点事的。当用户在编辑框上单击右键时,编辑框应该显示自己的上下文菜单,这个菜单中应包括"剪切/复制/粘贴"菜单。没问题。只要重载WM_CONTEXTMENU消息处理函数就可以了。但是MFC的命令中没有这样的东西——我是说针对编辑框的WM_CONTEXTMENU消息处理函数,也就是说MFC的OnInitMenuPopup处理器是在CFrameWnd中实现的,而不是在CWnd中——尽管任何窗口都能显示菜单。这个问题是个经常会在编程中碰到的问题。解决它的方法是利用我以前编写的可以用于任何CWnd的一个小玩意儿:它就是CPopupMenuInitHandler,这个类派生于CSubclassWnd 。
    用CSubclassWnd可以动态子类化任何CWnd以实现消息截获。CPopupMenuInitHandler动态子类化编辑控制来截获WM_CONTEXTMENU消息。当截获到这个消息后便调用另一个类/函数——CPopupMenuInit::Init完成工作,这个函数与CFrameWnd中的差不多,几乎是原封不动地拷贝过来的。CPopupMenuInit::Init为每个菜单项创建了一个CCmdUI对象并将它发送到ON_UPDATE_COMMAND_UI处理器。任何时候你都可以象初始化MFC菜单那样单独使用CPopupMenuInitHandler。详细代码请参考MenuInit.cpp文件。
    最终CEditSearch模拟出了IE地址栏中的功能,即第一次点击编辑框时,所有文本被选中。在单击一次可定位光标。实现这些功能必须处理几个消息——WM_MOUSEACTIVATE,WM_ SETFOCUS,WM_LBUTTONDOWN以及鼠标或键盘消息——这个处理过程我都放在了CEditSearch::WindowProc之中。有时侯用老的C++做法比用消息映射来得容易。有关细节请看源代码。
    如果你调试过桌面Band的应用,你就会发现那是件痛苦的事情,我写了一个单独的程序就叫TestEditSrch,专门用来测试CEditSearch,确定它能正常工作后再将它放入桌面Band里面(如图十五)。我强烈建议你也如法炮制。单独写出可执行代码,然后再把它们移到Band中去。

图十五

有关MyBands/BandObj的源代码很长,全都是单调乏味的MFC嵌套类COM代码。有兴趣就去下载源代码看吧!

小结

    可复用编程的实质是确定通用行为并将它封装到可以在不同场合重复使用的类或子例程当中。对于Band对象而言,BandObj将Band对象精炼成四个方面:类ID(GUID),MFC类,资源ID,以及种类ID。加上其它相关的资源。每一种Band对象,其COM接口及其数据之间的交换基本上大同小异。所以为什么还要自己去写全部的代码呢?没有理由不使用BandObj来创建与Band对象有关的应用。所有你要做的只是从框架类派生并调用AddBandClass。某些程序员会反对说BandObj依赖MFC及其庞大的MFC42.DLL。没错,但是我不认为那有什么负担,至少对于外壳扩展是这样。MFC42.DLL被认为是Windows的一部分,它可以用于所有的Windows应用当中。
    如果你看一看CBandObj,就会明白它的通用性所在——不仅仅是针对Band对象,它适用于所有的COM类。CBandObj用CMenu和加速键表实现了IContextMenu和IInputObject。还要求Band对象做些什么呢?它的IOleWindow 和IDockingWindow实现只需要一个CWnd。IObjectWithSite保存指针,就这么简单。与Band对象有关的只有一个接口IDeskBand及其仅有的一个函数GetBandInfo。其它所有的东西都没有什么实质性内容。它尽可能地在更深的层次上抽象这些接口的实现,在小巧的类中使你能轻松组装并一次又一次地在各种情况下重用这些接口。Shell文件夹,文件阅读器,ActiveX控件——所有一切。你只要组装几段代码并编译它就可以建立这些COM对象。这实在是太棒了。
    从下一部分开始,我将利用BandObj来建立自己COMToys,从而向你展示如何使用BandObj。(待续)

这篇关于编写可复用性更好的C++代码——Band对象和COMToys(四)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/605033

相关文章

Java对象和JSON字符串之间的转换方法(全网最清晰)

《Java对象和JSON字符串之间的转换方法(全网最清晰)》:本文主要介绍如何在Java中使用Jackson库将对象转换为JSON字符串,并提供了一个简单的工具类示例,该工具类支持基本的转换功能,... 目录前言1. 引入 Jackson 依赖2. 创建 jsON 工具类3. 使用示例转换 Java 对象为

javaScript在表单提交时获取表单数据的示例代码

《javaScript在表单提交时获取表单数据的示例代码》本文介绍了五种在JavaScript中获取表单数据的方法:使用FormData对象、手动提取表单数据、使用querySelector获取单个字... 方法 1:使用 FormData 对象FormData 是一个方便的内置对象,用于获取表单中的键值

Vue ElementUI中Upload组件批量上传的实现代码

《VueElementUI中Upload组件批量上传的实现代码》ElementUI中Upload组件批量上传通过获取upload组件的DOM、文件、上传地址和数据,封装uploadFiles方法,使... ElementUI中Upload组件如何批量上传首先就是upload组件 <el-upl

C++一个数组赋值给另一个数组方式

《C++一个数组赋值给另一个数组方式》文章介绍了三种在C++中将一个数组赋值给另一个数组的方法:使用循环逐个元素赋值、使用标准库函数std::copy或std::memcpy以及使用标准库容器,每种方... 目录C++一个数组赋值给另一个数组循环遍历赋值使用标准库中的函数 std::copy 或 std::

Java中对象的创建和销毁过程详析

《Java中对象的创建和销毁过程详析》:本文主要介绍Java中对象的创建和销毁过程,对象的创建过程包括类加载检查、内存分配、初始化零值内存、设置对象头和执行init方法,对象的销毁过程由垃圾回收机... 目录前言对象的创建过程1. 类加载检查2China编程. 分配内存3. 初始化零值4. 设置对象头5. 执行

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没