Windows Shell 编程
http://blog.csdn.net/chchzh/article/details/2210729序言:
看过一些对windows 外壳的扩展程序,在使用上一般都是直接利用windows的外壳API做一些工作
,因为外壳操作需要一些比较专业的知识,因此,大部分编程人员特别是使用集成编程环境的程序人员
对windows shell的扩展编程不很了解,也缺乏这方面的资料。
在做过一些程序之后,感觉到使用windows shell的扩展编程可以使程序与操作系统的结合更加
紧密,就好像应用程序已经融入到操作系统之中一样。在使用windows shell扩张编程工具的过程中深切
体会到windows shell技术资料的重要性,为此,根据Dino Esposito的《Visual C++ Windows Shell
Programming》为蓝本,编译形成以下文字,为希望使用windows shell编程的人员提供一点帮助。
引言
欢迎阅读CV++ Windows Shell编程,在这本书中,你将学到怎样扩展Windows Shell,客户化它的行为和
继承应用到Windows Shell中。你还将发现怎样使用和修改它的属性以达到最好的效能,以及调用那些资
料欠缺的Shell API 函数的方法。对Shell编程并不困难,但是很少有关于这方面的完整资料进行介绍。
系统的演变
在Windows 95 和 Windows NT 4.0 中微软通过各种不同的扩展使操作系统的Shell可编程并高度客户化
,Shell和命名空间扩展只是其各种扩展的冰山一角。对Shell编程的一般理解还包括有Shell的API函数
和注册表键等,他们也可以转变你的Win32应用。
集成的Web浏览器已经混淆了本地与远程对象的差别,它在你的桌面上使用相同的风格表示每一个事物。
它可以扩展到包含用户的应用和资料。集成新的Windows Shell部件是这个进程的关键部分。所以,现今
的Shell 不仅仅是COM、扩展和用户接口,还包含了Internet、动态HTML和脚本Windows。Shell已经成为
了包含大量客户端技术的汇集点。每一个开发基于Windows代码的程序员,很快或不久就将需要实际和深
入例程来探索Windows Shell 的内在特征。这本书包含了全部API函数详细说明,揭示其中的Bugs和没有
说明资料的特征。它深入到探测器的内部世界,带出其中闪亮的东西,比如,钩子、注册表、浏览器帮
助对象、Shell 扩展、命名空间扩展和Web观察。他还注意到Windows脚本环境扩展和Shell脚本对象。
这本书是为专业开发者定制的,有三个主要的目标:
为现存的和资料贫乏的Shell API 提供解释
给出新应用概念
显示关于IE4.0、活动桌面和Windows98的新特征和这些新特征怎样适应以存在的Shell。
本书范围
在这本书中我们见致力于回答下面一般的问题
怎样使用注册表来客户化我的Windows Shell
怎样才能建立向“回收站”和“我的公文包”那样的特殊目录
有哪些不同的方法把代码插入到探测器地址空间
怎样处理图标、任务条和“回收站”
什么是Windows脚本环境,我怎样才能使用它
什么是Shell脚本对象和浏览器帮助对象
怎样才能实现Shell对我的应用处理的文档的支持
怎样才能客户化我的文档的关联菜单
怎样在Shell和命名空间扩展中使用COM和ATL来改变Shell的行为
怎样排除Shell扩展中的错误
解释建立成功的Shell集成的应用的原理和所需要的技术
是怎样使用动态HTML客户化一个文件夹的
一些更有趣的例程:
“开始”按钮的子类
建立和安装新的增强型快捷方式处理器
和Windows脚本环境一起工作的新对象
依据控制板尺寸指派不同的图标和Bitmap文件的Shell扩展
探测器中的按键和建立新的文件夹
在探测器中把打开的窗口作为节点显示
技术准备
准备一台运行Windows98、Windows95 或Windows NT 4.0 (带有ServicePack4.0)的计算机就不用说了,
还要特别注意的是有些例程要求你安装IE4.0以上版本。
代码是由VC++6.0和ATL3.0写成的,在CV++5.0中可能会有错误。书中还使用了Windows脚本环境和IE4.01
环境编写了一些示例。
习惯用法
书中使用不同的文字方格和布局来区分不同的信息。下面是一些例子和他们的意义。
框中是重要的说明资料,它们直接向关于上下文内容。
其它信息以文字形式出现如下.
重要文字以黑体形式给出
出现在屏幕山的文字,如菜单选择,与实际屏幕显示的字体相同
键盘上的按键如Crtl和Delete使用斜体
所有文件名都有Pidl.cpp这种风格
函数名则有SHBrowseForFolder()这种风格
新的,重要的和与当前讨论问题相关的代码有如下形式:
void CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)
{
HWND hwndOK = GetDlgItem(g_hwndDlg, IDOK);
// Simulate the Close button being pressed
if(IsWindowEnabled(hwndOK))
PostMessage(g_hwndDlg, WM_COMMAND, IDCANCEL, 0);
}
========
Windows Shell 编程 第一章
http://blog.csdn.net/chchzh/article/details/2211475第一章 Windows Shell是什么
一个操作系统外壳的不错的定义是它是一个系统提供的用户界面,它允许用户执行公共的任务,如访问
文件系统,导出执行程序,改变系统设置等。MS-DOS有一个Command.COM扮演着这个角色。然而Windows
已经有了图形界面环境,他的外壳程序也就必然是图形方式的。在Windows95以前,默认的Windows
Shell就是程序管理器。
程序管理器是一个中央控制台,从那里你可以启动应用程序,重排和重组图标,执行新任务。
换句话说,程序管理器就像他的名字提示的那样管理所有集中在Windows大伞之下的程序。现在对应程序
管理器的是文件管理器,它是一个专门为维护文件系统而设计的工具。
随着Windows95的出现,探测器取代了这两个老工具,并集成了二者的功能,如果你愿意,你
仍能发现文件管理器仍然深深地隐藏在Windows系统目录中。然而,由于用户友善性方面比他的后继者差
,现今已经很少使用了。
一般错误的概念认为,探测器就是一个程序,当你需要通过点击“我的计算机”或右击“开始
”按钮来浏览文件系统时这个程序启动。事实上,探测器总是启动和运行着的,从引导开始一直到你关
闭计算机。直觉是“探测器”实际上就是新概念下的窗口。探测器是一个可执行模块(explorer.exe)
,它实现了Windows外壳功能。
在这一章中,主要是介绍外壳和探测器,更精确地讲是
Shell的组成部分
探测器结构
Shell的组成部分
Shell由许多不同的部分组成,现在我们就从最显而易见的桌面和任务条开始。从概念上讲,桌面是所有Windows Shell对象的父对象,即宿主对象。就实现方式而言,桌面是一个系统定义窗口类的特殊窗口(
命名为#32769),并且是所有窗口的祖先窗口。那些导出应用的顶层窗口一般而言都是桌面的子窗口。
在这些子窗口中有一个有趣的窗口子树,它的根是“程序管理器”。
你可以用CV++带的工具Spy++来检查窗口栈中打开的窗口。
程序管理器保持了兼容性,在图中你可以看到,他的封装结构:程序管理器的直接下级是一个命为
SHELLDLL DefView的窗口类,这个窗口包含了一个默认的观察对象,事实上这个窗口负责枚举标准文件
夹的内容,它总是从ListView控件中导出,这是一个通用的系统控件。SHELLDLL DefView窗口包含了一
个ListView(类名为SysListView32)和一个标题控件(类名为SysHeader32),标题控件仅仅用于ListView
的报告观察。
随着IE4.0的活动桌面和Windows98的发布,默认的观察对象已经有了基于某些浏览器能力的变
化。在下一章中我们将更进一步讨论这些观察对象和他们的变化。
程序管理器
就象前面提到的一样,程序管理器窗口是为了兼容性而保留的。它正好演示了一个窗口应用从16位到32
位的演变过程。在Win16环境中,与Shell唯一的通讯方式是通过动态数据交换(DDE)。这个层面的代码
在Windows95 甚至Windows98 中还在维护。为什么呢?,为了兼容性。
关于DDE接口变成与Shell的详细说明,建议察看资料Internet ClientSDK其中给出了最新的信
息。DDE是一款较老的技术,微软已经有大量的资料说明。
任务条
主要的Windows Shell部件就是任务条,它实际上就是由探测器进程所拥有的一个窗口。每当你需要终止
探测器进程的时候,都将引起任务条的消失和重新显现。每当他重新显现的时候他注册一个具有不同
HWND的新窗口。因而,就引用而言,你没有必要保留这个窗口的HWND 。任务条也是也各拥有“开始”按
钮的窗口,在托盘区域有时钟和类似按钮的控件,表示正在运行的应用。
任务条实际上与窗口一样,可以在其上作任何窗口上可以做的操作如移动、隐藏和子类化等。
在第七章中我们将说明怎样子类化任务条和“开始”按钮。在第九章中可以找到怎样隐藏任务条和编成
重新启动Shell。这后一个特性在编成实现Shell和命名空间扩展时是有用的。
桌面
你是否奇怪,桌面上的快捷方式是从哪里来的和属于谁,坦白地讲,开始我也认为探测器模块绘制了这
些图标,记录了用户设置,颜色,状态等。这个模块开可能在桌面背景上绘制用户设置的墙纸。
桌面并不是这样工作的,相反,快捷方式作为图标是由一个普通的ListView显示的。当然这个
ListView有了一个不寻常的变异,但是它确实是一个ListView,因此它也就不难从消息中获取处理对象
了,将在第九章中给出例程进行说明。
探测器结构
探测器是一个扮演着系统外壳角色的应用程序。当说到外壳扩展的时候,我们讲的就是有探测器感知的、装入的并最终执行的代码块。
探测器可以被看作为一个微型的窗口开发环境。想象一下:它有自己的函数和对话框;允许写
特殊的与已存在的体系集成的应用程序;能包容应用和文档。他甚至可以解释任何活动的脚本兼容语言
的脚本(VBScript,JScript,Perl,等等)。本书中将包含所有这些特征。
扩展的切入点
随Windows3.1一起发布的文件管理器有着非常好的旦未充分利用的特性,比如:它能够在运行时加载DLL
,执行具有特殊原形的已注册函数等。也就是说,在这个资源中有一点,其代码本身能够知晓某些活动
是由用户执行的。换句话说,文件管理器支持扩展行为,当执行特定的活动时,查找注册的扩展并装载
和运行之。
我们后面在探测器外壳和命名空间扩展中见到的实际上有相同的原理。不同的完全是细节方面
的实现。文件管理器用于加载具有预定义原形的传统的DLL全程函数,而探测器是这一过程更加规范。尤
其是它采用COM接口(可以看作预定义和固定函数原型的集合)和进程内服务器(实质上的DLL)
当然,COM接口和进程内服务器比函数集和DLL更复杂,但是,这也使探测器进程比老的基于DLL的进程更
规范和有力。
对探测器的扩展
在探测器环境中,基本上有两种类型的扩展:外壳扩展和命名空间扩展。从名字上看有点混淆
,探测器就是Windows的外壳,所以两种类型的扩展都可以作为外壳扩展。换句话说,外壳和命名空间扩
展二者都扩展了探测器能力。但是在他们之间有一些差别。
外壳扩展是一种加到给定类型的所有文件上的客户行为,给定类型的文件按照客户要求显示在
探测器的观察中。如此,你可以称之为“外壳观察的扩展”。客户的行为,如拖拽,右击关联菜单,绘
制图标或显示属性对话框等由一定数量的特殊事件触发。你可以定义这些事件的特殊处理器程序,例如
你可以确定显示给定.Bmp文件的图标,为所有Windows 元文件加一个预览页面到属性对话框,甚至可以
在关联菜单中增加一个可执行功能。将在十五章中给出例程。
命名空间扩展有两种形式,这要看你怎样连接。如果你用文件类型连接命名空间扩展,尽管有
复杂的代码支持,其功能上仍然等价于关联菜单的扩展。然而,如果你用文件夹连接命名空间扩展,这
个文件夹将变成客户文件夹,你的代码将确定文件夹的内容、探测器显示的图标、子文件夹、排序、关
联菜单等。
为什么要对Shell编成
这个问题很有道理,简单的回答就是,为了使我们的应用根号和更丰满。但是这个回答有点过于辞令化
,我们这样做是为了使我们的模块与系统集成到一起,或者说更自动化。
本书的结构
有两种方法对外壳编程,使用API函数和使用COM接口。这两种方法既不互相独立也不相互重叠
,它们是两个不同的方向和两种不同领域,这一点我们在下一章中将进一步阐述。现在让我们直接浏览
一下个章节的内容。
你知道Shell编程要求使用API函数和COM接口,API函数使你能够访问Shell的基本功能,如,
文件操作,浏览文件夹,执行程序,处理图标和快捷方式等。当你想要增强和精炼Shell的基本行为时,
COM方法则触及到了客户化Shell扩展的核心。
这本书中首先给出所有API函数的解释,进一步探讨函数的原型,资料介绍的差别以及其中的
Bugs。通常我的目的是要澄清所有你在资料中遇到的含混之处。第三章到第九章涉及到特殊的APIs分组
,其中包含了典型的Shell操作,特别在第三章中讲解SHFileOperation()函数,涉及到文件的拷贝、移
动、删除和重命名操作。第四章揭示了神秘的SHGetFileInfo()函数,系统提供了获取文件(属性、图标
、类型和显示名)的系统信息和Shell信息的方法。第五章解释文件夹内部组织的叠放过程,包括设置、
浏览和象Favorites和SendTo那样的特殊文件夹。
快捷方式和属性在第六章中介绍,其中将介绍建立和解析快捷方式和经常使用的字段。第七章则正式进
入探测器地址空间,并且从另一个角度讨论客户化问题:在探测器不可知的情况下什么是你所能安全操
作的。特别是我们向你展示一个置换过的“开始”按钮和不同的菜单。一旦你这样做过之后,你就有了
完全控制Windows系统的能力了。在余下的第八和第九章中我们将讲述程序的扩张,图标和任务条,我们
将说明怎样编程加入新的具有自己的菜单的按钮到任务条中。
这本书的第二部分是基于要求COM接口的探测器特征的。但是直到第十二章之前我们还没有涉及到接口知
识,中间的两章作为Shell函数和探测器接口的桥梁。第十章包含了最近更新的Windows的SDK函数。第十
一章给出了Shell对象的概览,例如“我的公文包”,“控制面板”和“打印机”,以及客户文件夹的概
念。在这一章中还包括了其他Shell对象和RunDLL32使用程序的说明以及全部探测器命令行的解释。
第十二章介绍Shell对象模型,首先致力于把API函数的一个子集移动到对应的COM接口中去,这个特性最
少要求系统中要安装“活动桌面”。有趣的是这个对象模型允许你访问系统的某些功能(绝大多数系统
对话框)。
第十三章介绍Windows脚本环境,这是一个执行Windows批处理文件的运行时引擎。技术上讲,这并不是
一个Shell实体,但是它与Shell有重要的留级关系。Windows脚本环境显露一个对象模型,使你能够使用
VBScript,Jscript等任何脚本语言编写程序。我将通过加入有用的新对象来扩展这个模型。
第十四章集中于指导你采用Shell和命名空间扩展的应用和理由方面。我将揭示实际上的Shell集成的应
用究竟是什么和为什么说Shell扩展是把应用模块与系统Shell融合的最好方法。第十五章说明怎样写一
个Shell扩展来客户化关联菜单、图标和属性,以及怎样排错。第十六章概括了命名空间扩展内容,并且
包含一个例子,说明怎样加一个可展开节点到探测器的树观察中,并以文件夹的形式展示了当前窗口的
堆栈过程。
小结
这一章中我们描绘了未来各章中打算作的事情,尤其是我们试图解释:
Shell的本质和结构
各Shell版本之间的差异
========
Windows Shell 编程 第二章
http://blog.csdn.net/chchzh/article/details/2222622第二章Shell的结构
“Shell 编程”的大伞之下有大量的API函数和COM接口。这个种类繁多的‘命令’集允许你用不同的方
法对Windows Shell进行编程。函数和接口并不是两种提供相同功能的等价途径,相反,它们在不同的逻
辑层上提供不同的功能。
API函数包含了用户想要在Shell对象上执行的基本操作,如文件和文件夹。COM接口则给出了
扩展增强,甚至客户化各种要素对象的机会,包括Shell本身标准行为。用面向任务的方法对函数和对象
进行分组将给我们一个总体上观察Shell的机会,因此,我们仍然能够把它看作一个具有属性和方法的对
象。在这一章中我们将努力分出每一个函数和接口究竟属于哪一个功能组。这将有助于我们从大量的功
能碎片中寻找出Shell编程接口。
在这一章中,将包含有:
我们在这本书中使用的定义
Shell API函数的功能分组
由Shell和其内涵部件实现的COM接口的功能分组
Shell结构是怎样随导入的活动桌面而演化的
最后,你将能更好地理解这本书的知识范围,并且作为Shell程序员,你将能清晰地勾画出书中的哪些功
能是可用的。
Shell模块
实际上到目前为止我们还不能说Windows Shell是面向对象的。有一些‘对象’查看结构就能
识别它。这些对象也有‘属性’一样的特征,以及象方法一样执行一些活动。但是它们是依赖API函数的
,一个典型的对象就是文件夹。
如果Shell不是面向对象的,它决不能有一个完全兼容的对象模型。我们能够想象一个看起来
象分层对象的体系结构。换言之,有一个对象集合,他们以如下图描述的方法一样工作。
基本上讲,Shell是由文件夹组成的,文件夹是一个包含有子元素的容器,包括子文件夹,这些元素通常
称为文件夹项。根文件夹称为‘桌面’,其子项包括‘我的计算机’,‘网上邻居’,‘回收站’,和
其他可能的项,所有这些文件夹的集合称之为Shell的命名空间。
Shell的命名空间
概念上讲,文件夹就类似于文件系统的目录,但是它不一定绑定到一个实际的物理目录上,如
果他没有邦定,就称之为虚拟文件夹。我们可以以这种方式区分两种主要的文件夹:正常的文件夹(命
名为文件型文件夹或目录)和客户文件夹。自然,包含在文件型文件夹中的是文件,其属性是名称、类
型、尺寸、最后修改日期等。包含在任何其他文件夹下的项目可以是文件—一般使用其他的扩展特征集
—但是也可能是完全不同的东西,如打印机或网络节点。
文件夹
文件夹是怎样实现的?文件夹实际上是一个Shell对象,它的行为被编码成一个COM模块,向Windows
Shell暴露公共的接口。通过连接,文件夹可以告诉Shell怎样设计它的内容,使用什么样的图标显示,
采用什么文字来描述,例如‘我的计算机’看起来像一个文件夹。他有一个代码层来感知PC上所有可用
的驱动器,并且为每个驱动器附加一个子树到探测器的观察中。
每一种不同的文件夹都有不同类型的层次代码来提供不同的行为,对于文件型文件夹,行为就
是扫描文件系统,恢复文件和子文件夹,并且通过列表控件显示它们。而打印机文件夹则记录所遇到的
和所安装的打印机,并且为每一个都显示一个图标。你可以设计任何类型的具有任何行为的虚拟文件夹
。文件型文件夹(即,目录)只是其中的一种。
对非文件型文件夹,Shell的资料相对较少,仅在特殊文件夹中有些说明。事实上Windows
Shell默认提供的是客户文件夹,他们与文件型文件夹的差别是:
可以包含文件和其他对象
能够提供对内容的不同观察
可以有选择地不被邦定到物理目录上
是系统定义的一部分,这部分由SDK提供特殊的函数集。
特殊文件夹列表可以在Win32 SDK资料和后面的第五章中找到。就像我原先说过的一样,特殊文件夹是具
有自己COM模块提供行为的特殊类型的文件夹。由于COM模块是新节点被加到Shell命名空间的前提,所以
它就被称之为命名空间的扩展。
特殊文件夹使用户能够经过适当的接口访问系统信息。也就是说,在大多数情况下,这种文件
夹与典型的文件型文件夹提供的内容观察多多少少有些一致的地方。当然,精确的程度依赖于文件夹的
类型。
与普通文件夹一样,特殊文件夹也可以包含文件。然而,通常是以稍微不同的方法表示,显示不同的特
征。因此,特殊文件夹给文件赋予了不同的意义,并且,不把它们当作文件系统的正常实体(如果不是
这样,它就不是特殊的了)。例如‘回收站’含有正常的而隐含的文件,因为这个文件夹要显示当前被
标志为删除的文件列表,所以它把初始位置和删除日期特征显示在前面。
绝大多数(不是全部)特殊文件夹都依附于磁盘上的物理目录,正常情况下这是一个只读的目
录,其内容就是所有需要以最适合的方法显示的信息。
换一个视角,绝大多数特殊文件夹都需要一个目录来存储它们的数据。这个目录可以被分配到
磁盘的任何地方,并且表示为文件夹和Shell支持的链接—这个特殊文件夹在命名空间中的位置。目录的
内容不必显示为文件列表,相反,关联文件夹的代码可用最适合于它的角色的形式解释和显示它。
文件夹这个有着包含任何事物能力的东西导出两个重要概念:文件对象和PIDLs,这些我们将
在后面章节中叙述。
文件对象
文件对象是一个包含在普通文件夹中的项—文件、记录、内存块、连接的设备等。‘文件夹项
’、‘文件夹元素’和‘文件对象’这些术语是等价的。如果文件夹是一个文件型文件夹则文件对象就
是文件。因此这里的‘文件’就比‘文件对象’稍微特殊一点,因为它精确地代表了文件系统中的一个
实体。文件是一个文件对象,但是,文件对象不一定是文件。
有一个敏感的问题出现在一般的文件夹和文件夹项的概念中,在Shell命名空间中我
们怎样才能安全并唯一地区分出其中的项。如果Shell与文件系统一致(就像Windows 3.x一样),则文
件的全名就能极好的保证这种唯一性。不可能有两个文件件具有相同的路径和名称。然而当文件夹比文
件目录变得更普通的的时候,区分其中的项就需要更普通的方法了。
PIDLs
PIDL是一个数据结构,它是唯一地标识包含在文件夹中的项的一种方法。PIDL—标识符列表指
针的缩写(pointer to an identifier list)—是一种比文件全名更通用的方法,它不仅在文件夹内而
且在Shell的整个命名空间中保证了项的唯一性。更重要的是,它能透明地处理文件和文件对象。为了理
解PIDLs的结构和作用,我们来分析一下它的二进制结构并与之所替代的路径名进行比较。
一个文件全名就是一个字符串,是一个具有非常特殊格式的字符串,是一些子串的串联,每一
个子串都在文件系统的层次中表示一个层,有驱动器名,然后是目录名,文件名,最后是扩展名,他们
都由反斜线分隔。你所了解的文件全名就是指向这些相连元素的指针—此时指向的是一个字符串。从概
念上讲,你可以把它看作是一个数组结构,其中的每一个元素都表示了一个路径名元素。
上图说明了路径名和PIDL的关系,同时他也给出了标识符列表在存储器中组织结构的概念。从编程的观
点讲,PIDL是由LPITEMIDLIST类型实现的,它是ITEMIDLIST结构的指针。
typedef struct _ITEMIDLIST
{
SHITEMID mkid;
} ITEMIDLIST, *LPITEMIDLIST;
中间构成路径名各部分的对象映射到PIDL的项目标识符上。它们存在于整个SHITEMID结构中。
typedef struct _SHITEMID
{
USHORT cb;
BYTE abID[1];
} SHITEMID, *LPSHITEMID;
结构的头两个字节指示项目标识符的尺寸—即,相关元素的数据以及用于表示的数据所占用的字节数。
cb值必须包含它自身的尺寸。对应路径名,cb应该是所表示的驱动器或目录的长度加上一个unsigned
short类型变量的长度。随后是这个结构数据的第一个字节。
一定要记住PIDL是一个‘平面’结构,不包含指针。形成PIDL的所有数据必须明显地聚集到一
起,而不是通过指针连接。这就是说,我们不能使用典型的链表结构方案,使一个元素的最后成员指向
链中的下一个元素。还有一点,就像图中所看到的,链表中下一个元素的地址可以通过cb相加到当前
SHITEMID对象计算得出。这是设计规定的,因此要求相连的SHITEMIDs要连续分配空间。
定义PIDLs的构造规则是约定实现文件夹行为的代码。这些代码也应该确定使用什么样的数据
来表示标识符的项。例如,假设想要实现一个文件夹,象文件系统那样显示Windows注册表,‘子文件夹
’应该是注册表键‘文件对象’应该是注册表值。在这种文件夹中表示每一个元素的可能方法应该是使
用相关的键名。这里我们能够看到PIDL是怎样使用与前面图中给出相同的方案格式的。注意
HKEY_CLASSES_ROOT是一个长整型值,所以它有四个字节加两个字节的无符号短整数。
项目链表表示了路径踪迹,从命名空间的根到特定文件夹项。这个标识符链表聚集了链条上的所有元素
,说明了一种通过Shell唯一地标识一个元素的方法。保证两个项目标识符在内存中连续分配是文件夹对
象相关代码的职责。尽管路径名与PIDLs类似,他们并不等价,他们也不能交互使用,他们是不同的数据
结构。
Shell观察
任何文件夹的内容都是通过一个对象调用Shell观察显示在Windows探测器中的。每一个文件夹
都定义了他自己的Shell观察对象,并且所有相关于这个用户接口的任务都指派到这个对象上。对于文件
型文件夹Shell观察对象是用列表观察控件实现,其中的项就是文件和子文件夹名。默认的Shell观察对
象在他被调用处理文件时为每一个文件分配图标、显示名和类型名。
图标有几种方法确定,这依赖于请求文件的性质。一般使用自身定义的图标显示图标文件
(.ico),而程序文件则显示其资源中定义的头一个图标。如果没有定义图标,则显示默认的。对于所
有其他文件,Shell通常采用文件归属类所定义的图标。然而正象下面要揭示的那样,这个行为可以被客
户化。
在整个Shell 环境中,文件都是根据文件扩展名指定的类型分组的,这种根据类型形成的文件
集通常称之为文件类。它与一个图标和一个描述字符串相连,这个字符串显示在Windows探测器观察的详
细信息窗口上的类型列上。然而,要置换它们,指定的文件类就需要在注册表中注册,Shell将从那里读
出类型信息和图标。
一旦定义了文件类,你就可以写代码来影响和修正Shell响应某些发生在特定文件类上事件的
默认行为,这其中就包括绘制文件图标,弹出关联菜单,和显示属性对话框等。通过定义Shell扩展,你
就可以动态地确定这些事件发生时要做些什么。例如,可以在关联菜单中加入新的项,和处理用户的点
击,和动态地确定基于每个文件的图标显示。
钩住Shell
一般情况下,Shell扩展可以看作为钩子,他被设置在整个Shell中。Win32中,钩子是一段由
应用定义的代码,一定事件发生时系统回调这段代码。有许多不同类型的钩子,他们的应用也非常广泛
,有一些仅仅影响安装他们的应用程序,而另一些则影响所有系统中运行的应用。
这其中典型的例子就是键盘钩子,它能够使你在相应消息发送到应用窗口之前得到键盘按下的
信息。其他钩子的例子如鼠标活动(移动,点击),窗口管理(建立,析构,活动),和消息处理。更
多信息请参见Win32 SDK资料。
从程序员的观点看,钩子是一个具有固定的和预定义语法的回调函数,作为回调函数,系统基
于已知的原形调用它。Shell扩展是COM接口,而不是回调函数,但是背后的原理是相同的,二者都允许
你指定某些系统将要执行的代码来处理一些预定义的活动。
这一节特别注意到Windows的钩子。通过设置局部钩子,你仅仅能够捕获相关应用内发生的事
件。但是设置全程钩子将会导致钩住任何运行的应用所发生的事件。设置全程钩子就是说,你的应用定
义了一段代码,它可以被运行中的其他相关进程执行。事实上使用钩子完成Win32的跨进程边界和注入代
码到其他进程地址空间是最容易的方法。它也是能在所有平台上工作的唯一方法。
Shell地址空间
注入代码到关联的另一个进程是重要的,因为,它允许你访问另一进程没有公开的对象,这对
Shell编程尤其重要。当你成功地把代码插入到Shell地址空间后,你就可以查询Shell接口,改变用户接
口,甚至置换‘开始’按钮。
全程钩子是一种使你的代码运行在Shell的地址空间中的方法,但是更有力和更灵活的机理是
提供浏览器帮助对象—一种COM对象,探测器和IE在启动主窗口时自动加载的对象。
Shell内存分配器
在使用Shell时很快你就会接触到内存分配的问题,Shell提供了一个存储分配器,这个封装了
IMalloc接口的服务可用来代替New或GlobalAlloc()。
要获得这个对象的引用,你应该使用SHGetMalloc()。它不是返回一个IMalloc接口的新指针—
由CoGetmalloc()函数返回的—而是由系统Shell对IMalloc对象的一次引用。使用这个指针,你可以安全
的释放由Shell分配的内存,并且使Shell释放这块内存。这可能有点陌生,但是在Shell编程中,这是个
好习惯。
Shell任务条
任务条窗口作为Windows用户接口的一个已知的部件,仅仅是因为它包含了‘开始’按钮。然
而我们之所以称之为‘Windows任务条’,是因为它实际上是一个窗口系列的特例,称之为‘应用桌面工
具条’,最好的例证就是Office97的快捷方式杆。有一个特殊的函数和消息集与桌面工具条相关,然有
趣的是仅有少量函数和消息影响到Windows的任务条。因此,即使资料没有明确地说明,系统任务条和桌
面工具条仍然是不同的对象。
关于任务条的另一个错误观点是它包含了所有运行中应用的按钮,但是有两点原因说明这不是
真的:
不是所有运行着的应用都显示在任务条上
作为按钮,任务条的唯一有的是‘开始’按钮
无论是否相信,作为按钮集出现的实际上是tab控件,只是具有特殊的类按钮风格罢了。
任务条起到了系统控制板的作用,使你能够访问所有运行中的应用。在很多情况下,我们希望
能够限制任务条的功能—这是运行在公共PCs上应用的一个典型的需求,在那里你不希望用户能够运行其
他程序或浏览文件系统。Win32 API并没有提供丰富的函数来操作任务条,但是,我们将试图在第九章中
对此进行一些补救。
Shell API 函数
在与VC++6.0一起提供的MSDN库的Shell参考一节列出了100多函数,然而,其中的大多数都只
涉及非常特殊的领域,有时感觉就象是Windows Shell的边界领域—这里所说的特殊是关于文件分析和屏
幕保护的例程。
在这本书中,你不能找到关于每一个函数的详尽的说明,然而我们可以集中于文件和文件加操
作的核心函数,并试图澄清他们含混不清的资料说明。为了有助于对其进一步分类,我们把它们分作五
个不同的函数组。
组
功能
一般Windows函数
Shell内部函数
任务条函数
文件函数
文件夹函数
涉及到屏幕保护,控制面板脚本程序,联机帮助,以及Shell拖拽(不是OLE拖拽)
访问探测器地址空间的函数,获得Shell存储分配器的函数,导出可执行程序的函数以及感觉用户接口改
变的函数。
涉及到托盘域的函数和与Windows任务条通讯的函数
操作文件的函数,他们执行如‘拷贝’,‘移动’,‘删除’和‘取得信息’等操作的系统活动,和添
加文件到特殊的系统文件夹如‘最近文档’等。
操作文件夹的函数,使用这些函数,你可以浏览文件夹,获得系统文件夹的路径,发现文件夹的设置。
根据这个分组结构,可以看到有几个函数作为Shell编程接口的一部分并没有被显式引用,但是,他们仍
值得出现在这个表中。
组
功能
图标函数
环境函数
Shell 轻量级API函数
从执行文件中抽取图标的函数
处理环境变量的函数
容易地访问注册表的函数,读写注册表函数,处理路径名函数,和处理字符串函数。
特别是,有些操作图标和环境变量的函数在shellapi.h头文件中,是我们在这里说明他们的主
要原因。就像对Shell轻量级API函数一样(在第10章中详细说明),我们说这些函数可以放到任何一类
中,但是,他们对Shell编程而言有特殊的用途。下一节的表中列出和描述以这种分类方式定义的一些函
数。之所以如此,是要你更好地理解函数操作的概念,以及给你提供一个快速查找其中函数的地方。
一般Windows函数
正象标题所提示的那样,这些函数仅仅稍微地涉及到Windows Shell,在绝大多数情况下,他
们都直接来自于Windows 3.x的API—他们仅处理如帮助文件和拖拽等操作,所有这些函数都很好地支持
32位Shell版本。
函数
描述
DragAcceptFiles()
标记允许窗口认可拖拽操作。
DragFinish()
从Shell中释放移动文件名列表所分配的内存
DragQueryFile()
从Shell处理拖拽而分配的内存块中抽取文件名
DragQueryPoint()
获得拖拽发生的点位置
CPlApplet()
控制面板脚本小程序的主程序
GetMenuContextHelpId()
返回关联于给定菜单的帮助ID
GetWindowContextHelpId()
返回关联于给定窗口的帮助ID
SetMenuContextHelpId()
设置关联于给定菜单的帮助ID
SetWindowContextHelpId()
设置关联于给定窗口的帮助ID
WinHelp()
打开帮助文件
ShellAbout()
显示默认和特定客户化的‘关于’信息框
Shell内部函数
这类函数包括Shell底层操作函数,和使你能够进入到Shell的地址空间以及可以从一旁操作它
并获得对其次年初空间访问的函数。
函数
描述
ShellExecute()
在指定的文件上执行特殊操作
ShellExecuteEx()
与上面函数相同,但是有更多的选择
SHChangeNotify()
通过这个函数程序能够让Shell知道什么变化了,以及要求它刷新它所保有的信息
SHGetInstanceExplorer()
返回探测器IUnknown接口指针
SHGetMalloc()
返回一个指向Shell存储分配器的指针
SHLoadInProc()
装载指定的COM对象到探测器地址空间
任务条函数
Windows Shell 并没有定义多少操作任务条的函数,所以,控制任务条经常需要自己做很多工
作,然而,有两个函数与任务条相关:
函数
描述
Shell_NotifyIcon()
显示和管理靠近时钟的托盘区域的图标
SHAppBarMessage()
发送消息到系统的任务条
文件函数
文件是Windows Shell最重要的元素之一,图形环境需要文件有许多不同的特性,因此需要特
殊的函数来处理。注意,在下表的版本列中显示的内容,有些函数是根据最近的Shell版本介绍的。
函数
描述
版本
FindExecutable()
返回指定文件名注册的可执行文件路径
所有版本
SHAddToRecentDocs()
把给定文件的连接加到系统的‘最近文档’文件夹中。
所有版本
SHFileOperation()
用于拷贝、移动、删除或重命名一个或多个文件。
所有版本
SHFreeNameMappings()
释放SHFileOperation()函数在特定情况下返回的存储结构
SHGetFileInfo()
返回给定文件的各种信息块
所有版本
SHGetNewLinkInfo()
建立新的快捷方式名
4.71
文件夹(Folder)函数
就像我们已经讨论的,文件夹比目录更普通一点,它可以包含文件以外的东西,因此文件夹背后的软件
就直接涉及到为其中的每一个项返回一个唯一的标识的问题。在活动桌面下,文件夹也可以有它自己的
图形特征集。
函数
描述
版本
SHBrowseForFolder()
显示选择文件夹的对话框
所有版本
SHEmptyRecycleBin()
销毁‘回收站’的内容
4.71
SHGetDataFromIDList()
从标识符表中恢复数据
所有版本
SHGetDesktopFolder()
返回‘桌面’文件夹的IShellFolder指针
所有版本
SHGetDiskFreeSpace()
返回指定驱动器的磁盘可用空间
4.71
SHGetPathFromIDList()
返回指定标识符列表的路径名(如果存在)
所有版本
SHGetSpecialFolder
Location()
返回特殊的系统文件夹的标识符列表
4.71
SHGetSpecialFolderPath()
返回系统特殊文件夹的路径名(如果存在)
所有版本
SHGetSettings()
返回文件夹当前设置的值
4.71
SHInvokePrinterCommand()
向打印机发送命令
4.71
SHQueryRecycleBin()
返回‘回收站’当前占有的空间
4.71
图标函数
图标是Windows图形环境的中心,操作系统外壳最显著的部分。因此,普遍认为,图标是
Windows Shell编程接口的中心。
函数
描述
ExtractIcon()
返回可执行文件的图标Handle
ExtractIconEx()
与上函数相同,但是有更多的选择。
ExtractAssociatedIcon()
基于文件类,返回指定文件的图标Handle
COM接口
我们可以使用COM接口就象使用API函数那样对Shell作类似的操作。再有,使用与CV++6.0一同
提供的MSDN库做为参考,我们可以将涉及到Shell相关的COM接口分成四类。
组
接口
Shell 扩展
涉及到所有Shell活动的COM接口,从图标到关联菜单,从UI活动到文件观察
Namespace 扩展
涉及到命名空间扩展的COM接口
钩子
能够钩住某些东西的接口,特别是程序执行,URL转换和建立Internet快捷方式
杂项接口
一些零碎接口,如客户化任务条的接口,与打开对话框通讯的接口和对‘我的公文包’编程的接口
对开发者,这些接口并不总是必须实现的—在某些情况下,紧紧需要知道它们,能够适当地调
用它们的方法就足够了。下面就更详细点介绍它们。
Shell接口
在开始,我们展示所有COM接口,然后利用它们在Shell及其扩展上做一点文章。
接口
描述
版本
IFileViewer,
IFileViewerSite
使你能定义对给定类型的文件提供‘快速观察’处理器的模块。
所有版本
IInputObject,
IInputObjectSite
这两个接口用于处理UI活动和对具有接收用户输入的Shell对象进行加速操作处理。
4.71
IShellIconOverlay,
IShellIconOverlayIdentifier
用于发送文件图标重叠消息,使你能够知道用于给定文件的重叠形式。一个图标重叠是Shell绘制在图标
上的Bitmap图像,以便更好地表现它,如,一个手形重叠表示文件夹的共享。
4.71
IContextMenu,
IContextMenu2
允许为特殊类型的文件添加新的关联菜单项。
IContextMenu2处理自绘菜单
所有版本
IContextMenu3
与IContextMenu2相同,但是给出了更好的键盘控制。
4.71
IShellExtInit
执行一个Shell扩展的初始化
所有版本
IShellChangeNotify
SHChangeNotify() API函数在Shell扩展上的副本,基本上,它允许你写一个模块钩住由
SHChangeNotify()函数通知的Shell层上的变化。
4.71
IExtractIcon
允许你获取任何文件夹项的图标信息。
所有版本
IShellIcon
提供另一种获取任何文件夹项图标信息的方法,在特定情况下,这种方法优于IExtractIcon方法。
所有版本
IShellLink
允许建立和解析文件和文件夹的快捷方式
所有版本
IShellPropSheetExt
用于为指定文件类增加属性页到‘属性’对话框。
所有版本
命名空间接口
要写一个命名空间扩展,本身就需要熟知大量的COM接口。这里仅列出最重要的和必须的一些
。
接口
描述
版本
IShellView,
IShellView2
用于定义命名空间扩展的观察对象。IShellView2仍然没有文档资料,但是在基于Web的观察中有使用。
所有版本
IShellBrowser
显示浏览器,他就是探测器或Internet探测器。
所有版本
IEnumIDList
提供Shell 枚举文件夹内容的方法。
所有版本
IShellFolder
提供令shell以标准方式处理客户文件夹的方法。IShellFolder对探测器隐藏客户代码。
所有版本
IPersistFolder
使你能初始化某些Shell扩展和所有命名空间扩展。
所有版本
IPersistFolder2
与上相同,加入了一些对基于Web的观察更强的支持。
4.71
IQueryInfo Retrieves flags and infotip text for items in a folder. 4.71
恢复文件夹项的标志和信息标签文字。
4.71
钩子接口
Windows Shell给我们的模块一个机会来感觉一定数量的事件,并使我们可以把客户代码加入
其中。
接口
描述
版本
ICopyHook
能钩住Shell中的所有文件操作(拷贝、移动、删除、重命名)。
所有版本
IURLSearchHook
使你能够探知探测器正在试图转换一个不可知的URL协议。
4.71
INewShortcutHook
使你能够探知探测器正在试图建立新的Internet快捷方式。
4.71
IShellExecuteHook
能够钩住通过ShellExecute()或ShellExecuteEx()导出的所有新进程的启动。
所有版本
杂项接口
覆盖Shell编程特殊领域的其它接口统称为杂项接口,如:‘我的公文包’,通用对话框,和
任务条等。
接口
描述
版本
INotifyReplica,
IReconcilableObject,
IReconcileInitiator
所有这些接口都涉及到文件调整过程。最终都产生同一个文档的更新版本。
所有版本
ICommDlgBrowser
当客户文件夹嵌入到通用对话框中时,提供特殊的浏览行为。
所有版本
ITaskbarList
允许在系统任务条中加入新的按钮。
4.71
为什么又有API,又有COM
现在我们已经看到了Windows Shell所有的功能,需要花费一点时间才能给出API函数和COM接
口的作用。本质上,整个Shell功能可以划分成两个领域,基本功能和扩展功能,从这个观点分析,就很
容易区分哪一种方法属于哪一个领域的了。
现在,大多数由API调用提供的功能可以看作调用“Shell”的伪对象的‘方法’。这个伪对象
允许你移动或拷贝文件,或浏览文件夹。你也可以恢复给定文档的信息,等等。对象模型的头一个特性
就是从描述它本身开始的。
换句话说,Windows初始是由纯C设计的,从没有被考虑过以面向对象的概念进行设计。因此,
所有的基本功能都通过直接的API调用给出也就不奇怪了。
COM技术允许写出部件模块,然后通过选择暴露它的接口来使用它们。使用接口很容易聚集相
关函数并提供对给定对象的访问。站在Shell的立场上看,COM接口就是封装的API调用—你可以在
ITastbarList接口中看到,这是头一个COM而不是API调用暴露的系统部件编程接口的例子。
这种模式的另一个例子是我们上面提到的钩子接口。在Win32 SDK中全部钩子都是通过回调函
数而不是COM接口编程的。换句话说,Shell编程接口包含有钩子,这就要求你编写并适当注册一个COM服
务器来实现。实际上,差别不是很大,但在体系上,他们就不同了。
有一股变革之风从Windows Shell吹来,COM就是他的源泉。在已经提到的例子中,可以看到,
所有COM接口都被用于扩展探测器的行为。由于探测器需要设计进程内服务器,因此,他们的技术是平行
的,API调用和COM接口技术同样重要。它们可以被看作为一个硬币的两面(这个硬币就是Shell),但是它
们确实是有差别的。
活动桌面有什么变化
活动桌面外壳的更新带来一些新的特征,并且使Windows Shell产生了几个方面的变化。它在
任何可能的地方都鼓励使用HTML,引进了Web观察的概念,文件夹的客户化,脚本能力,简化而有效的对
象模型,以及大把的新函数和COM接口。
上面列出的最后一项应特别引起注意,例如,我们现在有了一个非常原始的Shell对象模型,
通过COM,暴露了一些Shell的功能。在大脑中记住这些对于程序员来说是重要的。到目前为止,这个模
型还不完善,没有你所期望的灵活性,但这是重要的第一步。
抛开Shell API的变化不谈,活动桌面表现了桌面结构和文件夹的值得注意的演化。特别是:
Shell观察对象
任务条结构
此外还有Shell观察对象的增强,以此我们可以在文件夹层上执行脚本代码,以及使用动态HTML和脚本程
序。
新的Shell观察对象
最初,Shell观察对象是通过窗口栈顶的SHELLDLL_DefView类来实现和表示的。在第一章中你
也已经看到了:
这个截图显示了桌面的观察对象,然而,实际上它对任何文件夹都是一样的。例如下面的图像显示了‘
我的计算机’文件夹的窗口堆栈情况:
这里所看到的绝大多数窗口一起合作形成文件夹窗口的框架(窗框,组合框,工具条等)。提供显示实
际文件夹内容(即Shell观察对象)的总是窗口的SHELLDLL_DefView类与他的后代,列表观察。然而对活
动桌面,有另外一种观察对象,这个对象还包含有对HTML和脚本的支持,称之为Web观察,并且可以使用
文件夹的‘观察|作为Web页’菜单功能打开和关闭。下面就是在Web观察打开时,‘我的计算机’窗口所
看到的。
文件夹的内容以基于HTML的模版方式显示,其中的列表观察包容了文件对象详细信息的控件。
对应的窗口堆栈为:
应立即注意到的最大差别就是窗口类Internet Explorer_Server,它有一个子窗口类Shell Embedding,
所有这些形成了通过WebBrowser控件显示输出的窗口,而Shell Embedding则是一个封装了文件列表控件
的列表观察窗口。
WebBrowser是一个IE3.0以上版本使用的ActiveX控件,用以显示他们的内容:HTML文件,GIF
和JPEG图像,和活动文档。
概括地讲,如果Web观察打开,则
文件夹看上去是由WebBrowser控件显示的HTML页面
HTML页面从HTML模版生成,它在必要时可以被客户化。
包含文件的列表观察被嵌入到ActiveX控件中,并一起并入到HTML页面中。
Web观察也可以在客户文件夹上打开,但是,这种情况下,封装文件夹的命名空间扩展就需要实现特殊附
加的接口。
在桌面上事情也是一样的,你可以通过关联菜单打开和关闭Web观察:
当这个观察活动时,桌面的观察对象也使用WebBrowser控件显示桌面内容。桌面的图标在不同的比背景
更高层上绘制,尽管这种‘图标层’在以前的活动桌面上也存在,Web观察还是加入了一些HTML‘墙纸’
的东西,其内容总是显示在图标的下面。
客户化文件加
当Web观察打开的时候,你所访问的文件夹使用HTML模版显示。有一个标准文件夹模版文件
Folder.htt存储在Windows的Web子目录下,在没有指定其他的模板之前,它是默认的。如果想要学习它
的源码,要注意,他是一个隐藏文件,所以,在打开‘显示所有文件’的设置之前,你不能看到它。
通过右击文件夹,打开一个菜单,如图:
选择‘客户化文件夹’… 允许你直接运行编辑大师编辑folder.htt文件的内容。更精确地说,你实际所
编辑的就是在指定文件夹中由编辑大师最初建立的模版。只要你需要,完全可以通过简单地编辑这个
HTML文件改变文件夹的外观。尽管这个.htt扩展是一个完美的HTML文件,只要你想,你还可以删除或置
换这个文件的列表控件,仅仅显示你想要用户看到的信息。
由于文件夹模版是一个通过IE的WebBrowser可观察的HTML文件,所以,你可以采用所有XML的
特征,数据绑定,动态HTML,和脚本功能,以及改变一个简单的文件夹使其看起来像一个应用程序。这
样的客户化也相当类似于原始的命名空间扩展。
新任务条的布局
与观察对象的改变一样,活动桌面的任务条布局也发生了一定的变化。下图中给出了预期的概
念,以及新老结构的比较:
小结
这一章,我们讨论了:
从功能上对Windows Shell的API函数和COM接口进行分组
这本书的章节布局
概述了Shell结构及其对象
随着讨论的深入,我们总是试图使Shell编程接口的结构清晰印在你的脑海中,因而,在下一章中,将包
含那些最关键的API函数的详细说明。描述代码主要使用C++ 调用SDK函数。
然后,我们将开始向Shell和命名空间扩展靠近,观察一些有用的COM接口,以便使用这种方法
钩住和对其编程,以及测试这个初始的Shell对象模型。仍然有很长的路要走,到目前为止我们甚至还没
有看到任何代码。这些都需要花费我们的一定的时间。
========
Windows Shell 编程 第三章
http://blog.csdn.net/chchzh/article/details/2233634第三章 操作文件
我依然清楚地记得,Windows95 的贝塔版出现的情形,它在朋友之间和学院中传播,好酷,全
新的文件管理器,一种全图标,全彩色可客户化的界面,以及活泼的动画标识使得在文件拷贝和删除方
面的操作更容易和直观。
作为真正的软件狂人,我们能为一个比萨饼的奖金开始竞赛,一直以求成为第一个能够编程再
造如此行为的人—即,怎样以动画方式拷贝文件。花了几个小时的时间才在一大堆新函数中找出了
SHFileOperation()函数,这是一个响应动画拷贝的API函数,它也是探测器执行所有文件操作的函数。
竞赛的规则之一是建立一个具有这个唯一目标功能的演示程序。在这个函数出现之后,这个问题实
际上是十分简单的。事实上,当我确定在程序中使用这个函数作为标准函数来进行文件操作时,问题就
出现了。要这样做,你就必须彻底弄清楚这个函数的原型和它的能力,实际有趣的故事从这里就开始了
。
在这一章中,我打算向你展示SHFileOperation()的内部奥秘。
怎样正确地使用函数所支持的标志和命令
怎样正确使用源/目缓冲区
最有可能的返回码是什么
对于长文件名,可能遇到的问题
关于文件名映射,以前未暴露的问题
与这本书的其它任何地方一样,在这一章中,你将发现一些有帮助的函数,它们推动你使用Windows的通
用控件,对话框。
SHFileOperation()能做些什么
要得到这个问题的答案,先让我们先来看一下在文件shellapi.h中SHFileOperation()函数的
声明:
int WINAPI SHFileOperation(LPSHFILEOPSTRUCT lpFileOp);
进一步,看一看SHFILEOPSTRUCT结构,这也是一个在shellapi.h中定义的结构。
typedef struct _SHFILEOPSTRUCT
{
HWND hwnd;
UINT wFunc;
LPCSTR pFrom;
LPCSTR pTo;
FILEOP_FLAGS fFlags;
BOOL fAnyOperationsAborted;
LPVOID hNameMappings;
LPCSTR lpszProgressTitle;
} SHFILEOPSTRUCT, FAR* LPSHFILEOPSTRUCT;
通过这个结构,SHFileOperation()函数可以做任何想要做的操作。简要地说,这个函数可以做:
把一个或多个文件从源路径拷贝到目路经
删除一个或多个文件,把它们发送到‘回收站’
重命名文件
把一个或多个文件从源路径移动到目路径
到目前为止,我们没有看到任何新东西—至少没有特别刺激的东西。事实上,Win32 API(和C运行库)已
经提供了做同样事情的方法。特别是Win32 API提供了 CopyFile(), DeleteFile(), 和MoveFile()来执
行这些任务。
然而,强大的SHFileOperation()函数的出现,使你能够仅仅使用一个命令就可以处理对缺省目录的多重
拷贝和建立。他还支持‘Undo’操作,以及在目标名冲突的情况下自动重命名操作。最后,他还大方地
提供了一个空白纸页一个从文件夹漂动到另一个文件夹显示的动画。
毋庸置疑,你可以从Win32的底层APIs获得同样的功能,但是这可能需要做大量的工作。
SHFileOperation()函数怎样工作
与所有仅使用数据结构作为输入参数的函数一样,SHFileOperation()函数是一个相当灵活的
例程。通过以适当的方式组合各种标志,和使用(或不使用)各个SHFILEOPSTRUCT结构的成员,它可以执
行许多操作。 下面就让我们来看一看这个结构中每一个成员所起的的作用:
名
描述
Hwnd
由这个函数生成的所有对话框的父窗口Handle。
wFunc
表示要执行的操作
pFrom
含有源文件名的缓冲
pTo
含有目标文件名的缓冲(不考虑删除的情况)
fFlags
能够影响操作的标志
fAnyOperationsAborted
包含TRUE或FALSE的返回值。它依赖于是否在操作完成之前用户取消了操作。通过检测这个成员,你就可
以确定操作是正常完成了还是被手动中断了。
hNameMappings
资料描述它为包含SHNAMEMAPPING结构数组的文件名映射对象的Handle。
lpszProgressTitle
一个在一定情况下用于显示对话框标题的字符串。
简言之,有四个成员确实需要进一步研究,它们是:
wFunc (间接地包括pFrom和pTo)
fFlags
hNameMappings
lpszProgressTitle
可用的操作
wFunc成员指定了在给定文件上操作,这些文件由pFrom和pTo给出。wFunc的可能取值(在
shellapi.h定义)是:
代码
值
描述
FO_MOVE
0x0001
所有在pFrom中指定的文件都被移动到pTo指定的位置,pTo必须是一个目录名。
FO_COPY
0x0002
所有在pFrom中指定的文件都被拷贝到pTo指定的位置,其内容可以是目录名或甚至是一个与pFrom 1:1
对应的文件集。
FO_DELETE
0x0003
所有在pFrom中指定的文件都被发送到‘回收站’,pTo被忽略。
FO_RENAME
0x0004
所有在pFrom中指定的文件都重新命名为pTo中指定的名字,在pFrom和pTo之间,名字不需1:1对应。
pFrom和pTo都是包含一个或多个文件名的缓冲。如果包含了多于一个的文件名,则各个文件名之间就需
要用NULL(字符/0)进行分隔,并且整个串需要用两个NULL(/0/0)字符结束,无论有多少文件名。
如果pFrom和pTo不包含目录信息(即,它们不是全路径名),则,函数假设它应该使用由
GetCurrentDirectory()函数返回的驱动器和目录。pFrom可以包含通配符,也可以是“*.*”这样的字符
串。
设置SHFILEOPSTRUCT结构的fFlags成员标志能够影响所有这些操作。在线资料中按字符顺序列
出了所有标志。在我们的简短讨论中,将采取稍微不同的方法,将标志根据它能影响的实际操作分组,
如果你想要自然排列的表,请引用在线资料。
注意两个空的结尾符(/0/0)
其实,就pFrom和pTo是指向一个字符串列表的指针而不是通常意义的缓冲这样一个事实而言,
资料的说明并不充分。也就是说,SHFileOperation()总是期望传送来的串由两个NULL字符终止,即使你
传送的只有单个文件名或使用通配符的单个串也是如此。如果不使用两个NULL字符来终止pFrom和PTo中
的字符串,则可能的情况就是函数在分析传来的内容时失败。此时,它返回一个‘不能拷贝/移动文件’
错(错误码 1026)。没有两个NULL字符,函数可能会把字符串尾,单个NULL字符后的字节作为被拷贝或移
动的文件名。这些字节可以是任何东西,可能不是合法的文件名,因此错误就出现了。由于pFrom总是被
解释为文件名列表,而pTo只有在FOF_MULTIDESTFILES标志下才被解释为文件名列表,所以这个错误常常
伴随pFrom一同出现。在所有其它情况,SHFileOperation()都假设pTo引用单个文件名。因此单个NULL字
符终止是充分的—两个NULL终止仅仅在终止包含多个文件名的列表时被要求。除非明确说明有多个目标
文件,对pTo内容的解析停止于头一个NULL终止符。
解析方法依赖于指针是否引用了字符串列表或简单缓冲,为安全起见,你应该总附加一个终止符到
你打算赋值给pFrom的字符串后面,同样,对pTo,如果有多个目的文件的话,也是如此。字面上,你可
以显式加一个/0在串的结尾(当然,字符串自动终止在单个NULL字符上):
shfo.pFrom = "c://demo//one.txt/0c://demo//two.txt/0";
如果使用变量,可以采用下面的方法:
pszFrom[lstrlen(pszFrom) + 1] = 0;
移动和拷贝文件
要把文件从一个位置移动或拷贝到另一个位置,需要指定:
包含源文件名的缓冲。可以是一个名字序列,单个名字,一个包含通配符的
串,甚至可以是含通配符的串序列。
一个目的目录。如果你移动一个确定的文件列表,还要准备一个目标名列表,
注意保证1:1的与源名对应。换句话说,每一个源文件名都
必须有一个目标文件名以便移动或拷贝。如果有多个目标文
件,就必须在fFlags中指定FOF_MULTIDESTFILES标志。
这个标志可以影响的操作是:
标志
值
描述
FOF_MULTIDESTFILES
0x0001
pTo成员包含多个与源文件对应的目标文件。
FOF_SILENT
0x0004
发生的操作不需要返回到用户,就是说,不显示进度条对话框,而其它相关的消息框仍然显示。
FOF_RENAMEONCOLLISION
0x0008
如果目标位置已经包含了与打算移动或拷贝的文件重名的文件,这个标志指示要自动地改变目标文件。
FOF_NOCONFIRMATION
0x0010
这个标志使函数对任何消息框的回答总是Yes,只有一个例外,就是当询问是否建立缺省目录的对话框显
示时。此时,需要FOF_NOCONFIRMMKDIR标志帮忙。(参考后面的说明)。
FOF_FILESONLY
0x0080
这个标志仅仅应用于指定了包含子目录和通配符(*.*)的情况。设置了这个标志,函数仅仅处理文件而不
进入到子目录。
FOF_SIMPLEPROGRESS
0x0100
这个标志产生一个简化的用户界面:有一个动画窗口,但是不显示文件名,而是显示通过
lpszProgressTitle 成员指定的文字。
FOF_NOCONFIRMMKDIR
0x0200
如果目标目录不存在,这个标志使函数默默地建立一个缺省目录。没有这个标志,函数将提示是否建立
一个完整的目的路径。这个标志与下一个将要介绍的标志有点微妙的关系。
FOF_NOERRORUI
0x0400
如果设置了这个标志,发生的任何错误都不会引起消息框的显示,全部都返回错误码。这个标志与上一
个标志关系有点微妙。
FOF_NOCOPYSECURITYATTRIBS
0x0800
应用于WindowsNT,Shell4.71(WindowsNT具有IE4.0 和活动桌面),和更高版本。这个标志防止对具有安
全属性的文件进行拷贝。
现在让给我们更详细地了解一下这些选择,在移动或拷贝文件的时候,所关心的有两个主要方面:正确
地标识要传送的文件,和确保所设置的标志产生所希望的行为。
避免不想要的对话框
如果你希望操作默默地进行,不需要显示对话框或系统错误消息,你可能认为FOF_NOERRORUI
| FOF_SILENT标志的组合是一个好的选择。然而,这并不是真的,正象我所提到的,使用FOF_NOERRORUI
仅仅能隐藏错误引发的消息框。另一方面,FOF_SILENT标志自己不能防止这个函数显示所有可能的消息
框。事实上,FOF_SILENT仅仅影响到进度条对话框—即,显示被拷贝或移动的文件名,伴随一个通常的
动画对话框。如果函数发现给定的文件或目录在目标位置已经存在,它将总是显示提示。要避免这个行
为,你就需要把FOF_NOCONFIRMATION设置加到标志中。这将使函数在每一步都采用一个不可见的Yes点击
行为。然而这个故事远没有结束。
如果目标路径包含了缺省目录,所有这些标志都无效。在继续执行文件的拷贝或移动之前,这
个函数试图保证目标目录的存在,你可能已经合理地指定了一个不存在的目录,这个函数将小心地建立
它,但是,它首先要求一个显式的认可。
要跳过这个对话框,需要设置标志FOF_NOCONFIRMMKDIR。如果设置了这个位,函数就自动建立任何缺省
的目录而不显示提示框。
概括地说,如果想完成拷贝(或移动)操作而不需要用户的干涉,你可以使用如下的标志组合
设置SHFILEOPSTRUCT 结构的fFlags成员:
FOF_SILENT
FOF_NOCONFIRMATION
FOF_NOERRORUI
FOF_NOCONFIRMMKDIR
然而,关于同时使用FOF_NOERRORUI和FOF_NOCONFIRMMKDIR标志组合,仍然有一点是需要澄清的。
缺省目录
有趣的是,一个缺省目录可以看作是一个由系统对话框弹出的系统错。尽管你可以通过设置
FOF_NOCONFIRMMKDIR标志跳过这个对话框,但是FOF_NOERRORUI标志优先于FOF_NOCONFIMMKDIR,有效地
抑制了对话框,使后面所涉及到它的标志不被选择。如果这两个标志都被指定,你既不能被提示授权建
立不存在的目录,也不能自动建立目录,相反,这个函数继续执行就象拒绝建立目录一样,并将返回:
错误码117
取消标志fAnyOperationsAborted设置到True
不产生文件的移动或拷贝
这是否是说,要避免使用FOF_NOERRORUI标志呢?当然,如果你想要绝对静默的操作,就不可避免地要使
用它—以防止所有错误消息框显示。问题是它也阻止了新目录默认地建立,并且产生一个无谓而又麻烦
的错误。幸运地是,有一种方法能够绕过它,即,在使用这个标志调用SHFileOperation()前,确保pTo
中存储的是已存在的全路径名。Win32提供了一个实现这个目的的函数:
BOOL MakeSureDirectoryPathExists(LPCSTR DirPath);
使用这个函数需要 #include imagehlp.h 文件,和连接imagehlp.lib库。
文件重命名
SHFileOperation()函数在置换已存在文件时能够引起的问题之一是:
或类似地,它引起的已存在目录的问题:
通过设置FOF_NOCONFIRMATION,可以隐含地允许函数置换老对象,但是第二种可能出现了。你知道,如
果在Windows探测器中选择文件,并按Ctrl-C键,然后按Ctrl-V键,在同一个文件夹下将出现一个新文件
,这个文件具有同拷贝Xxxx相似的文件名,此处Xxxx就是你选择的文件。探测器自动重命名了这个新文
件以避免冲突。只要设置了FOF_RENAMEONCOLLISION标志,SHFileOperation()函数也能提供这个功能。
FOF_RENAMEONCOLLISION和FOF_NOCONFIRMATION标志组合禁止了置换操作时的确认对话框。然而接下来,
你的文件或目录将不可避免地被覆盖。如果不合理的情况下指定这两个标志,则FOF_RENAMEONCOLLISION
标志优先
标志间的关系
到目前为止,在你的脑海中应该有两个问题,一是各个标志之间究竟是什么样的关系,其次是
哪些标志影响哪类对话框。下表给出了问题的答案。
标志
抑制的对话框
相关性与优先级
FOF_MULTIDESTFILES
None
None
FOF_FILESONLY
None
None
FOF_SILENT
如果设置,进度对话框不显示。
优先于FOF_SIMPLEPROGRESS标志。
FOF_SIMPLEPROGRESS
None
为FOF_SILENT标志所抑制。
FOF_RENAMEONCOLLISION
如果设置了这个标志,当被移动或拷贝的文件与已存在文件同名时置换对话框不会出现。
名字冲突时,如果FOF_NOCONFIRMATION标志设置,则操作继续。
如果二者都设置了,则它优先于FOF_NOCONFIRMATION。即,文件以给定的新名字复制,而不是覆盖。
FOF_NOCONFIRMATION
如果设置,确认对话框在任何情况下都不出现。
名字冲突时,引起文件覆盖,除非设置了FOF_RENAMEONCOLLISION标志。
FOF_NOCONFIRMMKDIR
抑制请求建立新文件夹的对话框
缺省目录作为严重错误产生一个错误消息框。
建立目录的确认对话框作为错误消息框是否显示依赖于FOF_NOERRORUI的设置。
FOF_NOERRORUI
抑制所有错误消息框。
优先于前一个标志。如果设置,则,缺省目录引起不被处理的异常,并且返回错误码。
一个例程
为了有助于理解SHFileOperation()函数的特性,我们给出了一个简单的综合例子程序,称之
为SHMove。使用VC++ 建立基于对话框的应用,下面是需要建立的用户界面:
你可以在OnInitDialog()函数中看到默认的设置。这个函数在SHMove.cpp中声明。
void OnInitDialog(HWND hDlg)
{
// Set the icons (T/F as to Large/Small icon)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
// Initialize the 'to' and 'from' edit fields
SetDlgItemText(hDlg, IDC_TO, "c://NewDir");
SetDlgItemText(hDlg, IDC_FROM, "c://demo//*.*");
// Take care of the 'progress' title
SetDlgItemText(hDlg, IDC_PROGRESSTITLE, "This is a string");
// Select the default operation
CheckRadioButton(hDlg, IDC_COPY, IDC_MOVE, IDC_COPY);
}
要使这个对话框引起对SHFileOperation()的调用,需要实现点击SHFileOperation 按钮的
OnOK()函数的功能。pTo和pFrom成员的内容以及相关的FOF_ 标志在这个函数中设置。
void OnOK(HWND hDlg)
{
SHFILEOPSTRUCT shfo;
WORD wFunc;
TCHAR pszTo[1024] = {0};
TCHAR pszFrom[1024] = {0};
TCHAR pszTitle[MAX_PATH] = {0};
// 设置要执行的操作
if(IsDlgButtonChecked(hDlg, IDC_COPY))
wFunc = FO_COPY;
else
wFunc = FO_MOVE;
// 取得进度条文字串
GetDlgItemText(hDlg, IDC_PROGRESSTITLE, pszTitle, MAX_PATH);
// 取得from 缓冲
GetDlgItemText(hDlg, IDC_FROM, pszFrom, MAX_PATH);
pszFrom[lstrlen(pszFrom) + 1] = 0;
// 取得To缓冲
GetDlgItemText(hDlg, IDC_TO, pszTo, MAX_PATH);
// 取得标志
WORD wFlags = 0;
if(IsDlgButtonChecked(hDlg, IDC_FOFSILENT))
wFlags |= FOF_SILENT;
if(IsDlgButtonChecked(hDlg, IDC_FOFNOERRORUI))
wFlags |= FOF_NOERRORUI;
if(IsDlgButtonChecked(hDlg, IDC_FOFNOCONFIRMATION))
wFlags |= FOF_NOCONFIRMATION;
if(IsDlgButtonChecked(hDlg, IDC_FOFNOCONFIRMMKDIR))
wFlags |= FOF_NOCONFIRMMKDIR;
if(IsDlgButtonChecked(hDlg, IDC_FOFSIMPLEPROGRESS))
wFlags |= FOF_SIMPLEPROGRESS;
if(IsDlgButtonChecked(hDlg, IDC_FOFRENAMEONCOLLISION))
wFlags |= FOF_RENAMEONCOLLISION;
if(IsDlgButtonChecked(hDlg, IDC_FOFFILESONLY))
wFlags |= FOF_FILESONLY;
// 调用SHFileOperation()函数
ZeroMemory(&shfo, sizeof(SHFILEOPSTRUCT));
shfo.hwnd = hDlg;
shfo.wFunc = wFunc;
shfo.lpszProgressTitle = pszTitle;
shfo.fFlags = static_cast<FILEOP_FLAGS>(wFlags);
shfo.pTo = pszTo;
shfo.pFrom = pszFrom;
int iRC = SHFileOperation(&shfo);
if(shfo.fAnyOperationsAborted)
{
Msg("Aborted!");
return;
}
// 显示操作结果
SPB_SystemMessage(iRC);
}
这个函数从对话框的控件中取得了所有它所需要的数据,然后填入SHFILEOPSTRUCT结构中。如果操作失
败,fAnyOperationsAborted成员被填入TRUE。在上面的代码中有两个陌生的函数Msg()和
SPB_SystemMessage(),这两个函数其实就是MessageBox()的包装变种,你可以自己写一个这样的变种函
数来跟踪SHFileOperation()函数实际返回的信息。现在我们集中精力于源/目缓冲,把#include
resource.h 加到SHMove.cpp中,并且建立一个工程(project)。
源与目
在把文件从源位置移动或拷贝到目位置时,有下列几种可能:
一组文件到单一文件夹
众多单个文件到单一文件夹
单一文件到单一文件夹
众多单一文件到众多单一文件夹
上述的‘单一文件’意思是说一个全路径文件—即,一个具有完整名的文件。对应的‘组文件’则是包
含通过通配符标识的文件,这些文件是不知名的文件。仅仅在上述的第四种情况,才需要使用
FOF_MULTIDESTFILES标志。
上述代码在的默认情况时给pFrom赋予带有通配符的串,例如:c:/demo/*.* ,在这种情况下
,你必须指定一个目的文件夹。通过pTo缓冲传递的任何东西都被作为文件夹名,除非其中包含了不合法
的字符。如此,将得到错误(在第一个文件拷贝或移动时),就如下面显示的那样。
前面解释过,可以通过传递两个NULL终止的多重文件名串(每一项由单个NULL分隔)来操作多重文件,例
如,可以把如下编码写到OnOK()中:
shfo.pFrom = "c://demo//one.txt/0c://two.txt/0c://three.txt/0";
shfo.pTo = "c://NewDir";
这里我们努力想要一次拷贝/移动三个文件:one.txt, two.txt,和three.txt。所有这三个文件都将被拷
贝到根C:/下的目录NewDir中。第一个源文件的位置在c:/demo目录下,其他两个在c:/下。
如果pFrom缓冲中正好仅包含一个名字,则SHFileOperation()函数有两种方法处理pTo的内容
:
shfo.pFrom = "c://demo//one.txt/0";
shfo.pTo = "c://NewDir";
如果目录或文件c:/NewDir已经存在,则它会被适当地处理,即,文件c:/demo/one.txt或者拷贝到目录
,或者置换已经存在的文件。反之,如果c:/NewDir不存在,则它就会被当作新文件名,而不再被当作作
文件夹名。
如果想要拷贝单一文件到新文件夹,则可以考虑在pTo的内容后面加一个反斜线 /来进行操作。
shfo.pFrom = "c://demo//one.txt/0";
shfo.pTo = "c://NewDir//";
奇怪的是这将导致建立缺省文件夹。并且使文件的拷贝或移动失败。如果重试,则它可以象所期望的那
样工作,因为,在第二次运行时,这个文件夹已经存在了。所以,在拷贝单个文件到不存在的文件夹时
需要做些什么工作?唯一总能正常工作的方法是把一个 *字符加到文件名的末尾。这样做是糊弄函数,
使它认为它是在操作一个通配符表达式。
shfo.pFrom = "c://demo//one.txt*/0";
shfo.pTo = "c://NewDir";
另一个可能的情况是你想要拷贝多重单个文件到同样数目的单个文件上。这必须满足两个要求,首先应
该设置FOF_MULTIDESTFILES标志,其次,一定要保证每一个源文件都有一个目的文件—需要完备的1:1
对应。原文件列表中第n个文件被拷贝或移动到目的文件列表中的第n个文件。
shfo.fFlags |= FOF_MULTIDESTFILES;
shfo.pFrom = "c://one.txt/0c://two.txt/0";
shfo.pTo = "c://New one.txt/0c://New two.txt/0";
如果哪个方面没有满足,哪个方面就失败。例如执行下面的代码:
shfo.fFlags |= FOF_MULTIDESTFILES;
shfo.pFrom = "c://one.txt/0c://two.txt/0c://three.txt/0";
shfo.pTo = "c://New one.txt/0c://New two.txt/0";
目标文件列表的第一项(即c:/New one.txt)被作为所有源文件要去的文件夹名。实际上,这个操作被处
理成多对一的操作了。
在使用通配符时,源缓冲可以隐含地包括文件和目录。如果想要函数仅处理文件,加一个
FOF_FILESONLY标志就可以了。如果想要拷贝整个目录,就需要加/*.*到其路径末尾。
除非你指定了FOF_SILENT标志,否则SHFileOperation()函数总是显示带有动画和进度条的进
度对话框,其中的标签显示正在拷贝或移动的文件。通过设置FOF_SIMPLEPROGRESS标志,你可以隐藏这
些标签,用在lpszProgressTitle成员中给出的文字替换他们。这有助于隐藏被拷贝或移动的文件名。
删除文件
文件删除是一个简单的操作,它仅仅影响到输入缓冲pFrom—pTo缓冲被忽略。与前面一样,操
作的详细情况依赖于标志的设置。相关的标志是:
标志
值
描述
FOF_SILENT
0x0004
这个操作不回馈给用户,就是说,不显示进度对话框。相关的消息框仍然显示。
FOF_NOCONFIRMATION
0x0010
这个标志使函数对任何遇到的消息框都自动回答Yes。
FOF_ALLOWUNDO
0x0040
如果设置,这个标志强迫函数移动被删除的文件到‘回收站’中。否则,文件将被物理地从磁盘上删除
。
FOF_FILESONLY
0x0080
设置这个标志导致函数仅仅删除文件,跳过目录项。它仅仅应用于指定通配符的情况。
FOF_SIMPLEPROGRESS
0x0100
这导致简化用户界面。使之只有动画而不报告被删除的文件名。代之的是显示lpszProgressTitle成员中
指定的文字。
FOF_NOERRORUI
0x0400
如果设置了这个标志,任何发生的错误都不能使消息框显示,而是程序中返回错误码。
这里出现的标志最要紧的是FOF_ALLOWUNDO,它允许程序员决定文件是否一次就全部删除,或存储到‘回
收站’中等候可能的恢复。如果FOF_ALLOWUNDO被设置,文件则被移动到回收站,并且这个操作可以被
Undo(尽管可以手动Undo)。涉及到‘回收站’的API函数在第十章中讲述。Undo特征仅在删除下可用—在
拷贝与移动中没有等价的操作。
说明FOF_ALLOWUNDO标志,影响到前面程序中的用户界面。修改我们的简单工程来接受一个删
除请求也不是很困难。但是为了简练,我们还是直接把代码写进OnOK()函数:
ZeroMemory(&shfo, sizeof(SHFILEOPSTRUCT));
shfo.hwnd = hDlg;
shfo.wFunc = FO_DELETE;
shfo.lpszProgressTitle = pszTitle;
shfo.fFlags = FOF_NOERRORUI;
shfo.pFrom = "c://demo//*.*/0";
上面代码企图删除整个c:/demo目录的内容,并且导出对话框:
就象看到的那样,由于没有指定FOF_ALLOWUNDO标志,消息框中没有提到‘回收站’。通过设置
FOF_ALLOWUNDO标志,文件将改为直接发送到回收站:
上表列出的其他标志与拷贝或移动操作所作的完全相同。因此,可以通过设置FOF_SIMPLEPROGRESS或
FOF_SILENT隐藏正在被删除的文件名,通过设置FOF_FILESONLY,仅仅删除文件。注意FOF_FILESONLY标
志不能进入子目录。上面显示的对话框也没有提示有多少文件要被删除。然而这是好理解的,因为发起
计算的命令中包含了通配符(否则将显示文件数),所以函数不能得出文件数。这也可能就是为什么没有
文件被删除时它返回成功的原因吧。
按惯例,操作系统在文件被删除前将请求确认。如果你发现这样的对话框是无用的,你就可以
通过对所有询问回答Yes来自动地隐藏它,这只需设置FOF_NOCONFIRMATION标志即可。典型地,一个
FO_DELETE操作如下图所示:
文件重命名
在这一节中第一个要注意的事情就是不能用通配符来使SHFileOperation()函数重命名文件。
通过指定单个源文件名到pFrom和单个目标文件名到pTo来改变文件名似乎是唯一的方法:
ZeroMemory(&shfo, sizeof(SHFILEOPSTRUCT));
shfo.wFunc = FO_RENAME;
shfo.pFrom = "c://demo//one.txt/0";
shfo.pTo = "c://demo//one.xxx";
显然,有两件事情在重命名文件操作中不允许做是有道理的,明确地说,它们是:
改变目标目录。重命名只是改变名字,不是文件夹。
覆盖已存在的文件
如果努力执行这样的操作,则自然只能获得错误。收索所有的错误代码我们发现,试图传递下面的参数
到函数:
shfo.pFrom = "c://demo//*.*/0";
shfo.pTo = "c://newdir";
显然是不对的,并且函数适时地返回下面的错误消息:
尽管命令荒谬地返回成功(值是0),这个消息是足够清楚的了。然而,这个消息隐含地说明用MS-DOS所用
的语法在这里也能正常工作。换句话说,我们应该能够重命名,如*.txt 到 *.xtt。在MD-DOS下这些是
没问题的,在SHFileOperation()下,它不行。如果你测试,将得到如下消息:
这个消息由下面的两行代码引起:
shfo.pFrom = "c://demo//*.txt/0";
shfo.pTo = "c://demo//*.xtt";
就这个例子而言,c:/demo目录下含有两个文件one.txt 和 two.txt。One在这种情况下实际是所包括的
文件之一,没有扩展名。所以这个消息之后的返回码是2—‘文件没找到’。
由于FO_RENAME命令似乎仅在单个文件时成功,因此影响用户对话框界面的标志就失去了重要性—加速操
作简单地使用户界面不可见。但是下述标志产生作用仍然可以感觉到:
标志
值
描述
FOF_RENAMEONCOLLISION
0x0008
如果目标位置已经包含了一个与要被重命名文件有相同名字的文件,这个标志指示函数自动改变目标名
来拷贝Xxx,此处的Xxx是没有扩展的初始文件名。如果没有设置这个标志,仍然不会有提示,但是,你
将得到错误消息。
FOF_NOERRORUI
0x0400
如果设置了这个标志,所发生的所有错误都不引起消息框的显示,而是返回错误码。
SHFileOperation()函数的返回值
在线资料中说明,SHFileOperation()在成功时返回0,失败时返回非0值。显然这是真的,但
并不是最有用的解释。重复测试这个函数,可以确信它有非常多的终止方式。事实上,我们经常在系统
错误的提示中运行,在有些地方这个函数只是简单地返回从更靠近文件系统的其它程序中获得的返回码
。这里的列表给出了SHFileOperation()返回的最通常的错误(可以肯定不是最详尽的)。
错误码
描述
2
就象上面提到的,如果你试图重命名多重文件,这个消息就会出现。描述是相当直接的—系统不能找到
指定的文件—但是并不能理解它为什么不能找到文件。
7
在询问是否想要置换给定文件时,你回答了‘取消’,函数就返回这个错误码。它的描述也是相当的不
明确—存储控制块被销毁。
115
在试图重命名文件到不同的文件夹时,发生这个文件系统错。重命名文件只是改变文件名,而不能改变
文件夹。
117
一个IOCTL错(输入/输出控制),在目的路径中有错误时或取消了新目录的建立时,这个错误发生了。
123
你正在试图重命名一个文件,然而你给出的名字是一个已经存在的文件。它也有一个无用的描述:文件
名,目录名,或卷标号的语法是不正确的。
1026
在试图移动或拷贝一个不存在的文件时,出现这个文件系统错。一般地,它提示了,源缓冲中的某些东
西应该修改一下。这个错误码引出下面的错误框—你可以通过设置FOF_NOERRORUI标志抑制它的显示。
在很多情况下都返回错误码117,所有这些都与目标目录的问题有关。例如,如果你取消了要
求建立目录的请求,函数就返回这个码(不显示系统消息框)。如果在指定的目录名中有明显的错误,
错误框就被显示,你可以使用FOF_NOERRORUI标志来抑制它。
两个关于错误信息显示的简单例程
错误消息为绝大多数程序员所诅咒。因此,或者写出除文字描述以外的大量代码,或者通过其
他方法绕过错误消息。框架环境如MFC提供了一些工具,然而,你一定不想要只是为了开发这样的特征把
代码移植到MFC中吧。为此,我们生成了包含两个实用函数的文件,在附录A中。头一个是经过修订的
MessageBox()。它通过增加常用的printf()的格式化能力来扩展功能,改名为Msg(),代码如下:
#include <stdarg.h>
void WINAPI Msg(char* szFormat, ...)
{
va_list argptr;
char szBuf[MAX_PATH];
HWND hwndFocus = GetFocus();
// 初始化va_ 函数
va_start(argptr, szFormat);
// 格式化输出串
wvsprintf(szBuf, szFormat, argptr);
// 读显示标题
MessageBox(hwndFocus, szBuf, NULL, MB_ICONEXCLAMATION | MB_OK);
// 关闭va_ 函数
va_end(argptr);
SetFocus(hwndFocus);
}
主要是这段代码使用了va_ 函数,它包含在stdarg.h头文件中。变量列表经由wvsprintf()格式化,最终
由普通的MessageBox()函数显示。现在我们可以象下面那样写代码了:
iRC = CallFunc(p1, p2);
Msg("The error code returned is: %d", iRC);
第二个实用函数是SPB_SystemMessage()(SPB前缀表示Shell Programming Book,用于区别你自己所写的
函数)。它接受错误码,并传递到FormatMessage(),一个Win32 API函数,对所有系统错误码(至少是
winerror.h里定义的错误码)返回描述文字串的函数。FormatMessage()提供的串与代码号聚在一起,并
一同显示之:
void WINAPI SPB_SystemMessage(DWORD dwRC)
{
LPVOID lpMsgBuf;
DWORD rc;
rc = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, dwRC,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
reinterpret_cast<LPTSTR>(&lpMsgBuf), 0, NULL);
Msg("%s: %ld./n/n/n%s:/n/n%s", "This is the error code", dwRC,
"This is the system's explanation", (rc == 0 ? "<unknown>" : lpMsgBuf));
LocalFree(lpMsgBuf);
}
函数适当地工作了吗
毋庸置疑,SHFileOperation()函数就其返回码而言,有一些问题。特别是,即使在输入参数
错,要求的操作不能完成的情况下,它也返回0(成功标志):
测试下面拷贝或移动文件的代码:
shfo.pFrom = "c://demo//one.txt/0";
shfo.pTo = "c://NewDir";
如果one.txt文件在初始文件夹中存在,操作能正常进行。如果不存在,错误1026出现。没说的,这正是
所期望的。然而,如果你试一下下面的代码(要保证没有匹配这个模式的文件),会发生什么情况呢:
shfo.pFrom = "c://demo//x.*/0";
shfo.pTo = "c://NewDir";
这个函数仍然返回0,即使没有文件被处理。相同的情况也出现在删除文件操作中。即使没有文件被删除
,返回码仍然表示成功。就信誉而言,这是否算做一个bug,或是一个故意的行为。没有快捷的方法来验
证操作是否获得了所希望的结果,因此一定要记住函数返回之后一定要检查文件是否还存在。
长文件名
尽管Windows Shell的设计和实现是要带给用户最大的方便,然而,其中的某些函数对于长文
件名似乎有点问题。确实,这是对的—长文件名。下面就让我们看看什么是长文件名。
在所有上面看到的样例中,我们为目标文件夹指定了全路径名(通常使用c:/NewDir)。资料上
说,如果没有提供全路径名,这个函数就假设使用由API函数GetCurrentDirectory()返回的当前目录。
好,现在就测试一下,在函数SHFileOperation()中使用下面代码:
shfo.pFrom = "c://demo//*.*/0";
shfo.pTo = "NewDir";
我们想要拷贝或移动c:/demo目录下的所有文件到一个称之为NewDir的新的或已经存在的目录,该目录定
位于当前目录下。如果在传输的文件中所提供的文件名没有长文件名的话,所有操作都能顺利地执行。
如果有任何长文件名,下面这个对话框将出现:
这个函数所发生的操作是试图缩短长文件名以确保它能正确地被存储到目标驱动器。当目标机器运行在
Windows3.1的情况下,在网络上移动文件,这样做是非常自然的。不幸的是我们是在运行32位操作系统
的单个机器上拷贝或移动文件—这是适应长文件名的系统。如果不缩短文件名,函数SHFileOperation()
就不工作。
奇怪的是,如果加一个驱动器到目标文件夹上,所有事情就又能工作了。还有一个不太陌生的
情况,你将惊奇地发现,使用相对路径时,所有操作都是顺利的。奇怪,究竟发生了什么?
如果路径名开始字符是一个可用驱动器的逻辑标识符时,SHFileOperation()函数在长文件名
下能顺利工作。否则,它认为你正在连接远程驱动器,为此,支持长文件名的检查失败(如果没有N:驱
动器,肯定失败)。例如,在我的机器中,直到F都能顺利工作,这是一个CD-ROM驱动器。
这可能是计算文件的目标驱动器所使用的代码中某个地方有错误而造成的,要想正常地操作,最简
单的方式就是总使用全路径名。
文件名映射对象
在阅读SHFileOperation()的官方资料时,你可能已经注意到了关于文件名映射对象的谨慎描
述。特别是,在对SHFILEOPSTRUCT结构的成员hNameMappings的表述时,资料中讲到了这个对象。
hNameMappings是一个指向内存块的指针—声明为LPVOID,该内存块中包含一定数量的SHNAMEMAPPING结
构。SHNAMEMAPPING的数据结构定义如下:
typedef struct _SHNAMEMAPPING
{
LPSTR pszOldPath;
LPSTR pszNewPath;
int cchOldPath;
int cchNewPath;
} SHNAMEMAPPING, FAR* LPSHNAMEMAPPING;
这个结构标识了被拷贝,移动甚至重命名的文件。更精确地说,它不仅存储了初始(全路径)文件名而且
还有新的(全路径)文件名。因此,它暗示了一种可能性:在SHFileOperation()函数执行期间,你能够获
得所发生情况的完整报告。可惜的是,事情并不象想象的那么简单。
首先,要使SHFileOperation()填写hNameMappings成员,你就必须指定一个附加的标志
FOF_WANTMAPPINGHANDLE。只这样做还不够,因为只有你也设置了FOF_RENAMEONCOLLISION标志,这个成
员才被填写。进一步说,要使函数填写所有东西而不是0,文件操作就要使用重命名来避免冲突。所有其
它情况,hNameMappings 只简单地指向NULL。
文件映射示例
建立一个基于对话框的应用称之为FileMap,用以测试关于文件映射的一些东西。这里是用户
界面:
要使用现实的值设置对话框,和初始化列表观察,你需要象如下方式调整OnInitDialog()函数(记住附加
#include resource.h语句):
void OnInitDialog(HWND hDlg)
{
HWND hwndList = GetDlgItem(hDlg, IDC_LIST);
// 设置报告观察
LV_COLUMN lvc;
ZeroMemory(&lvc, sizeof(LV_COLUMN));
lvc.mask = LVCF_TEXT | LVCF_WIDTH;
lvc.cx = 200;
lvc.pszText = "Original File";
ListView_InsertColumn(hwndList, 0, &lvc);
lvc.pszText = "Target File";
ListView_InsertColumn(hwndList, 1, &lvc);
// 初始化编辑区域
SetDlgItemText(hDlg, IDC_FROM, "c://thedir//*.*");
SetDlgItemText(hDlg, IDC_TO, "c://newdir");
// 设置图标(T/F 作为大/小图标)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
}
现在可以编辑OnOK()函数了,附加的代码说明怎样取得和测试文件名映射对象的Handle:
void OnOK(HWND hDlg)
{
TCHAR pszFrom[1024] = {0};
TCHAR pszTo[MAX_PATH] = {0};
GetDlgItemText(hDlg, IDC_FROM, pszFrom, MAX_PATH);
GetDlgItemText(hDlg, IDC_TO, pszTo, MAX_PATH);
SHFILEOPSTRUCT shfo;
ZeroMemory(&shfo, sizeof(SHFILEOPSTRUCT));
shfo.hwnd = hDlg;
shfo.wFunc = FO_COPY;
shfo.pFrom = pszFrom;
shfo.pTo = pszTo;
shfo.fFlags = FOF_NOCONFIRMMKDIR |
FOF_RENAMEONCOLLISION |
FOF_WANTMAPPINGHANDLE;
int iRC = SHFileOperation(&shfo);
if(iRC)
{
SPB_SystemMessage(iRC);
return;
}
// 跟踪Handle值
Msg("hNameMappings is: %x", shfo.hNameMappings);
// 象推荐那样释放对象
if(shfo.hNameMappings)
SHFreeNameMappings(shfo.hNameMappings);
}
要特别注意代码的最后一行—释放文件名影射对象是你能在其上执行的最简单的操作。你必须调用
SHFreeNameMapping(),并且传递从SHFileOperation()中接受的Handle参数。每一步都能正常地执行,
并且也能很好地理解。或许有一天,Windows的资料也能如此清晰。
总之,运行这段代码后,你将发现hNameMappings总是NULL,除非在执行的(拷贝,移动,重命名)操作中
引起了名字冲突。如果发生重命名操作,这个Handle 用于向你报告已经实际重命名文件的情况,以避免
覆盖其他文件,报告给出了文件的新名和原名。
所以文件名影射对象与内存影射文件或其他进程内通讯的机理一样,没有做任何事情。它就是一个内存
块,允许Shell (和你)保持对已经重命名的文件踪迹的跟踪,以避免名字冲突。
如果目标目录(本例中为 c:/newdir)不存在,或它包含的文件名全都不同于源路径(本例中为
c:/thedir/*.*)中的文件,则不论指定了什么标志,Handle都是NULL:
相反,如果至少有一个重命名冲突发生,这个Handle 就引用了有意义的数据块,因此,也就返回了可用
的内存地址。
使用文件映射对象
获取文件名映射对象的Handle只是完成了一半工作,现在我们来评估一下,这个Handle是多么地有用!
在资料中仅简单地说,(在非NULL时)hNameMappings指向一个SHNAMEMAPPING结构的数组。并没有提到怎
样获得这个数组的尺寸。更有甚者,说这个SHFileOperation()用于存储Handle的LPVOID成员不是一个指
向数据结构数组的指针。显然,简单地经由循环遍历数组的方法在这里就不能工作了。
在有些旧的MSDN资料中,你将发现两个提到的函数SHGetNameMappingCount()和 SHGetNameMappingPtr()
。然而,现在这两个函数不仅在资料中没有说明,而且也没有公开。在Shell32 (来自IE4.0或更高)的版
本中也没有它们的任何踪迹。这样就很不好了,因为它们确实是使你能正确编码的函数。不可理解,为
什么删除了这些函数,而且对hNameMappings成员的支持显得既生冷又陈旧。
一个未写进资料的结构
资料说明的东西是真的,但是,是不完整的。问题在于它忽视了上面提到的落在
hNameMappings和数组之间的数据结构。有两条线索是我获得了正确的踪迹,第一,来自下面代码的输出
:
TCHAR* pNM = static_cast<TCHAR*>(shfo.hNameMappings);
Msg(pNM);
在测试这段代码时,我顺利地获得了另一种访问非法错,奇怪的是,它正好重复了错误号(如 9)。这是
重命名冲突错误号吗?在检查了目录之后发现,确实是。当然我立即执行了另一个使用不同文件数的检
测,并且验证了这个想法。无论hNameMappings指向什么,开始都与全体文件名映射对象数一致。
所以下一步的工作将是遍览Internet客户端SDK和MSDN文档,探讨某些未知的剪裁板格式,它们是:
Windows ShellAPI 和拖拽操作
MSDN的知识库文章Q154123
这些格式(其中有一个是“文件名映射”),在请求拷贝和粘贴操作时,或在从一个文件夹到另一个文件
夹拖拽文件对象时是由Shell内部使用的。更有趣的是,很多这样的格式在剪裁板中都是以数据块的方式
存储的,包含了一个数字和一个指向客户数据结构的指针。数字表示数组的尺寸,指针指向他的第一个
元素。
近似的方案
高兴的是同样的模式也可以应用到了映射对象,所以,我定义了一个结构
SHNAMEMAPPINGHEADER具有如下格式:
struct SHNAMEMAPPINGHEADER
{
UINT cNumOfMappings;
LPSHNAMEMAPPING lpNM;
};
typedef SHNAMEMAPPINGHEADER* LPSHNAMEMAPPINGHEADER;
这个结构实际上与hNameMappings所指向的数据有相同的格式。画图说明如下,这也说明了一种访问
SHNAMEMAPPING数据结构的方法:
如此,写一个函数来枚举所有文件名映射对象就是直接了当的事情了;我把它称之为。
SHEnumFileMapping()。在观察函数本身之前,先要扩展一下前面的OnOK(),以包含对该函数的调用:
void OnOK(HWND hDlg)
{
...
// 跟踪这个handle的值
Msg("hNameMappings is: %x", shfo.hNameMappings);
// 枚举文件映射对象
SHEnumFileMapping(shfo.hNameMappings, ProcessNM,
reinterpret_cast<DWORD>(GetDlgItem(hDlg, IDC_LIST)));
// 如推荐那样释放对象
if(shfo.hNameMappings)
SHFreeNameMappings(shfo.hNameMappings);
}
SHEnumFileMapping()函数接受Handle,回调过程,和通用缓冲。它枚举所有SHNAMEMAPPING,并逐一传
送它们给回调函数,以便作进一步的处理。
int WINAPI SHEnumFileMapping(HANDLE hNameMappings, ENUMFILEMAPPROC lpfnEnum,
DWORD dwData)
{
SHNAMEMAPPING shNM;
// 检查Handle
if(!hNameMappings)
return -1;
// 获得结构头
LPSHNAMEMAPPINGHEADER lpNMH = static_cast<LPSHNAMEMAPPINGHEADER>(hNameMappings);
int iNumOfNM = lpNMH->cNumOfMappings;
// 检查函数指针; 如果NULL, 直接返回影射数
if(!lpfnEnum)
return iNumOfNM;
// 枚举对象
LPSHNAMEMAPPING lp = lpNMH->lpNM;
int i = 0;
while(i < iNumOfNM)
{
CopyMemory(&shNM, &lp[i++], sizeof(SHNAMEMAPPING)); if(!lpfnEnum(&shNM,
dwData))
break;
}
// 返回实际处理的对象数
return i;
}
SHEnumFileMapping()函数与绝大多数Windows枚举函数所遵循的模式一样。它接受回调函数和通用缓冲
,这个缓冲用于保存调用程序传输给回调函数的客户数据,此外,它期望回调函数在终止枚举时返回0。
我定义的回调函数类型为ENUMFILEMAPPROC:
typedef BOOL (CALLBACK *ENUMFILEMAPPROC)(LPSHNAMEMAPPING, DWORD);
这个函数接受一个SHNAMEMAPPING对象的指针,和调用程序发送的客户数据。
当然使用类枚举函数列出所有结构是一个个人偏好。等价地也可以使用导航界面,提供
FindFirstSHNameMapping()和FindNextSHNameMapping()函数。
事实上,由回调函数执行这个操作要好得多。在这里我所使用的(ProcessNM())是从任何它所接收的
SHNAMEMAPPING结构中抽取pszOldPath和 pszNewPath字段值。并且把它们加到报告的列表观察中:
BOOL CALLBACK ProcessNM(LPSHNAMEMAPPING pshNM, DWORD dwData)
{
TCHAR szBuf[1024] = {0};
TCHAR szOldPath[MAX_PATH] = {0};
TCHAR szNewPath[MAX_PATH] = {0};
OSVERSIONINFO os;
// 我们需要知道在什么样的 OS 上
os.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&os);
BOOL bIsNT = (os.dwPlatformId == VER_PLATFORM_WIN32_NT);
// 在 NT 下,SHNAMEMAPPING结构包含 UNICODE 串
if(bIsNT)
{
WideCharToMultiByte(CP_ACP, 0, reinterpret_cast<LPWSTR>(pshNM->pszOldPath),
MAX_PATH, szOldPath, MAX_PATH, NULL, NULL);
WideCharToMultiByte(CP_ACP, 0, reinterpret_cast<LPWSTR>(pshNM->pszNewPath),
MAX_PATH, szNewPath, MAX_PATH, NULL, NULL);
}else{
lstrcpy(szOldPath, pshNM->pszOldPath);
lstrcpy(szNewPath, pshNM->pszNewPath);
}
// 保存列表观察Handle
HWND hwndListView = reinterpret_cast<HWND>(dwData);
// 建立 /0 分隔的串
LPTSTR psz = szBuf;
lstrcpyn(psz, szOldPath, pshNM->cchOldPath + 1);
lstrcat(psz, __TEXT("/0"));
psz += lstrlen(psz) + 1;
lstrcpyn(psz, szNewPath, pshNM->cchNewPath + 1);
lstrcat(psz, __TEXT("/0"));
// 加串到报告观察中
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_TEXT;
lvi.pszText = szBuf;
lvi.cchTextMax = lstrlen(szBuf);
lvi.iItem = 0;
ListView_InsertItem(hwndListView, &lvi);
psz = szBuf + lstrlen(szBuf) + 1;
ListView_SetItemText(hwndListView, 0, 1, psz);
return TRUE;
}
注意,在Windows NT下,SHNAMEMAPPING结构中的串是Unicode格式的。因此,如果操作系统是NT,则转
换串到ANSI格式,以便在例程中使用它们。还要注意的是dwData缓冲,它用于传输列表观察的Handle到
回调函数。
把这个代码与较早期的例子集成到一起后,现在就能够给出调用SHFileOperation()函数引起
重命名文件的详细过程。在典型情况下测试,可以看到下面的情况:
小结
这一章深入讨论了一个函数SHFileOperation(),对它的每一个方面都作了彻底地测试了。从
拷贝,移动,重命名或删除文件,以及设置标志改变函数行为开始,然后展开了对某些未写进资料的返
回码,Bugs,函数缺陷的讨论。概括地讲,在这一章中,给出了:
怎样编程SHFileOperation()
最普遍的编程错。
这个函数在资料方面的短缺
怎样利用文件名映射的优点
========
自己动手写一个简单的Windows shell扩展程序
http://blog.csdn.net/clever101/article/details/7583460一.Shell程序编写
这里采用的开发环境为WindowsXP+sp3, VS 2005 + sp1 (应该支持VS 2005以上的VS版本,VC
6.0估计不支持)。
1.新建一个ATL项目,输入工程名:ImportShell,具体如下图:
2. 在应用程序设置中的服务器类型中选择:动态链接库(DLL),其它选项采用默认设置,具体如下图:
这样单击完成后就新建了ATL工程。
3.新建一个ATL简单对象(英文版的VS为ATLSimple Object),具体如下图:
4.输入一个简称:ImportShellExt,其它的VS会帮你自动填写,具体如下图:
新建CImportShellExt类需要新继承两个基类:IShellExtInit和IContextMenu。新加的接口函数主要有
四个:
当我们的shell扩展被加载时, Explorer 将调用我们所实现的COM对象的 QueryInterface() 函数以取得
一个 IShellExtInit 接口指针.
该接口仅有一个方法 Initialize(), 其函数原型为:
HRESULTIShellExtInit::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj,HKEY
hProgID );
Explorer 使用该方法传递给我们各种各样的信息.
PidlFolder是用户所选择操作的文件所在的文件夹的 PIDL 变量. (一个 PIDL [指向ID列表的指针] 是
一个数据结构,它唯一地标识了在Shell命名空间的任何对象, 一个Shell命名空间中的对象可以是也可
以不是真实的文件系统中的对象.)
pDataObj 是一个IDataObject 接口指针,通过它我们可以获取用户所选择操作的文件名。
hProgID 是一个HKEY注册表键变量,可以用它获取我们的DLL的注册数据.
一旦 Explorer 初始化了扩展,它就会接着调用 IContextMenu 的方法让我们添加菜单项, 提
供状态栏上的提示, 并响应执行用户的选择。
添加IContextMenu 方法的函数原型: public:
[cpp] view plain copy
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);
修改上下文菜单IContextMenu 有三个方法.
第一个是QueryContextMenu(), 它让我们可以修改上下文菜单. 其原型为:
[cpp] view plain copy
HRESULT IContextMenu::QueryContextMenu ( HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd,
UINT uidLastCmd, UINT uFlags );
hmenu 上下文菜单句柄.
uMenuIndex 是我们应该添加菜单项的起始位置.
uidFirstCmd 和 uidLastCmd 是我们可以使用的菜单命令ID值的范围.
uFlags 标识了Explorer 调用QueryContextMenu()的原因。
而返回值根据你所查阅的文档的不同而不同.
Dino Esposito 的书中说返回值是你所添加的菜单项的个数.
而 VC6.0所带的MSDN 又说它是我们添加的最后一个菜单项的命令ID加上1.
而最新的 MSDN 又说:
将返回值设为你为各菜单项分配的命令ID的最大差值,加上1.
例如, 假设 idCmdFirst 设为5,而你添加了三个菜单项 ,命令ID分别为 5, 7, 和8.
这时返回值就应该是: MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1).
我是一直按 Dino 的解释来做的, 而且工作得很好.
实际上, 他的方法与最新的 MSDN 是一致的, 只要你严格地使用 uidFirstCmd作为第一个菜单项的ID,再
对接续的菜单项ID每次加1.
我们暂时的扩展仅加入一个菜单项,所以 QueryContextMenu() 非常简单:
[cpp]
HRESULT CImportShellExt::QueryContextMenu( HMENU hmenu,UINT uMenuIndex, UINT uidFirstCmd,
UINT uidLastCmd, UINT uFlags )
{
// 如果标志包含CMF_DEFAULTONLY 我们不作任何事情.
if ( uFlags & CMF_DEFAULTONLY )
{
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
}
InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("工程入库") );
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}
首先我们检查 uFlags.
你可以在 MSDN中找到所有标志的解释, 但对于上下文菜单扩展而言, 只有一个值是重要的:
CMF_DEFAULTONLY.
该标志告诉Shell命名空间扩展保留默认的菜单项,这时我们的Shell扩展就不应该加入任何定制的菜单
项,这也是为什么此时我们要返回 0 的原因.
如果该标志没有被设置, 我们就可以修改菜单了 (使用 hmenu 句柄), 并返回 1 告诉Shell我们添加了
一个菜单项。
下一个要被调用的IContextMenu 方法是 GetCommandString().如果用户是在浏览器窗口中右击
文本文件,或选中一个文本文件后单击文件菜单时,状态栏会显示提示帮助.
我们的 GetCommandString() 函数将返回一个帮助字符串供浏览器显示.
GetCommandString() 的原型是:
[cpp] view plain copy
HRESULT IContextMenu::GetCommandString ( UINT idCmd, UINT uFlags, UINT *pwReserved, LPSTR
pszName, UINT cchMax );
idCmd 是一个以0为基数的计数器,标识了哪个菜单项被选择.
因为我们只有一个菜单项, 所以idCmd 总是0. 但如果我们添加了3个菜单项, idCmd 可能是0, 1, 或 2.
uFlags 是另一组标志(我以后会讨论到的).
PwReserved 可以被忽略.
pszName 指向一个由Shell拥有的缓冲区,我们将把帮助字符串拷贝进该缓冲区.
cchMax 是该缓冲区的大小.
返回值是S_OK 或 E_FAIL.
GetCommandString() 也可以被调用以获取菜单项的动作("verb") .
verb 是个语言无关性字符串,它标识一个可以加于文件对象的操作。
ShellExecute()的文档中有详细的解释, 而有关verb的内容足以再写一篇文章, 简单的解释是:verb 可
以直接列在注册表中(如"open" 和 "print"等字符串), 也可以由上下文菜单扩展创建. 这样就可以通过
调用ShellExecute()执行实现在Shell扩展中的代码.
不管怎样, 我说了这多只是为了解释清楚GetCommandString() 的作用.
如果 Explorer 要求一个帮助字符串,我们就提供给它. 如果 Explorer 要求一个verb, 我们就忽略它.
这就是 uFlags 参数的作用.
如果 uFlags 设置了GCS_HELPTEXT 位,则 Explorer 是在要求帮助字符串. 而且如果 GCS_UNICODE 被设
置,我们就必须返回一个Unicode字符串.
我们的 GetCommandString() 如下:
[cpp] view plain copy
#include <atlconv.h>
// 为使用 ATL 字符串转换宏而包含的头文件
HRESULT CImportShellExt::GetCommandString( UINT idCmd, UINT uFlags,UINT* pwReserved, LPSTR
pszName, UINT cchMax )
{
USES_CONVERSION;
//检查idCmd, 它必须是,因为我们仅有一个添加的菜单项.
if ( 0 != idCmd )
return E_INVALIDARG;
// 如果Explorer 要求帮助字符串,就将它拷贝到提供的缓冲区中.
if ( uFlags & GCS_HELPTEXT )
{
LPCTSTR szText = _T("统计文件夹中的文件个数");
if ( uFlags & GCS_UNICODE )
{
// 我们需要将pszName 转化为一个Unicode 字符串, 接着使用Unicode字符串拷贝API.
lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );
}
else
{
// 使用ANSI 字符串拷贝API 来返回帮助字符串.
lstrcpynA ( pszName, T2CA(szText), cchMax );
}
return S_OK;
}
return E_INVALIDARG;
}
这里没有什么特别的代码; 我用了硬编码的字符串并把它转换为相应的字符集.
如果你从未使用过ATL字符串转化宏,你一定要学一下,因为当你传递Unicode字符串到COM和OLE函数时
,使用转化宏会很有帮助的.
我在上面的代码中使用了T2CW 和 T2CA 将TCHAR 字符串分别转化为Unicode 和 ANSI字符串.
函数开头处的USES_CONVERSION 宏其实声明了一个将被转化宏使用的局部变量.
要注意的一个问题是: lstrcpyn() 保证了目标字符串将以null为结束符.
这与C运行时(CRT)函 数strncpy()不同.当要拷贝的源字符串的长度大于或等于cchMax 时 strncpy()不
会添加一个 null 结束符.
我建议总使用lstrcpyn(), 这样你就不必在每一个strncpy()后加入检查保证字符 串以null为结束符的
代码.
IContextMenu 接口的最后一个方法是InvokeCommand(). 当用户点击我们添加的菜单项时该方法将被调
用. 其函数原型是:
[cpp] view plain copy
HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );
CMINVOKECOMMANDINFO 结构带有大量的信息, 但我们只关心 lpVerb 和 hwnd 这两个成员.
lpVerb参数有两个作用 – 它或是可被激发的verb(动作)名, 或是被点击的菜单项的索引值.
hwnd 是用户激活我们的菜单扩展时所在的浏览器窗口的句柄.
因为我们只有一个扩展的菜单项, 我们只要检查lpVerb 参数,如果其值为0, 我们可以认定我们
的菜单项被点击了。我能想到的最简单的代码就是弹出一个信息框, 这里的代码也就做了这么多. 信息
框显示所选的文件夹的名字。具体代码如下:
[cpp] view plain copy
HRESULT CImportShellExt::InvokeCommand( LPCMINVOKECOMMANDINFO pCmdInfo )
{
// If lpVerb really points to a string, ignore this function call and bail out.
if ( 0 != HIWORD( pCmdInfo->lpVerb ) )
return E_INVALIDARG;
// Get the command index - the only valid one is 0.
switch ( LOWORD( pCmdInfo->lpVerb) )
{
case 0:
{
TCHAR szMsg [MAX_PATH + 32];
wsprintf ( szMsg, _T("选中的文件夹为%s"),m_szFile);
MessageBox ( pCmdInfo->hwnd, szMsg, _T("信息"),
MB_ICONINFORMATION );
return S_OK;
}
break;
default:
return E_INVALIDARG;
break;
}
}
这时可能你会问:操作系统是如何知道我们要插入这个菜单的?这里涉及到一个COM组件的注册问
题。所谓COM组件的注册,简单来说是将COM组件的相关信息写进注册表,然后操作系统通过读取注册表
的相关信息来加载COM组件。Shell程序的注册分为两步:
第一步在Win NT/Win 2000上确保你的Shell扩展能被没有管理员权限的用户调用,需要在注册
表HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ShellExtensions\Approved添加
我们的程序信息。这个需要在工程中的DllRegisterServer函数(注册函数)和DllUnregisterServer函
数(反注册函数)。代码如下:
[cpp] view plain copy
// DllRegisterServer - 将项添加到系统注册表
STDAPI DllRegisterServer(void)
{
// 注册对象、类型库和类型库中的所有接口
if ( 0 == (GetVersion() & 0x80000000UL) )
{
CRegKey reg;
LONG lRet;
lRet = reg.Open ( HKEY_LOCAL_MACHINE,
_T("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"),
KEY_SET_VALUE );
if ( ERROR_SUCCESS != lRet )
return E_ACCESSDENIED;
lRet = reg.SetValue ( _T("ImportShell extension"),
_T("{06001B8E-8858-4CEE-8E91-60E12A6C81A7}") );
if ( ERROR_SUCCESS != lRet )
return E_ACCESSDENIED;
}
HRESULT hr = _AtlModule.DllRegisterServer();
return hr;
}
// DllUnregisterServer - 将项从系统注册表中移除
STDAPI DllUnregisterServer(void)
{
if ( 0 == (GetVersion() & 0x80000000UL) )
{
CRegKey reg;
LONG lRet;
lRet = reg.Open ( HKEY_LOCAL_MACHINE,
_T("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"),
KEY_SET_VALUE );
if ( ERROR_SUCCESS == lRet )
{
lRet = reg.DeleteValue ( _T("{06001B8E-8858-4CEE-8E91-60E12A6C81A7}") );
}
}
HRESULT hr = _AtlModule.DllUnregisterServer();
return hr;
}
这里的一个问题是reg.SetValue ( _T("ImportShell extension"),
_T("{06001B8E-8858-4CEE-8E91-60E12A6C81A7}") );
中的键名和键值是如何来的。实际上当你新建COM简单对象后,就会自动生成一个ImportShellExt.rgs文
件,打开这个ImportShellExt.rgs文件,就会有如下的文件:
ImportShell.ImportShellExt.1 = s 'ImportShellExt Class'
{
CLSID = s '{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
}
ImportShell.ImportShellExt = s 'ImportShellExt Class'
{
CLSID = s '{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
CurVer = s 'ImportShell.ImportShellExt.1'
}
这个键名一般取自程序名+ extension,如ImportShell extension,键值则来自它的guid的字符串形式
: {06001B8E-8858-4CEE-8E91-60E12A6C81A7}。
第二步则涉及到该Shell程序所操作的文件类型。比如我们要求它在选中文件夹才弹出我们这个右键
菜单。这时就需要在ImportShellExt.rgs文件添加一些信息:
NoRemove Folder
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove ImportShellExt = s'{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
}
}
}
上面这个其实很好理解的:每一行代表一个注册表键, "HKCR"是HKEY_CLASSES_ROOT 的缩写.
NoRemove 关键字表示当该COM服务器注销时该键 不用被删除.
最后一行有些复杂. ForceRemove 关键字表示如果该键已存在, 那么在新键添加之前该键先应被删除.
这行脚本的余下部分指定一个字符串,它将被存为ImportShell键的默认值。
如果你要操作txt文件,可以添加这样的信息:
NoRemove .txt
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove ImportShellExt = s'{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
}
}
}
如果要操作任意类型的文件,则是:
NoRemove *
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove ImportShellExt = s'{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
}
}
}
二.Shell程序调试
在Win NT/2000上, 你可以找到如下键:
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer
并创建一个名为DesktopProcess的DWORD值 1. 这会使桌面和任务栏运行在同一个进程中, 而其他每一个
Explorer 窗口都运行在它自己的每一个进程内. 也就是说,你可以在单个的Explorer窗口内进行调试,
而后只要你关闭该窗口,你的DLL就会被马上卸载, 这就避免了因为DLL正被Windows使用而无法替换更新.
而如果不幸出现这种情况,你就不得不注销登录后再重新登录进Windows从而强制卸载使用中的Shell扩
展DLL。
按F5开始,这时会弹出一个对话框,这时请输入exploer.exe的路径,如下图:
这时一般会出现一个警告框,按是不予理会,如下图:
接着是打开一个我的文档的窗口,如下图:
这时就可以在代码中设置断点调试了。
三.Shell程序的部署
Shell程序的部署很简单,就是在生成的dll的目录下新建两个批处理文件:
install.bat ——shell程序的安装脚本,内容为:
regsvr32.exe ImportShell.dll
uninstall.bat ——shell程序的卸载脚本,内容为:
regsvr32.exe /u ImportShell.dll
运行这两个批处理文件就能安装或卸载shell程序。
四.遇到问题及解决办法
链接器工具错误 LINK : fatal error LNK1168: cannot open..\outdir\Debug
\ImportShell.dll for writing。
在改变注册com对象的guid会出现该问题。解决办法是打开任务管理器,杀死所有explorer.exe,
然后新建一个explorer进程。
参考文献:
《Windows Shell扩展编程完全指南》,作者:Michael Dunn。
文中参考工程源码下载地址:CSDN下载。
========
Windows Shell 编程 第七章
第七章 侵入Shell
与所有其它Win32进程一样,Shell也有其自己的内存地址空间,这是其它应用完全不可知的地
址空间。为了进入这个地址空间,我们必须传递一定数量的控制点,就象我们正在跨过国家边界一样。
在Windows Shell这块陆地上什么是我们感兴趣的呢,它是一个伊甸园吗,它有丰富的金矿吗,它是天堂
的宝库吗?不幸,它都不是。进入Shell,只是简单地允许我们编写代码执行在Shell外面不能执行的操
作。通过注入代码到一个Win32进程的地址空间,我们能够控制这个程序的行为,能够过滤它的事件,查
看消息流,以及强迫它做(或不做)一定的操作。
为了达到这个目的,我们可以采取几种不同的方法。有力的方法是使用某些Windows的特征(或
弱点)进入进程的地址空间和子类化它的窗口。此外,有些程序明确地允许外部模块介入,且可以一同工
作。此时我们要做的是写一个具有必要接口的模块(一般是一个COM进程内服务器),并且在主模块要求的
地方注册它。
第三种方法是让每个进程都在自己的空间中运行,但是建立一个通道,使它们之间可以通讯。你可以想
象一个程序合理地影响另一个程序行的情形—或者,一个程序能够做一些使另一个程序能够知道的操作
。在这种情况下,有一个潜在的通道连接这些模块—允许探测器知道你可能对文件或文件夹作出的任何
改变就使用了这种方式。
在这一章中,我们将给出实现上述三种模块的例子。另外还解释:
Shell怎样感知文件系统的变化
你的事件怎样才能通知到Shell
怎样进入到Shell的地址空间
作为上述结果,怎样改变‘开始’按钮的行为
我们重点使用Win32软件的两个部分:钩子和通知对象。在我们将要研讨的很多关键点上这些机理都是隐
含的。
Shell通知事件
你一定已经注意到了,探测器能非常快地感知文件系统的任何变化,周期地刷新当前观察和反
映其它应用引起的任何改变。例如,当你打开DOS窗口和探测器窗口时,在两者中选择相同的目录,然后
在DOS窗口中建立一个目录,后者将没有任何迟滞地更新显示。
似乎有某件事情告诉探测器已经建立了一个新的文件夹。在这个外壳下,使所有这些成为可能
的控件是通知对象。
通知对象
通知对象是同步线程的核心对象,其概念是你建立这样一个对象,并给它赋予某些用以配置事
件的属性,然后在其上阻塞线程等待事件的发生。如果你愿意,你可以把通知对象当成专门的事件,在
它感觉到文件系统改变时自动获得信号。通过通知对象,你可以控制目录,子树,甚至整个驱动器,以
及监视文件和文件夹事件—建立,重命名,删除,属性更改等。
通知对象的用法
Win32 SDK定义了三个操作通知对象的函数,它们是:
FindFirstChangeNotification()
FindNextChangeNotification()
FindCloseChangeNotification()
第一个函数‘建立’新通知对象,最后一个函数删除这个对象。奇怪的是,你不必象对待其它核对象那
样使用CloseHandle()来释放通知对象。
前面讲过,在通知对象背后是一个标准的Win32同步对象,但是它已经增加了监视文件系统的
特殊行为。在FindFirstChangeNotification()和FindNextChangeNotification()函数的背后有捕捉这个
核对象信号状态的秘密任务。在通过调用FindFirstChangeNotification()建立对象时,它是非信号状态
的,当它感觉到一个满足滤波条件的活动时,状态改变信号发送给等待线程。为了继续查询事件,必须
显式地重置初始状态,这就是FindNextChangeNotification()所要做的。
同步对象包括‘互斥体(mutexes)’,‘信号灯(semaphores)’,‘事件(events)’和‘临界
节(critical sections)’等等,在VC++ 帮助文件中有完备描述。它们有不同的行为,但是基本上都作
用于线程的同步过程。从高层观点上考虑,你可以认为它们是线程相遇的控制点。
同步对象有两种状态:信号状态和非信号状态。线程停止在非信号状态,在捕捉到信号状态后
继续执行。
建立参数
FindFirstChangeNotification()声明如下:
HANDLE FindFirstChangeNotification(LPCTSTR lpPathName,
BOOL bWatchSubtree,
DWORD dwNotifyFilter);
lpPathName是包含要监视目录名的缓冲指针。bWatchSubtree布尔值指定是否路径中包含子树。
dwNotifyFilter使你能设置通知的实际触发规则。通过在dwNotifyFilter上使用可能的组合标志,你能
够决定监视哪种类型的文件系统事件。可用的标志是:
标志
描述
FILE_NOTIFY_CHANGE_FILE_NAME
文件被建立,删除,移动
FILE_NOTIFY_CHANGE_DIR_NAME
文件夹被建立,删除,移动
FILE_NOTIFY_CHANGE_ATTRIBUTES
文件或文件夹的任何属性改变
FILE_NOTIFY_CHANGE_SIZE
文件或文件夹的尺寸改变,仅当任何缓存写回到磁盘时才有这个感觉。
FILE_NOTIFY_CHANGE_LAST_WRITE
文件或文件夹的最近写入时间改变,仅当任何缓存写回到磁盘时才有这个感觉。
FILE_NOTIFY_CHANGE_SECURITY
文件或文件夹的任何安全描述符改变
显然在监视路径时这些事件必然发生。例如,如果你发起一个如下调用:
HANDLE hNotify = FindFirstChangeNotification(__TEXT("c://"), TRUE,
FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE);
在C驱动器上建立任何新文件,都将唤醒等待这个通知对象的线程。如果在第二个参数中指定FALSE,则
仅仅C驱动器根目录下的变化被感觉。调用FindFirstChangeNotification()产生的返回对象是在非信号
状态的,意思是,要求使用这个对象同步的线程将停止。
监视目录
现在我们已经知道了怎样建立一个变动通知对象,另一个问题是:这是否就能完全能监视目录
活动。实际上不能,就象其它监视活动一样,目录监视需要耐心,因此,你还必须准备捕捉任何时间发
生的事件。用软件术语讲,你需要在代码中设置某种循环。每当处理完一个事件后,你还要立即通知准
备处理事件的下一次发生或准备处理同时发生的其它事件。FindNextChangeNotification()就是此时要
使用的函数。
BOOL FindNextChangeNotification(HANDLE hChangeHandle);
下面是从示例应用中截取的一段代码,显示了函数的典型用法:
// 注意线程外设置的逻辑保护.
// 这是一段工作线程上摘下来的代码.
while(g_bContinue)
{
// 等待改变发生
WaitForSingleObject(hNotify, INFINITE);
// 改变已经发生, 通知主窗口.
// 使之有机会来刷新程序的UI.
// WM_EX_XXX 是应用定义的客户消息.
PostMessage(ci.hWnd, WM_EX_CHANGENOTIFICATION, 0, 0);
// 准备下一次改变到达
FindNextChangeNotification(hNotify);
// NB:
// 在这一点上由hNotify封装的同步对象处于非信号状态,所以当这个线程再次执行
// WaitForSingleObject()时,它将停止,直到新的改变发生和变成信号状态
}
如上所见,在循环内部没有使循环终止的事件。g_bContinue逻辑变量是线程外设置的全程变量,也就是
说,这段代码暗示有两个线程:主应用线程和涉及到通知对象的工作线程。
由于这段代码假定在调用FindFirstChangeNotification()后执行,因此在执行了一段后将停止在
WaitForSingleObject()的调用上,因为此时的通知对象已经变成非信号状态了。当满足hNotify通知对
象条件的事件发生时,对象的状态改变成信号状态,执行继续,并抛出一个客户消息到指定窗口,给它
一个刷新用户界面或做进一步处理的机会,然后再一次停止,等待新的事件发生。调用
FindNextChangeNotification()之后包含在hNotify中的同步对象的状态又变回到非信号状态。
在处理通知对象时,明智的选择是用不同的工作线程隔离所有等待事件的代码。这样能够避免
主程序不确定的阻塞。如果你不想要多线程应用,则应该使用MsgWaitForMultipleObjects()代替
WaitForSingleObject()来等待消息或事件。同时设置多个通知对象也是可能的。例如,你可能需要对相
同或不同驱动器上的不同目录进行分别监视,如果需要这样做,WaitForMultipleObjects()可以帮助你
一起同步化所有通知对象。
停止监视
释放通知对象必须调用FindCloseChangeNotification(),传递的唯一变量是由
FindFirstChangeNotification()建立的Handle:
BOOL FindCloseChangeNotification(HANDLE hChangeHandle);
总体示例
让我们看一个示例应用,这个程序概念性的展示探测器在屏幕后面的工作。这个程序让你选择
路径和建立监视整个子树的通知对象,所有变动通知的处理都在不同的线程中完成。每一次事件的感觉
都有消息抛给应用主窗口。作为示范,我们仅简单地增加一个包含当前时间的行到报告列表观察中。而
在实际工作中你可能需要做更多的处理。工作线程接受窗口Handle和监视路径,窗口Handle用于发送消
息到应用主窗口,使用用户定义的结构传递数据。程序的用户界面显示如图:
在你单击按钮时,通知对象使用上面调用的属性安装:
FILE_NOTIFY_CHANGE_FILE_NAME,
FILE_NOTIFY_CHANGE_DIR_NAME,
FILE_NOTIFY_CHANGE_ATTRIBUTES,
FILE_NOTIFY_CHANGE_SIZE
下面是需要加入框架的代码:
// 数据
HICON g_hIconLarge;
HICON g_hIconSmall;
bool g_bContinue; // 在WinMain()中应该设置为FALSE
const int WM_EX_CHANGENOTIFICATION = WM_APP + 1;
// 传递给线程的客户数据
struct CUSTOMINFO
{
HWND hWnd;
TCHAR pszDir[MAX_PATH];
};
typedef CUSTOMINFO* LPCUSTOMINFO;
在上面的代码中我们显式地声明了WM_EX_CHANGENOTIFICATION消息常量。一般在定义常量作为Windows消
息时,应该使用RegisterWindowMessage()函数,以确保系统唯一的消息号。然而在相关的单个应用中,
如果没有广播消息,使用基于WM_APP的显式声明常量是安全的。WM_APP是一个基本常量,它以后的消息
常量不能与Windows系统消息冲突。唯一的冒险是可能与来自其它应用的客户消息冲突,这一点在这个例
子中是不能发生的。
有一个新处理器要加到APP_DlgProc()中,它在通知对象感觉到变化时被唤醒,你好需要在
IDCANCEL处理器上做一点小的改变,用以在程序关闭时终止线程。
BOOL CALLBACK APP_DlgProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
OnInitDialog(hDlg);
break;
case WM_EX_CHANGENOTIFICATION:
UpdateView(hDlg);
break;
case WM_COMMAND:
switch(wParam)
{
case IDOK:
OnOK(hDlg);
return FALSE;
case IDCANCEL:
g_bContinue = false;
EndDialog(hDlg, FALSE);
return FALSE;
}
break;
}
return FALSE;
}
再有,这个处理器对于‘安装通知对象’按钮,仍然调用OnOK(),因为我没有改变按钮的ID,而仅仅是
标签改变了。
void OnOK(HWND hDlg)
{
TCHAR szDir[MAX_PATH] = {0};
GetDlgItemText(hDlg, IDC_EDIT, szDir, MAX_PATH);
SHInstallNotifier(hDlg, szDir);
}
OnOK()调用SHInstallNotifier()函数,这个函数建立一个CUSTOMINFO对象并传递给调用Notify()的线程
函数:
HANDLE SHInstallNotifier(HWND hwndParent, LPCTSTR pszDir)
{
DWORD dwID = 0;
CUSTOMINFO ci;
ZeroMemory(&ci, sizeof(CUSTOMINFO));
ci.hWnd = hwndParent;
lstrcpy(ci.pszDir, pszDir);
// 建立工作线程
g_bContinue = true;
HANDLE hThread = CreateThread(NULL, 0, Notify, &ci, 0, &dwID);
return hThread;
}
Notify()本身存在产生调用FindXXXChangeNotification()函数的地方,并在循环中保持对指定目录树的
监视:
DWORD WINAPI Notify(LPVOID lpv)
{
CUSTOMINFO ci;
ci.hWnd = static_cast<LPCUSTOMINFO>(lpv)->hWnd;
lstrcpy(ci.pszDir, static_cast<LPCUSTOMINFO>(lpv)->pszDir);
HANDLE hNotify = FindFirstChangeNotification(ci.pszDir, TRUE,
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_SIZE);
if(hNotify == INVALID_HANDLE_VALUE)
{
SPB_SystemMessage(GetLastError());
return 0;
}
while(g_bContinue)
{
WaitForSingleObject(hNotify, INFINITE);
PostMessage(ci.hWnd, WM_EX_CHANGENOTIFICATION, 0, 0);
FindNextChangeNotification(hNotify);
}
FindCloseChangeNotification(hNotify);
return 1;
}
当事件变为信号事件时,WM_EX_CHANGENOTIFICATION类型的消息被发送,引起UpdateView()函数调用:
void UpdateView(HWND hDlg)
{
TCHAR szTime[100] = {0};
HWND hwndList = GetDlgItem(hDlg,IDC_LIST);
GetTimeFormat(LOCALE_SYSTEM_DEFAULT, 0, NULL, NULL, szTime, 100);
AddStringToReportView(hwndList, szTime, 1);
}
你可以看到这段代码使用了AddStringToReportView()函数,这是我们在上一章中开发的发送串到报告观
察的函数。其伴随函数是MakeReportView(),在OnInitDialog()中被调用,以设置报告观察。
void OnInitDialog(HWND hDlg)
{
// 设置图标T/F 大/小图标)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
LPTSTR psz[] = {__TEXT("Date and Time"), reinterpret_cast<LPTSTR>(400)};
MakeReportView(GetDlgItem(hDlg, IDC_LIST), psz, 1);
}
要加#include resource.h到源文件的顶部,并编译连接这个应用。运行这个应用之后,你会注意到,如
果拷贝文件,你能够获得两个通知,删除可以有三个通知,如果删除所有标志,仅保留
FILE_NOTIFY_CHANGE_FILE_NAME,并且重复拷贝操作,通知数被减少到1,因为我们不再对属性和尺寸的
变化感兴趣。尽管如此,在删除操作时仍然有两个通知发生。为了查看为什么这样,按住Shift键后试着
删除文件—你将发现现在只有一个通知了。这种情况说明,此次删除文件是直接销毁文件而不是保存到
‘回收站’中。因此消除了正常文件删除的两个步骤中的一个—拷贝到‘回收站’,然后删除文件。
简单地删除文件在文件被实际删除时产生一个通知。
探测器和通知对象
概略地讲,探测器的行为与这个应用一样:它设置通知对象到当前显示的文件夹上,每次接收
到某个事件变动的通知,它都重新装入这个文件夹以响应那些变化。稍微思考一下,你就会认识到通知
对象的机理就是为探测器的需要而精确定制的。
探测器不是文件系统的监视例程,它需要知道当前被观察的文件夹中某些东西是否被改变了,
以及改变所影响的显示数据:文件和子文件夹名,属性,尺寸,日期,安全等。无论确切的操作如何,
重要的是已经发生了某些事情。这个机理似乎在系统与探测器性能方面是一个好的折中。
揭示文件系统的监视例程
就象我们已经看到的,通知对象的最大缺陷是对于实际发生事件所能提供的信息十分贫乏,通
知对象就像一个防盗和火灾报警铃:在铃声响时,你不知道是被盗了还是着火了,或者二者都发生了。
这个限制使它很难(不是不可能)应用于建立文件系统监视实用程序来使我们知道在整个系统中程序正在
处理哪些文件。
以后我们将考虑使用ICopyHook Shell扩展方法解决这个问题。即使这是一个重大进步,但是
与我们的最终目标仍然有一定的距离。
关于Windows NT的说明
到目前为止,我们并没有讨论不同的操作系统。你可能认为在Windows95,Windows98和
Windows NT之间没有什么重大的差别,但事实上,我们所希望的事情出现在Windows NT4.0以上版本中。
Windows NT的Win32 SDK输出和说明了ReadDirectoryChangesW()函数,它有一个类似于
FindFirstChangeNotification()的原型,但是有一个很大的差别:它使用活动发生的特殊信息和所涉及
的活动者信息填充一个缓冲。
关于ReadDirectoryChangesW()函数和通知对象的更多信息,一般来讲可以在Advanced Windows资料
中找到。
SHChangeNotify()函数
当系统变动的事情发生时,探测器本身能够感知到它们(特别是文件的变化),但是还不需要显
式地告知程序执行的什么变化。为了使这容易些,Shell API定义了SHChangeNotify()函数,他唯一的目
的就是通知探测器,某些系统设置已经被修改。概念上,SHChangeNotify()与通知对象产生相同的效果
,但是,它遵从不同的逻辑。因此,一个外部应用可以用来向探测器通报某些它所制造的变化。在响应
这个通知中,探测器将刷新用户界面。这是一个明显的例子,它说明了我们前面提到的在应用与Shell之
间的‘通道’。
调用SHChangeNotify()函数
这个函数定义在shlobj.h中,下面是它的原型:
void WINAPI SHChangeNotify(LONG wEventId,
UINT uFlags,
LPCVOID dwItem1,
LPCVOID dwItem2);
wEventId参数指定通知系统的事件,它接收一个或多个可能值的集合。最常用的值列表如下:
事件
描述
SHCNE_ASSOCCHANGED
一个文件关联的类型已经改变,没有指定具体是哪一个。
SHCNE_NETSHARE
一个本地文件夹正在被共享,这引起图标的变化。dwItem1应包含文件夹名,文件夹名可以是全路径名或
PIDL(见下面)。
SHCNE_NETUNSHARE
一个本地文件夹不再被共享。这引起图标改变。dwItem1中包含文件夹名(全路径名或PIDL)。
SHCNE_SERVERDISCONNECT
这台PC已经与服务器断开。dwItem1中包含服务器名。
SHCNE_UPDATEDIR
给出文件夹的内容已经改变,但是这个变化并不影响文件系统。dwItem1中包含文件夹名(全路径名或
PIDL)。
SHCNE_UPDATEIMAGE
系统图像列表中的图标已经改变。dwItem1包含图标索引。这引起探测器刷新用户界面,必要时绘制新图
标。探测器使用的所有图标都存储在称为‘系统图像列表’的全程结构中或‘探测器内部图标缓存’中
。在第四章中已经显示了怎样获得这个图像列表的Handle。
SHCNE_UPDATEITEM
一个非文件夹项已经改变。dwItem1中包含全文件名或PIDL。
这个事件列表不是完整的,我们将在后面给出剩余的标志。完整的标志列表可以参考MSDN库。
SHChangeNotify()的另外三个参数受wEventId变量指定的事件标识符影响,dwItem1和
dwItem2变量包含事件相关的值。uFlags参数用于表示dwItem1和dwItem2的类型。它可以表示DWORD数
(SHCNF_DWORD),PIDL(SHCNF_IDLIST),串(SHNCF_PATH)或指针名(SHCNF_PRINTER)。此外,uFlags还能指
出函数是否应该等待通知被处理完。SHCNF_FLUSH常量表示等待;SHCNF_FLUSHNOWAIT则表示不等待,使
函数立即返回。
SHChangeNotify()函数的作用
函数SHChangeNotify()是作为通知对象的补充功能而提出的,换句话说,它确实是绝对需要的函数
。这个函数努力提供与通知对象相同的功能(尽管使用不同的逻辑),但是,它并不仅仅限于文件系统对
象。正象我们在第五章中看到过的,Windows Shell是由文件对象组成,并且绝大多数文件对象都映射到
文件系统的物理实体上,但并不总是这样。比如文件对象‘我的计算机’和‘打印机’就没有对应的目
录。更进一步,即使你有一个连接到目录的文件夹,它们所包含的项也不一定是文件。也就是说,你可
以添加新项(或删除项)到文件夹对文件系统没有任何影响。此时探测器怎样感觉这些变化?
对这个问题有了深层次的了解之后,我们反而困惑了,是否能够设计出监视系统整个范围可能活动
的软件程序呢?后面我们将看到,命名空间扩展通过文件夹风格的接口可以用于显示很多东西。例如,
Internet客户端SDK有一个示例RegView,在探测器层次观察上加了一个新节点,就象一个普通文件夹一
样,其特征是所包含的内容是系统注册表,实际上是一两个文件的内容。探测器或其它工具怎样感觉这
里的变化?你可以写一段软件来钩住注册表的活动,但是,如果某人用另一个命名空间扩展替换了
RegView,并且做完全不同的操作,怎么办?
只要操作超出了传统文件系统关联的范围,我们就需要改变通知的方式。它不再是探测器本身感觉
变化,而是应用发送通知的事情。这就是SHChangeNotify()所设想的方式。某些用于调用
SHChangeNotify()而定义的事件可能是多余的,例如,事件SHCNE_CREATE可能没有用—它表示建立一个
新文件,但是探测器已经知道了这个事件,回想一下通知对象。反之,如果这个项不是文件系统对象,
你就必须调用SHChangeNotify(),使探测器知道这个变化:
SHChangeNotify(SHCNE_CREATE, SHCNF_IDLIST, pidl, NULL);
SHChangeNotify()的其他事件
现在SHChangeNotify()函数的基本概念已经有点清楚了,但是还需要时间来进一步补充说明。
下面是全部可以通过wEventId变量传递给函数的事件:
事件
描述
SHCNE_ATTRIBUTES
文件或文件夹的属性改变。dwItem1是文件或文件夹名(全路径名或PIDL)。
SHCNE_CREATE
已经建立了一个文件对象。dwItem1是文件对象名。
SHCNE_DELETE
已经删除了一个文件对象。dwItem1是文件对象名。
SHCNE_DRIVEADD
添加了一个驱动器。dwItem1是驱动器的根,有形式:C:/。
SHCNE_DRIVEADDGUI
添加了一个驱动器并且需要一个新窗口。dwItem1是驱动器的根,有形式:C:/。
SHCNE_DRIVEREMOVED
删除了一个驱动器。dwItem1是驱动器的根。
SHCNE_FREESPACE
驱动器上可用空间量变化。dwItem1是驱动器的根,有形式:C:/。
SHCNE_MEDIAINSERTED
存储介质已经插入到驱动器中。dwItem1是驱动器的根,有形式:C:/。
SHCNE_MEDIAREMOVED
存储介质已经从驱动器中删除。dwItem1是驱动器的根,有形式:C:/。
SHCNE_MKDIR
已经建立了一个文件夹。dwItem1是文件对象名。
SHCNE_RENAMEFOLDER
文件夹已经重命名。dwItem1是老文件夹名,dwItem2是新文件夹名。名字可以是全路径名或PIDLs。
SHCNE_RENAMEITEM
重命名了一个文件对象。dwItem1是老文件对象名,dwItem2是新文件对象名。
SHCNE_RMDIR
删除了一个文件对象。dwItem1是文件对象名。
使用SHChangeNotify()
在开始写命名空间扩展程序时,SHChangeNotify()函数是非常有用的,因为它使你向探测器隐藏了
一个项或文件夹可能不是实际文件系统对象这样一个事实。在第十六章中我们将开发一个命名空间扩展
程序,它以窗口本身作为文件夹的内容显示系统中当前存在窗口的信息。通过扩展与全程钩子的组合,
程序能够感知任何新窗口的建立,并使用SHCNE_CREATE标志调用SHChangeNotify(),并将使探测器能够
有规律地刷新这个客户文件夹的内容。
尽管在第二章中我们已经提到了,在这里我们还是不想论及Windows钩子。你可以参考MSDN库来得到更多
信息。
相反,一般的应用很少需要开发SHChangeNotify()服务。动态改变文件关联类型的程序可能需要使
用—即,它改变了程序用于处理特殊类文档的信息,这些信息存储在注册表的下面指定的位置:
HKEY_LOCAL_MACHINE
/Software
/Microsoft
/Windows
/CurrentVersion
/Extensions
为了通知探测器更新,你可以调用:
SHChangeNotify(SHCNE_ASSOCCHANGED, 0, NULL, NULL);
入侵Shell存储空间
如果你是一个有经验的Win32程序员,就会知道每一个进程都在它自己的地址空间中运行,并
且只有在这个地址空间内,内存地址才有一个一致的值。例如,你不能子类化由另一个进程建立的窗口
,因为新窗口过程的地址仅能指向你在另一个地址空间中可以看到它的地方。事实上,SetWindowLong()
函数阻止了这种努力,如果你试图这样做,它就返回零。
但是使你的程序代码映射进另一个应用进程的地址空间确实是可能的,这需要几个步骤。微软
之所以阻止这样做是因为它产生的潜在错误比使用其它更普通的编程技术要高,然而,访问另一个应用
的地址空间是安全的,只要你知道你打算做什么,你必须做什么,以及上面的全部知识。突破进程边界
没有阻碍和实质危险的事情。就象使用指针一样—如果不正常使用的话,能够引起Bug。
Shell是一个Win32进程,你可以使用与侵入Notepad应用一样的方法侵入它的内存空间。为什
么我们需要侵入Shell呢,这与你进入任何其他Win32或Win16进程的理由是一样的:需要改变(或过滤)一
个程序的行为。你是否已经注意到Notepad的拷贝有一种维持某些交叉会话设置的能力。运行Notepad,
并打开‘词重叠’模式,这个设置会永久保持,在每次打开时都会恢复。如果你要在Windows95或
Windows98下实现这个功能,你就必须客户化Notepad的标准行为。换句话说,你需要使你的代码侵入到
它的地址空间中。
在这一章的剩余部分,我们将显示三种进入探测器的地址空间的方法。头一个是传统的SDK技
术,如钩子和子类化。第二种探索不为人知的Shell API函数SHLoadInProc()。这两项技术都能在Win32
平台上工作,除了WindowsCE 。第三种选择仅在4.71以上版才可用,探索探测器与IE共有的特征:浏览
辅助对象。
强制进入方式
在认识到不通过菜单就没有办法建立文件夹的时候,我们考虑子类化探测器窗口。我不相信我
是唯一的没有找到梦幻组合键的人,我努力添加一个快速建立新文件夹的键盘加速器。即使在包含
Windows键盘快捷键列表的知识库文章中也没有提到建立新文件夹的组合键。我不知道别人怎样,我却发
现所有这些工作都相当失败:右击(或单击‘文件’菜单),然后选择两项,最后再次单击。
我的目的就是建立一个小应用程序,把它放进‘启动’文件夹,它安装一个系统范围的钩子,
用以保持属于一定类的窗口建立轨迹,在这个问题中,这个窗口类是探测器窗口‘ExploreWClass’。
我们使用了Spy++探测存在的窗口栈才找到这个类名字。
一旦获得了探测器窗口的Handle,我们就可以安装键盘钩子到建立这个窗口的特定线程。第二
个钩子响应键盘活动,和在键盘组合满足指定的规则时建立文件夹。这个任务可以分解成两个部分:
进入探测器
用与探测器相同的方法建立文件夹
在Win32中,没有太多使你的代码映射进另一个进程地址空间的方法。如果想使你的代码兼容于
Windows9x和WindowsNT,则只有一种可能:建立系统范围内的钩子。
为什么使用钩子
即使最终目标不是钩子而是简单地子类化窗口,如果这个窗口属于另一个进程的话,在做这个
子类化工作之前,你也必须安装一个钩子。不管你使用什么钩子,问题在于它施加到系统所有线程上的
是什么。
如第二章中介绍的概念,使用钩子意思是指定了一个回调函数,当一定的(相关于这个钩子的)
事件发生时系统将唤醒这个函数。如果想要监视所有运行中的进程,函数就必须驻留在一个DLL中,因为
系统需要将它映射进那些进程中。
进入到探测器内部
我们的程序将寻找正在建立的窗口(特别,寻找探测器窗口)。类型为WH_CBT的钩子过程需要在
程序启动时安装以便系统在窗口上执行任何活动(建立,删除,激活等)时触发这个函数:
g_hShellHook = SetWindowsHookEx(WH_CBT, ShellDll_MainHook, g_hThisDll, 0);
这个钩子在退出时必须被删除:
if(g_hShellHook != NULL)
UnhookWindowsHookEx(g_hShellHook);
显然,在整个系统范围内有一个钩子存在会影响到它的性能。任何系统范围内的钩子因为它们的存在都
将影响到系统性能。它使系统做附加的工作,这毋庸置疑地等比例缩减系统性能。因此我们建议,系统
的钩子要尽可能地小。我这里是最小的一个,它极大地缩减了性能损失的风险。这个钩子的过程如下:
LRESULT CALLBACK ShellDll_MainHook(int nCode, WPARAM wParam, LPARAM lParam)
{
TCHAR szClass[MAX_PATH] = {0};
// 任何钩子过程都有的典型开头
if(nCode < 0)
return CallNextHookEx(g_hShellHook, nCode, wParam, lParam);
// 系统正在建立窗口。注意钩子从这段代码内被唤醒CreateWindow() and CreateWindowEx()。
// 在这一点上,窗口已经存在,HWND是有效的,即使我们仍然在建立过程中间。
if(nCode == HCBT_CREATEWND)
{
// 获得窗口的HWND
HWND hwndExplorer = reinterpret_cast<HWND>(wParam);
//比较'ExploreWClass'和安装键盘钩子
GetClassName(hwndExplorer, szClass, MAX_PATH);
if(!lstrcmpi(szClass, __TEXT("ExploreWClass")))
InstallKeyboardHook(hwndExplorer);
}
return CallNextHookEx(g_hShellHook, nCode, wParam, lParam);
}
每当有窗口建立时都执行这段代码。如果窗口类名与探测器窗口类名匹配(为ExploreWClass),则安装键
盘钩子,在这一点上,我们就已经进入到探测器的地址空间了。注意,‘键盘’钩子可以是局部于探测
器线程的,它拥有窗口类ExploreWClass,不必在整个系统上钩住键盘活动,因为当我们在建立新文件夹
时,输入焦点自然在探测器上(在编写辅助对象那一节我们将进一步说明)。
怎样建立新文件夹
为了使钩子代码映射到一个进程的地址空间,充分的条件是从进程内部唤醒一个系统范围的钩
子过程。现在这个问题缩减到要建立一个新文件夹。显然我们希望获得与手动建立习惯相同的操作方式
。所以最容易的方法是精确地重复探测器使用‘新建|文件夹’菜单时的操作。
你可能要问为什么不选择采用前面讨论过的方法—也就是说,为什么不使用Shell API,取得当前目录和
建立新目录。原因就是在这种情况下那些方法已经失效了。首先,你怎么知道探测器当前显示的是哪一
个文件夹?GetCurrentDirectory()返回的名字是不完备的。其次,很多特殊文件夹不允许建立子文件夹
,如果这样做将引起麻烦。
我论述了探测器在响应发送到主窗口的WM_COMMAND消息时建立新文件夹的原理。为了便于研究
,我写了一段程序,子类化了ExploreWClass窗口,以便在每次处理WM_COMMAND消息时探测它的参数。通
过这个方法,我们发现,要请求探测器建立新文件夹,你只需要向这个窗口发送如下消息即可:
PostMessage(hwndExplorer, WM_COMMAND, 29281, 0);
魔力数29281是‘新建|文件夹’菜单项的ID。这是非官方信息,而且它可能在新版本的Shell中被改动。
但是,现在,它能与Windows9x和WindowsNT一起工作。如果将来这个数改变,除非Shell的本质结构变化
,你只需要简单地找出新的ID号就可以了。这个数从4.00到4.71一直都没有变。
安装了键盘钩子后,Shell可以响应一键建立新文件夹操作。我们选择了F12键—没有什么特殊
的原因,可以自由地采用任何其它的键。当键盘钩子过程感觉到F12按下时,它简单地恢复探测器窗口,
和发送一个消息。
示例程序
正象说明的那样,示例程序必然分为两个部分:DLL和可执行程序。首先是DLL程序源码,包含
了两个钩子,它是基于DLL框架的,我们取名为ExpHook工程(project)。全程变量和函数声明加到
ExpHook.h文件中:
/*---------------------------------------------------------------*/
// 原型节
/*---------------------------------------------------------------*/
HHOOK g_hShellHook;
HHOOK g_hKeybHook;
HWND g_hwndExplorer;
void InstallKeyboardHook(HWND hwnd);
void APIENTRY ShellDll_Hook();
void APIENTRY ShellDll_Unhook();
LRESULT CALLBACK ShellDll_KeybHook(int nCode, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK ShellDll_MainHook(int nCode, WPARAM wParam, LPARAM lParam);
自然,原型的实现在ExpHook.cpp,这些函数正好实现了我们讨论过的原理:
// 设置钩子来感觉探测器的启动
void APIENTRY ShellDll_Hook()
{
g_hShellHook = SetWindowsHookEx(WH_CBT, ShellDll_MainHook, g_hThisDll, 0);
}
void APIENTRY ShellDll_Unhook()
{
if(g_hKeybHook != NULL)
UnhookWindowsHookEx(g_hKeybHook);
if(g_hShellHook != NULL)
UnhookWindowsHookEx(g_hShellHook);
}
// 列表中的ShellDll_MainHook()钩子插入这段代码
LRESULT CALLBACK ShellDll_KeybHook(int nCode, WPARAM wParam, LPARAM lParam)
{
// 任何钩子过程典型的开头
if(nCode < 0)
return CallNextHookEx(g_hKeybHook, nCode, wParam, lParam);
// 一般这段代码在键盘按下和松开时都执行.状态变换信息存储在lParam的最高两位中
//因此,我们仅处理一次键盘操作。
if((lParam & 0x80000000) || (lParam & 0x40000000))
return CallNextHookEx(g_hKeybHook, nCode, wParam, lParam);
if(wParam == VK_F12)
{
//取得探测器窗口Handle和发送消息。
g_hwndExplorer = FindWindow("ExploreWClass", NULL);
PostMessage(g_hwndExplorer, WM_COMMAND, 29281, 0);
}
return CallNextHookEx(g_hKeybHook, nCode, wParam, lParam);
}
// 安装键盘钩子
void InstallKeyboardHook(HWND hwnd)
{
g_hwndExplorer = hwnd;
DWORD dwThread = GetWindowThreadProcessId(g_hwndExplorer, NULL);
g_hKeybHook = SetWindowsHookEx(WH_KEYBOARD, ShellDll_KeybHook,
g_hThisDll, dwThread);
}
为了使这个库输出我们需要的函数,还应该把这些行加到.def文件:
EXPORTS
ShellDll_Hook @2
ShellDll_Unhook @3
ShellDll_KeybHook @4
ShellDll_MainHook @5
这就是我们需要的DLL,编译连接之后移到主程序一起,运行后将在托盘通知区域增加一个图标,便于你
容易地卸载这个钩子。除了建立图标,主程序本身还包含安装和卸载WH_CBT钩子功能。由于这个应用程
序的特性,不象一般的应用程序那样有多少客户需求。首先建立一个基于对话框的应用ExpFold,加一个
#include语句,包含DLL函数定义:
/*---------------------------------------------------------------*/
// 包含节
/*---------------------------------------------------------------*/
#include "ExpFold.h"
#include "ExpHook.h"
其次需要两个新常量:一是客户消息,当托盘图标被点击时发送的消息,再有就是图标的ID:
// Data
const int WM_MYMESSAGE = WM_APP + 1; // 托盘图标消息
const int ICON_ID = 13;
HICON g_hIconLarge;
HICON g_hIconSmall;
HINSTANCE g_hInstance;
新的全程变量用于存储应用实例的Handle ,这在后来调用LoadMenu()时是必须的。下面是对WinMain()
需要作出的改变:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevious,LPTSTR lpsz, int iCmd)
{
// 保存全程数据
g_hInstance = hInstance;
g_hIconSmall = static_cast<HICON>(LoadImage(hInstance, "APP_ICON",
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CXSMICON), 0));
// 建立不可视对话框获得来自图表的消息
HWND hDlg = CreateDialog(hInstance, "DLG_MAIN", NULL, APP_DlgProc);
// 在托盘区域显示图标
TrayIcon(hDlg, NIM_ADD);
// 安装探测器钩子
ShellDll_Hook();
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
if(!IsDialogMessage(hDlg, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// 卸载钩子
ShellDll_Unhook();
// 删除图标
TrayIcon(hDlg, NIM_DELETE);
DestroyWindow(hDlg);
DestroyIcon(g_hIconSmall);
return 1;
}
与显示的对话框不一样,这个应用通过调用CreateDialog()而不是DialogBox()建立了一个不可视对话框
,对话框过程如下:
BOOL CALLBACK APP_DlgProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_COMMAND:
switch(wParam)
{
case IDCANCEL:
PostQuitMessage(0);
return FALSE;
}
break;
case WM_MYMESSAGE:
if(wParam == ICON_ID)
{
switch(lParam)
{
case WM_RBUTTONUP:
ContextMenu(hDlg);
break;
}
}
break;
}
return FALSE;
}
TrayIcon()函数由WinMain()在对话框设置后调用,它显示一个图标到任务条托盘,退出时要删除它:
// 在托盘区域显示图标
BOOL TrayIcon(HWND hWnd, DWORD msg)
{
NOTIFYICONDATA nid;
ZeroMemory(&nid, sizeof(NOTIFYICONDATA));
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = hWnd;
nid.uID = ICON_ID;
nid.uFlags = NIF_TIP | NIF_ICON | NIF_MESSAGE;
nid.uCallbackMessage = WM_MYMESSAGE;
nid.hIcon = g_hIconSmall;
lstrcpyn(nid.szTip, __TEXT("Explorer's Hook"), 64);
return Shell_NotifyIcon(msg, &nid);
}
最后当用户点击托盘图标时ContextMenu()函数被调用。为了正常工作,需要加一个IDR_MENU类型的菜单
资源到工程(project)中,菜单中应该包含一个项‘关闭’,它的ID是IDCANCEL。
// 显示图标的关联菜单
void ContextMenu(HWND hwnd)
{
POINT pt;
GetCursorPos(&pt);
HMENU hmenu = LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MENU));
HMENU hmnuPopup = GetSubMenu(hmenu, 0);
SetMenuDefaultItem(hmnuPopup, IDOK, FALSE);
SetForegroundWindow(hwnd);
TrackPopupMenu(hmnuPopup, TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, NULL);
SetForegroundWindow(hwnd);
DestroyMenu(hmnuPopup);
DestroyMenu(hmenu);
}
程序活动
包含#include "resource.h",并编译程序后,连接库exphook.lib,将获得.exe和.dll两个文
件。而后建立可执行文件的快捷方式,把这个快捷方式拷贝到‘启动’文件夹下。
这个程序可以通过右击托盘图标并选择‘关闭’删除。只要它被安装,就能钩住每一个探测器窗口
,并在响应线程中建立和安装键盘钩子,这个键盘过程查寻F12 并向相应窗口发送消息。
进入Shell存储空间
把外部代码注入到Shell地址空间有两种方法。侵入(我们已经看到的),和邀请(一种友好的方
法,仅当我们可以找到一种办法这么做)。前一种方式下,主程序完全不知道它正在运行什么。相反,后
一种方法主程序直接控制每一件事情的发生。
Windows Shell 正是通过邀请而不是侵入提供了一种方法来进入它的存储空间—Shell API提
供了一个经常不受重视的函数SHLoadInProc(),它定义在shlobj.h中,并且有令人惊讶的能力。然而,
这个函数的说明资料确是十分贫乏的,根据仅有的资料,你可能会怀疑这个函数是否有想象的能力。正
是为了说明它的能力,在这一节我们打算建立一个DLL的例子,这个例子允许我们恢复和置换Windows‘
开始’按钮。在开始这个任务之前,更进一的说明是必要的。
SHLoadInProc()函数
在坚固的外壳下,SHLoadInProc()函数将你的模块装入到Shell的地址空间。这实际上是你在
上一节中努力要达到的目的。SHLoadInProc()装入模块,然后保留它独自作任何操作。下面是Internet
客户端SDK中对它的描述资料:
WINSHELLAPI HRESULT WINAPI SHLoadInProc(REFCLSID rclsid);
在关联的Shell进程内建立一个指定对象的实例,如果成功,返回NOERROR,否则返回OLE定义的错误结果
。
rclsid
要建立的对象类的CLSID。
这个资料是绝对正确的,问题是一点也没有提到‘对象类’的结构。哪些接口是必须要实现的,哪些特
殊的规则是必须要遵循的,没有特定接口的COM服务器能做什么,如果不要求特定的接口,对象如何启动
工作。
所有这些问题在资料中都没有回答,诚实地讲,我们不能理解怎样做才能使这个函数工作。
最小COM对象
SHLoadInProc()函数是把我们的代码引入Shell地址空间最快和最有效的方法,但是,这个代
码必须是一个COM对象。然而,为了探索这个函数,我们没有必要建立一个完整的COM对象—只使用部分
COM和DLL代码。但是它必须实现COM服务器的规则(因而需要自注册和一个CLSID),而实际上它更象一个
老的DLL而不象进程内COM服务器。
怎样建立COM对象
一个进程内COM对象是一个DLL,即,它有一个DllMain()函数。更重要的是,一个COM对象输出
四个全程函数,这些函数是由与进程内对象一同工作的任何容器来操作的。它们是:
DllGetClassObject()
DllCanUnloadNow()
DllRegisterServer()
DllUnregisterServer()
后两个是自动注册和注销函数,如果承诺手动注册和注销,可以不用实现这两个函数。我们的COM对象现
在就减去这两个函数,此时只有两个全程函数输出:DllGetClassObject()和DllCanUnloadNow()。
DllGetClassObject()函数的作用
任何COM对象的客户必须首先加载包含这个COM对象的库,然后通过DllGetClassObject()函数
取得接口指针:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv);
重点是在类对象被装入后,总是立即调用这个函数。换言之,这段代码总能得到执行。更重要的是,它
是在Shell的关联空间中执行的。
满足客户的期望
一般,加载类对象的模块调用DllGetClassObject(),请求IClassFactory接口,我们的客户—
当前情况下是探测器—期望一个由DllGetClassObject()函数返回的接口指针。由于我们并没有实现这个
接口,怎样才能应对这个期望呢?
对我们而言,显式地说明请求的类不可用就足够了,这仅需简单地返回一个适当的错误码:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
return CLASS_E_CLASSNOTAVAILABLE;
}
上面是一种DllGetClassObject()函数可能的实现,它产生的一个感觉是不支持特定的接口。
使用Shell地址空间
除了返回错误码以外,这个函数还可以同任何驻留在Shell地址空间上的对象一道做任何想要
做的工作。在DllGetClassObject()被调用时,我们就已经在Shell的关联空间中了,因此,这将允许我
们子类化‘开始’按钮。在花一点时间讨论DllCanUnloadNow()函数之后,我们将很快开始证实这一点。
DllCanUnloadNow()函数的作用
通过DllGetClassObject()加载COM对象的客户模块调用DllCanUnloadNow()函数来保证DLL可以
被安全地卸载和释放。探测器周期地执行这个检查,尽管这个周期可能延迟十秒或十分钟。在第十五章
的Shell扩展中我们将展开讨论这一点。
如果DllCanUnloadNow()返回S_OK,则宿主DLL将被卸载。如果它总是返回S_FALSE,或DLL没有输出
具有这个名字的函数,则只有在主应用程序调用CoUninitialize()函数关闭COM库时,这个DLL库才被释
放。因为此时的主应用是探测器,所以调用CoUninitialize()函数之后的一段时间才发生DLL库被释放操
作。
COM对象源码
接下来是这个‘假冒’COM对象的最小源代码,用来结合SHLoadInProc()函数,并且作为一个
例程的种子,它将逐步成长为‘开始’按钮子类化的应用。在VC++ 中建立一个新的Win32动态链接库起
名为‘Start’(选择‘简单DLL’选项),添加下述代码到start.ccp:
#include "start.h"
HINSTANCE g_hInstance;
BOOL APIENTRY DllMain(HINSTANCE hInstance,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
g_hInstance = hModule;
return TRUE;
}
/*---------------------------------------------------------------------------*/
// DllGetClassObject
// COM 进程内对象住函数
/*---------------------------------------------------------------------------*/
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
// 在这了做一些操作
return CLASS_E_CLASSNOTAVAILABLE;
}
/*---------------------------------------------------------------------------*/
// DllCanUnloadNow
// 确认卸载COM库
/*---------------------------------------------------------------------------*/
STDAPI DllCanUnloadNow()
{
return S_OK;
}
start.h头文件包含有上面定义的‘假冒’COM对象的CLSID:
#include <windows.h>
#include <windowsx.h>
#include <objbase.h>
#include <shlobj.h>
DEFINE_GUID(CLSID_NewStart, 0x20051998, 0x0020,0x0005,
0x19, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
对于库的输出函数,还需要定义start.def文件:
LIBRARY START
EXPORTS
DllCanUnloadNow @1 PRIVATE
DllGetClassObject @2 PRIVATE
作为这一节的结束,这里是一段代码,它使用SHLoadInProc()函数加载COM到探测器地址空间:
void DoGoInsideExplorer()
{
const CLSID clsid = {0x20051998,0x0020,0x0005,
{0x19,0x98,0x00,0x00,0x00,0x00,0x00,0x00}};
SHLoadInProc(clsid);
}
注册COM对象
本质上有两种方法可以注册COM对象:在DllRegisterServer()函数中插入代码,或手动地注册—最
好用一个注册脚本。现在让我们关注一下这两种方法。先从最简单的注册脚本开始。
下面是一个脚本文件REG的内容,它由注册表编辑器自动处理。它添加两个注册CLSID的键到
HKEY_CLASSES_ROOT的CLSID节点下,并且存储实现COM的可执行文件名。
REGEDIT4
[HKEY_CLASSES_ROOT/CLSID/{20051998-0020-0005-1998-000000000000}]
@= "Start Button"
[HKEY_CLASSES_ROOT/CLSID/{20051998-0020-0005-1998-000000000000}/InProcServer32]
@= "C://Chap07//Source//Start//start.dll"
"ThreadingModel" = "Apartment"
当然,一定要保证注册的路径是实际文件所在的路径。实际,一个需要加到CLSID下的键应该具有封装为
如下用括号括起来的CLSID名字:
HKEY_CLASSES_ROOT
/CLSID
/{20051998-0020-0005-1998-000000000000}
此外,我们还需要在这个键下添加另一个称为InProcServer32的键,其默认值指向实际服务器的名。值
ThreadingModel指示必须的线程模型。要注册这个服务器,需要在探测器上双击这个REG文件,或使用注
册编辑器引入它。
正规的方法是把这些内容全部编写进DllRegisterServer()函数中,这要求我们使用Win32注册
表API编程。我们在第十章中将说明在Shell4.71以上版中包含了涉及到注册表的新的高层函数集。在这
里我们可以使用这些函数,但是,这段代码将只能在4.71或以上版使用。下面的代码使用传统的Win32
注册表API:
STDAPI DllRegisterServer()
{
TCHAR szSubKey[MAX_PATH] = {0};
TCHAR szCLSID[MAX_PATH] = {0};
TCHAR szModule[MAX_PATH] = {0};
HKEY hKey;
DWORD dwDisp;
//设置CLSID
lstrcpy(szCLSID, __TEXT("{20051998-0020-0005-1998-000000000000}"));
// 取得模块名
GetModuleFileName(g_hInstance, szModule, MAX_PATH);
// HKCR: CLSID/{...}
wsprintf(szSubKey, __TEXT("CLSID//%s"), szCLSID);
LRESULT lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT, szSubKey, 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, &dwDisp);
if(lResult == NOERROR)
{
TCHAR szData[MAX_PATH] = {0};
wsprintf(szData, __TEXT("Start Button"), szModule);
lResult = RegSetValueEx(hKey, NULL, 0, REG_SZ,
reinterpret_cast<LPBYTE>(szData), lstrlen(szData) + 1);
RegCloseKey(hKey);
}
// HKCR: CLSID/{...}/InProcServer32
wsprintf(szSubKey, __TEXT("CLSID//%s//InProcServer32"), szCLSID);
lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT, szSubKey, 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, &dwDisp);
if(lResult == NOERROR)
{
lResult = RegSetValueEx(hKey, NULL, 0, REG_SZ,
reinterpret_cast<LPBYTE>(szModule), lstrlen(szModule) + 1);
TCHAR szData[MAX_PATH] = {0};
lstrcpy(szData, __TEXT("Apartment"));
lResult = RegSetValueEx(hKey, __TEXT("ThreadingModel"), 0, REG_SZ,
reinterpret_cast<LPBYTE>(szData), lstrlen(szData) + 1);
RegCloseKey(hKey);
}
return S_OK;
}
COM对象在DEF文件中输出了DllRegisterServer(),可以使用系统实用程序regsvr32.exe来进行注册:
regsvr32.exe <full_server_name>
注销COM对象
REG脚本不允许注销设置,所以要这样做的唯一方法是通过注册表编辑器的帮助手动删除。如果安装
了Windows脚本环境(WSH)则可以有另一种方案,写一个VB脚本或Java脚本函数,使用WSH注册表对象来删
除键和值。由于使用脚本语言比REG更灵活和通用,因此这种方法在未来将可能成为流行的方法。
说到脚本语言,其价值在于用ATL写的COM对象可以用RGS文件提供注册和注销。RGS脚本并不是注册
表编辑器REG文件的增强版。
返回到我们关于API函数的讨论,要使COM对象自己注销,你应该使用下面的编码:
STDAPI DllUnregisterServer()
{
TCHAR szSubKey[MAX_PATH] = {0};
TCHAR szCLSID[MAX_PATH] = {0};
TCHAR szModule[MAX_PATH] = {0};
HKEY hKey;
DWORD dwDisp;
// 设置CLSID
lstrcpy(szCLSID, __TEXT("{20051998-0020-0005-1998-000000000000}"));
// 打开HKCR
LRESULT lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT, "", 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, &dwDisp);
if(lResult == NOERROR)
{
wsprintf(szSubKey, __TEXT("CLSID//%s//InProcServer32"), szCLSID);
RegDeleteKey(hKey, szSubKey);
wsprintf(szSubKey, __TEXT("CLSID//%s"), szCLSID);
RegDeleteKey(hKey, szSubKey);
RegCloseKey(hKey);
}
return S_OK;
}
在这个函数中,我们打开HKEY_CLASSES_ROOT,和删除其中的键。RegDeleteKey()在Windows9x和Windows
NT下稍微有点差别。前者允许包含子键的键,这种递归删除在NT下不支持,如果给定键不空,这个函数
失败。注意‘空’意思是没有子键,而不是他表示的值。由于上述代码首先删除最内部的键,所以在两
个平台上都能工作。
输出DllUnregisterServer()的COM对象可以由regsvr32.exe系统实用程序加以注销:
regsvr32.exe /u <full_server_name>
一个崭新的开始按钮
为了说明SHLoadInProc()的能力,我们给出了扩展DllGetClassObject()函数的代码,建立了
一个崭新的‘开始’按钮,它具有不同的图像和菜单。我们将通过以下步骤达到这个目标:
取得‘开始’按钮的Handle
置换它的图像
子类化按钮窗口,改变菜单和光标
建立和显示客户化菜单
而后,你就可以控制‘Windows’键和‘Ctrl+Esc’组合键。你也可以限制它们,让它们显示标准的‘开
始’菜单,或用新的客户化的菜单连接它们。期望的结果显示如下:
第一件事情是建立在DllGetClassObject()中调用的主函数。这是进入Shell未可知领域的第一步。
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
InstallHandler();
return CLASS_E_CLASSNOTAVAILABLE;
}
/*-------------------------------------------------------*/
// InstallHandler
// 置换开始菜单和安装钩子
/*-------------------------------------------------------*/
void InstallHandler()
{
if(g_bInstalled)
{
int irc = MessageBox(HWND_DESKTOP,
__TEXT("The extension is installed. Would you like to uninstall?"),
__TEXT("Start"), MB_ICONQUESTION | MB_YESNO | MB_SETFOREGROUND);
if(irc == IDYES)
UninstallHandler();
return;
}
// 记住是否已经安装了处理器
g_bInstalled = TRUE;
// 设置新的‘开始’按钮
SetNewStartButton(TRUE);
}
在完成以后并需要恢复标准行为时,调用卸载其函数:
void UninstallHandler()
{
// 恢复标准设置
SetNewStartButton(FALSE);
// 处理器卸载
g_bInstalled = FALSE;
}
在探测器调用DllCanUnloadNow()来探索我们的库是否可以卸载时,这个处理器的存在现在就变成了关键
因素。在这一节的最后我们要做的则是确保在安装这个处理器期间没有有威胁的事件发生。
STDAPI DllCanUnloadNow()
{
return (g_bInstalled ? S_FALSE : S_OK);
}
给出了这个函数之后,现在我们就可以操作安装和卸载‘开始’按钮的处理器了,现在让我们看一下完
成任务所需要的几个步骤。
取得按钮的Handle
因为我们正在变换一个熟知的Windows界面部件,其结果是明显的,但是事实上我们正在做进
入Shell地址空间这个工作的最艰难部分。剩余的工作仅简单地是把Win32编程技术应用于某些Shell对象
而已。注意,这里真正重要的是我们的最小COM对象(在start.dll中)正工作在与探测器相同的环境中。
‘开始’按钮是一个普通的‘Button’类窗口,就象在Spy++中显示的那样:
使用Spy++搜索工具在大量窗口栈中查找这个按钮是比较容易的:只要拖动搜索器到期望的窗口,它将在
窗口列表中被选中。这个搜索工具在‘搜索| 查找窗口…’菜单下。
如果想要编程恢复子窗口的Handle,应该使用FindWindowEx(),而不是FindWindow(),其差别
在于,前者可以指定搜索开始的根窗口。在我们的情况下,‘开始’按钮是任务条的子窗口,在系统中
它是Shell_TrayWnd窗口类的唯一窗口。
hwndTray = FindWindowEx(NULL, NULL, "Shell_TrayWnd", NULL);
hwndStart = FindWindowEx(hwndTray, NULL, "Button", NULL);
上面片断首先恢复任务条窗口的Handle,然后恢复类为‘Button’的第一个子窗口。
不管它们的外貌如何,你所看到的任务条上的其他‘按钮’都不是按钮。事实上它们也不是窗
口—它们仅仅是类按钮的tab控件。
置换图像
再次观察上面的Spy++截图,你会注意到‘开始’按钮没有标题,也就是说,‘开始’这个词(
对非英文版的Windows是被本地化的)是一个bitmap图像。然而在shell32.dll,或explorer.exe,或任何
其它系统模块中你都不能找到这个图像的踪迹。这个图像是通过合并Windows标记图和一个资源中的串动
态地建立的。二者均被存储在explorer.exe之中。
Windows标记图的ID是143,而‘开始’串在串表中的位置ID是578。
组合图像在内存设备关联中通过拷贝Windows标记图和绘制文字建立。
探测器资源的反向工程
如果查看探测器资源,你将发现,很多各种对话框中流行的图像(例如,‘任务条属性’对话
框中显示的)都是动态建立的,以节省存储空间。事实上,explorer.exe文件中仅包含某些元素图像,而
不是最终显示的结果图像。
要浏览某个应用的资源,下面是建议德操作步骤:
建立要浏览文件的备份,这是必要的,因为这个文件可能正在使用中。
用VC++打开它,一定要保证在‘打开’时的下拉框中指定‘资源’条件。
在Windows9x下,IDE将警告,不能更新资源。不管它。
在显示出资源树后,很容易就可以把它们保存到不同文件中。只需右击希望保存的资源和选择‘输出…
’。这个操作仅仅适用于映射到文件的资源,如Bitmap,图表和光标。以及象AVI文件那样的客户资源。
你不能保存对话框模版到文本文件。
开始按钮的风格
‘开始’按钮有BS_BITMAP风格,即,它的表面由图像而不是通常的文字覆盖,(你可以通过在
Spy++列表中右击窗口,然后选择‘属性…|风格’来证实这一点)。调用下面函数可以很容易地得到这个
图像的Handle :
g_hbmStart = reinterpret_cast<HBITMAP>(SendMessage(hwndStart,
BM_GETIMAGE, IMAGE_BITMAP, 0));
置换这个图像也不太困难。首先是用LoadImage()函数从应用的资源中装载一个新图像,其次
SendMessage()函数允许我们把图像赋值给具有BS_BITMAP风格的按钮。lParam参数引用由LoadImage()返
回的Handle。
HBITMAP hbm = reinterpret_cast<HBITMAP>(LoadImage(g_hInstance,
MAKEINTRESOURCE(IDB_NEWSTART), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE));
SendMessage(hwndStart, BM_SETIMAGE,IMAGE_BITMAP, reinterpret_cast<LPARAM>(hbm));
我们在示例中使用的图像,其ID是IDB_NEWSTART,在resource.h文件中定义:
对这个示例我们选择了一个类似超链的图像,为了简化编码,我们把这个图像放进模块的资源中。这个
图像与‘开始’按钮有相同的尺寸(48X16),你可以使用任何你喜欢的图像,但是建议你保持这个尺寸。
简单地改变图像不能必然地导致按钮表面的立即刷新,按钮需要重新绘制它的非客户区域来反映我们所
作的改变。我们可以通过调用SetWindowPos()函数强制执行这个操作:
SetWindowPos(hwndStart, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOMOVE | SWP_DRAWFRAME);
为了看到工作的效果,我们需要实现SetNewStartButton()函数,它把我们前面给出的所有代码段穿成串
,如下所示:
void SetNewStartButton(BOOL fNew)
{
// 取得‘开始’按钮的 handle
HWND hwndTray = FindWindowEx(NULL, NULL, "Shell_TrayWnd", NULL);
HWND hwndStart = FindWindowEx(hwndTray, NULL, "Button", NULL);
// 改变图像
g_hbmStart = NewStartBitmap(hwndStart, fNew);
}
取得按钮的Handle是微不足道的工作,而使用一种方法置换其图像的操作就要求有一点逻辑了。这就是
为什么我们将这段代码分离出来组成NewStartBitmap()函数调用的原因:
HBITMAP NewStartBitmap(HWND hwndStart, BOOL fNew)
{
if(!fNew)
{
if(g_hbmStart)
SendMessage(hwndStart, BM_SETIMAGE, IMAGE_BITMAP,
reinterpret_cast<LPARAM>(g_hbmStart));
// 刷新按钮响应变化
SetWindowPos(hwndStart, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOMOVE | SWP_DRAWFRAME);
return NULL;
}
// 保存当前图像
g_hbmStart = reinterpret_cast<HBITMAP>(SendMessage(hwndStart,
BM_GETIMAGE, IMAGE_BITMAP, 0));
// 装如何设置新图像
HBITMAP hbm = reinterpret_cast<HBITMAP>(LoadImage(g_hInstance,
MAKEINTRESOURCE(IDB_NEWSTART), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE));
SendMessage(hwndStart, BM_SETIMAGE, IMAGE_BITMAP,
reinterpret_cast<LPARAM>(hbm));
// 刷新按钮享用变化
SetWindowPos(hwndStart, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOMOVE | SWP_DRAWFRAME);
return g_hbmStart;
}
现在有了一个需要建立工作DLL的全部代码,注册之后,就可以使用DoGoInsideExplorer()这样的函数来
调用SHLoadInProc(),并且可以使这个假冒的COM对象进入探测器的地址空间。
子类化窗口
改变‘开始’按钮图像是一个重大结果,但是我们还可以达到更大的。下一个目标是改变这个
按钮的行为,即:
设置一个手形光标代替通常的光标
删除关联菜单
客户化窗口工具标签
最激动的事情是在‘开始’按钮上的点击产生一个不同的菜单。
手形光标
由于我们已经使‘开始’按钮像一个超链了,所以它上面的光标也应该应该变化成手指指针的
形状,就象通常出现在HTML链上一样。使用与上面在探测器上使用的相同技术,从IE的资源中取出这个
光标,称之为IDC_HAND(在我们的应用资源中)。
每次Windows需要为窗口显示光标时,他都发送WM_SETCURSOR消息。如果应用不处理它,Windows为
这个类设置预定义的光标。在使用RegisterClass()或RegisterClassEx()注册类时,定义类的光标—它
是WNDCLASS(或WNDCLASSEX)结构的一个字段。对于系统控件(如按钮),预定义的光标是标准的矢量,唯
一的例外是编辑控件。
如果我们打算开始处理由系统发送到‘开始’按钮的消息,我们现在就需要子类化它。从添加
代码到SetNewStartButton()开始,安装一个称为NewStartProc()的过程:
void SetNewStartButton(BOOL fNew)
{
// 取得‘开始’按钮的 handle
HWND hwndTray = FindWindowEx(NULL, NULL, "Shell_TrayWnd", NULL);
HWND hwndStart = FindWindowEx(hwndTray, NULL, "Button", NULL);
// 改变图像
g_hbmStart = NewStartBitmap(hwndStart, fNew);
// 子类化按钮
if(fNew)
{
if(!g_bSubclassed)
{
g_pfnStartProc = SubclassWindow(hwndStart, NewStartProc);
g_bSubclassed = TRUE;
}
}else{
if(g_pfnStartProc != NULL)
SubclassWindow(hwndStart, g_pfnStartProc);
g_bSubclassed = FALSE;
}
}
当鼠标指针在这个窗口上时为了显示不同的光标,我们需要指令它响应WM_SETCURSOR消息,这是由我们
子类化‘开始’按钮时窗口过程接收的消息:
LRESULT CALLBACK NewStartProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_SETCURSOR:
SetCursor(LoadCursor(g_hInstance, MAKEINTRESOURCE(IDC_HAND)));
return 0;
}
return CallWindowProc(g_pfnStartProc, hwnd, uMsg, wParam, lParam);
}
在处理了WM_SETCURSOR消息之后,从这个窗口过程返回是极其重要的。如果不这样,则Windows将终止,
执行默认的消息代码并恢复矢量光标。
删除标准关联菜单
隐藏标准关联菜单更简单,只需要在接收到WM_CONTEXTMENU消息时返回0:
switch(uMsg)
{
case WM_SETCURSOR:
SetCursor(LoadCursor(g_hInstance, MAKEINTRESOURCE(IDC_HAND)));
return 0;
case WM_CONTEXTMENU:
// 在这里建立自己的弹出菜单
return 0;
}
当然,谁也不能阻止你在标准菜单显示的地方显示你自己的弹出菜单,要做的仅仅是在上段代码中用你
自己的代码替换注释行。
客户化工具标签
另一个可以客户化的形状是工具标签—可以考虑改变默认的讯息,‘点击这里开始’。尽管在Win32程序
中你从来没有使用过工具标签,也应该知道这是一个很难打开的坚果。没有容易的方法来感觉当前哪个
工具标签是活动的,而且即使捕捉到了TTN_SHOW通知(当工具标签窗口在显示中时,发送的通知消息),
你也不能取消这个标签。
‘开始’按钮的工具标签处理代码在远离按钮处理代码的地方,在启动时,任务条建立工具标
签窗口,设置一些工具。因而,要获得这个窗口的Handle用于显示‘开始’按钮的工具标签,一个可能
的方法是使用EnumThreadWindows()函数遍历由当前线程建立的所有窗口。条件是只有一个工具标签窗口
:仅一个。下面的代码给出怎样获得工具标签窗口和相关‘开始’按钮的工具。(这里的工具是一个标签
出现的区域—在‘开始’按钮的情况下是客户区域)。
void RemoveTooltip(HWND hwndStart)
{
EnumThreadWindows(GetCurrentThreadId(), EnumThreadWndProc,
reinterpret_cast<LPARAM>(hwndStart));
}
// 这个线程仅建立一个工具标签窗口,所有属于这个线程的窗口都被枚举,以找到这个工具标签
// 回调函数接收所有由这个线程建立的窗口Handle。lParam是(hwndStart)开始按钮的Handle。
BOOL CALLBACK EnumThreadWndProc(HWND hwnd, LPARAM lParam)
{
TCHAR szClass[MAX_PATH] = {0};
GetClassName(hwnd, szClass, MAX_PATH);
if(0 == lstrcmpi(szClass, TOOLTIPS_CLASS))
{
// 找到工具标签窗口,试着查找工具
int iNumOfTools = SendMessage(hwnd, TTM_GETTOOLCOUNT, 0, 0);
for(int i = 0 ; i < iNumOfTools ; i++)
{
// 取得第 I 个工具的信息
TOOLINFO ti;
ti.cbSize = sizeof(TOOLINFO);
SendMessage(hwnd, TTM_ENUMTOOLS, i, reinterpret_cast<LPARAM>(&ti));
if(ti.uId == static_cast<UINT>(lParam))
{
// 找到‘开始’按钮的相关工具。
ti.lpszText = __TEXT("Buy this book!");
SendMessage(hwnd, TTM_UPDATETIPTEXT, 0,
reinterpret_cast<LPARAM>(&ti));
}
}
return FALSE;
}
return TRUE;
}
获得了工具标签窗口的Handle之后,我们利用工具标签的接口来枚举各个工具。工具是一个规则区域,
当鼠标在其上盘旋时,引发一个提示,它由TOOLINFO结构描述。在枚举工具期间,‘开始’按钮的工具
通过对比TOOLINFO的uId字段与‘开始’按钮的Handle来识别。然后可以删除它,最好保留,文字可以通
过TTM_UPDATETIPTEXT消息替换掉。
这段代码有两个方面的限制,一是,当前线程仅建立一个工具标签窗口,其次,相关于‘开始
’按钮的工具有TTF_IDISHWND标志,即,工具相关于窗口的客户区域,而不是一个一般的矩形。再有,
TOOLINFO结构的uId成员包含了相关窗口的HWND。这实际一点也不奇怪,因为在对一个窗口定义工具标签
时,赋值TTF_IDISHWND标志是普遍习惯。了解了这些事情就极大地简化了我们的工作。因为你可以很容
易地鉴别(甚至删除)‘开始’按钮的工具。TOOLTIPS_CLASS是由通用工具库—显示工具标签的一些控件
—提供的一个窗口类名。
如果想要改变工具标签的文字,记住,这个变化并不邦定在运行的模块上。即使安装它的模块
已经卸载,它仍然继续出现。唯一恢复老标签的方法是把它改回到前一个设置。
新菜单
当用户点击这个按钮时,显示默认的‘开始’菜单。更精确地讲,当按钮接收到BM_SETSTATE
消息并且wParam设置到TRUE时,显示菜单。BM_SETSTATE是按钮专有的消息,用于请求按钮绘制‘按下’
或‘释放’模式。wParam值为TRUE说明按钮要求按下,而FALSE则是释放。如果你的目标就是简单地隐藏
标准菜单,只需要处理BM_SETSTATE消息和返回0。
当敲击Windows键或按Ctrl+Esc时,能够引起BM_SETSTATE消息发送到这个按钮。通过处理这个消息
的操作,你也能捕捉这些键的组合。
正确的行为
假设你有一个要显示的菜单。你可以通过处理WM_LBUTTONDOWN消息试着显示它:
TrackPopupMenu(hmnuPopup, uFlags, ix, iy, 0, hwnd, NULL);
如果指定了正确的坐标,菜单将显示在靠近按钮的地方。然而,按钮将不能绘制成‘按下’模式。
如此,你需要发送BM_SETSTATE消息来‘按下’和‘释放’这个按钮。反之,给按钮本身发送
这个消息,它将终止并由初始窗口过程进行处理,这是我们已经置换的。结果,显示标准的‘开始’菜
单。
这个问题是因为‘开始’按钮是任务条的子窗口。每次点击(或发送BM_SETSTATE消息),
Windows都自动地通知父窗口这个事件。对于按钮就是BN_CLICKED消息。通过处理BN_CLICKED消息,任务
条(不是按钮)显示标准菜单。
我们想要按钮提供菜单,但是需要绘制‘按下’的方法。我们怎样获得这个行为呢?我们需要
一个独立绘制按钮外观的函数,这要借助于初始的按钮过程—是以正常方式绘制按钮的过程,不作任何
其它的操作或引起任何其它事情发生的过程。这个过程的地址可以在GetClassInfo()恢复的WNDCLASS结
构中找到:
switch(uMsg)
{
case WM_SETCURSOR:
SetCursor(LoadCursor(g_hInstance, MAKEINTRESOURCE(IDC_HAND)));
return 0;
case WM_CONTEXTMENU:
return 0;
case WM_LBUTTONDOWN:
{
WNDCLASS wc;
GetClassInfo(NULL, "Button", &wc);
CallWindowProc(wc.lpfnWndProc, hwnd, BM_SETSTATE, TRUE, 0);
// 在这里调用 TrackPopupMenu()
CallWindowProc(wc.lpfnWndProc, hwnd, BM_SETSTATE, FALSE, 0);
return 0;
}
}
上面这段代码保证了我们的‘开始’按钮有正确的行为并在菜单出现时显示‘按下’。这一行代码:
CallWindowProc(wc.lpfnWndProc, hwnd, BM_SETSTATE, TRUE, 0);
现在就象一个外部函数一样,以‘开始’按钮的Handle作为参数。
可能会奇怪,还有另一种方法做这些:子类化任务条窗口和解释BN_CLICKED消息。然而,我更
喜欢这个方法,因为它减少了子类化窗口的数量。
怎样捕获Ctrl-Esc和Windows键
在按下Ctrl-Esc和Windows键发送BM_SETSTATE消息(使wParam设置为TRUE)到‘开始’按钮时,引起
它显示‘开始’菜单。子类化‘开始’按钮,我们就可以决定忽略那个事件:
case BM_SETSTATE:
return 0;
或选择显示我们自己的替代菜单:
case BM_SETSTATE:
case WM_LBUTTONDOWN:
{
...
}
建立自绘制菜单
TrackPopupMenu()是在一定的屏幕位置显示菜单的好方法,但是‘开始’有两个附加的属性,
这使它有别于普通的菜单。第一,它是自绘制菜单,第二,它必须显示在严格定义的位置—靠近任务条
和‘开始’按钮的位置。如果任务条停泊在屏幕的底部,菜单就必须显示在‘开始’按钮之上;如果它
在顶部,菜单应该在它下方。因而,为了决定菜单的正确坐标,我们首先需要知道系统任务条的位置。
确定菜单的屏幕位置
TrackPopupMenu()需要表示为(x,y)屏幕坐标的位置。有趣的是,你可以告诉函数怎样解释每
一个坐标,和怎样排列这个菜单。例如,如果指定TPM_BOTTOMALIGN标志,y坐标就是菜单的底,如果指
定TPM_RIGHTALIGN,则x坐标是菜单的右边。
弹出菜单的位置依赖于这三个信息片:x-和y-坐标,以及一堆标志。我们把它封装在一个称之
为STARTMENUPOS结构中,并定义一个辅助函数检查任务条的位置和统一填充这个结构:
struct STARTMENUPOS
{
int ix;
int iy;
UINT uFlags;
};
typedef STARTMENUPOS* LPSTARTMENUPOS;
void GetStartMenuPosition(LPSTARTMENUPOS lpsmp)
{
// 取得任务条的边缘和位置
APPBARDATA abd;
abd.cbSize = sizeof(APPBARDATA);
SHAppBarMessage(ABM_GETTASKBARPOS, &abd);
switch(abd.uEdge)
{
case ABE_BOTTOM:
lpsmp->ix = 0;
lpsmp->iy = abd.rc.top;
lpsmp->uFlags = TPM_LEFTALIGN | TPM_BOTTOMALIGN;
break;
case ABE_TOP:
lpsmp->ix = 0;
lpsmp->iy = abd.rc.bottom;
lpsmp->uFlags = TPM_LEFTALIGN | TPM_TOPALIGN;
break;
case ABE_LEFT:
lpsmp->ix = abd.rc.right;
lpsmp->iy = 0;
lpsmp->uFlags = TPM_LEFTALIGN | TPM_TOPALIGN;
break;
case ABE_RIGHT:
lpsmp->ix = abd.rc.left;
lpsmp->iy = 0;
lpsmp->uFlags = TPM_RIGHTALIGN | TPM_TOPALIGN;
break;
}
}
SHAppBarMessage()是一个API函数,定义在shellapi.h中,返回系统任务条的边和位置。它也可以提供
其它的服务(在第九章中)。GetStartMenuPosition()函数允许我们在相对任务条的正确位置显示‘开始
’菜单。这段显示弹出菜单的程序代码如下:
case WM_LBUTTONDOWN:
{
WNDCLASS wc;
GetClassInfo(NULL, __TEXT("Button"), &wc);
CallWindowProc(wc.lpfnWndProc, hwnd, BM_SETSTATE, TRUE, 0);
STARTMENUPOS smp;
GetStartMenuPosition(&smp);
HMENU hmenu = LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MENU));
HMENU hmnuPopup = GetSubMenu(hmenu, 0);
TrackPopupMenu(hmnuPopup, smp.uFlags, smp.ix, smp.iy, 0, hwnd, NULL);
CallWindowProc(wc.lpfnWndProc, hwnd, BM_SETSTATE, FALSE, 0);
return 0;
}
我们选中的每一个菜单项都发送WM_COMMAND消息到hwnd窗口,这与按钮本身没有区别。因而,我们的子
类化过程也处理用户的选择。过一会作进一步的解释。
装入新菜单
让我们建立一个非常简单的预定义菜单来置换标准菜单,把它命名为IDR_MENU。你可以自己通
过TrackPopupMenu()函数装入它和显示它,但是,你很快就会认识到这是相当失败的。事实上我们得到
的是一个传统的文字风格菜单:
相反,Windows的‘开始’菜单是一个自绘制菜单,其中的每一个项都由用户定义的过程分别绘制。然而
不幸的是,VC++ 的资源编辑器不允许你以‘可视’方式建立自绘制菜单,所以必须编程做每一件事情。
如果你想要绘制的菜单已经存在(如果它被存储在模块的资源中),则第一步应该是遍历所有的
项,为每一个菜单项指派特殊的MF_OWNERDRAW属性。这个标志限定了其内容必须由用户定义的过程绘制
。下面是代码段,取得弹出菜单和为每一项设置自绘制风格:
// 允许项目名的最大尺寸
const int ITEMSIZE = 100;
struct MENUSTRUCT
{
TCHAR szText[ITEMSIZE];
int iItemID;
TCHAR szFile[MAX_PATH];
};
typedef MENUSTRUCT* LPMENUSTRUCT;
void MakePopupOwnerDraw(HWND hwnd, HMENU hmnuPopup)
{
// 循环遍历弹出项
for(int i = 0 ; i < GetMenuItemCount(hmnuPopup) ; i++)
{
// 为自绘制函数保留一些数据
LPMENUSTRUCT lpms = GlobalAllocPtr(GHND, sizeof(MENUSTRUCT));
int iItemID = static_cast<int>(GetMenuItemID(hmnuPopup, i));
GetMenuString(hmnuPopup, iItemID, lpms->szText, ITEMSIZE, MF_BYCOMMAND);
lpms->iItemID = iItemID;
UINT uiState = GetMenuState(hmnuPopup, iItemID, MF_BYCOMMAND);
ModifyMenu(hmnuPopup, iItemID, uiState | MF_BYCOMMAND | MF_OWNERDRAW,
iItemID, reinterpret_cast<LPCTSTR>(lpms));
}
}
在为菜单项赋值自绘制风格时,可能想要保存一些项的信息,如显示串,此时可以通过客户结构
MENUSTRUCT存储它们,一个指向这个结构的指针作为ModifyMenu()函数的最后一个参数传递给函数,存
储缓冲也就传递到了那个实际绘制菜单的函数。这个内存必须由一个类似的例程释放,它应该在完成菜
单操作时被调用。
自绘制分隔线
如果我们真正打算产生一个类似于Windows标准‘开始’菜单的菜单,还需要生成自绘制分隔
线。由于‘开始’菜单沿着一个边缘产生连续垂直的没有分隔线的带,因而有效地缩减了项和分隔线占
用的水平区域。默认情况下,分隔线在运行时作为插入的线绘制,即,我们需要把分隔线作为项来绘制
。
动态采集菜单项
为了这个例子,我们决定不从工程(project)的资源中装入新菜单。‘开始’菜单是一个半动
态菜单,因此,菜单项可以部分地运行时确定。如果你建立了一个‘开始菜单’特殊文件夹(见第六章)
的快捷方式,你就可以引进新项,使之显示在菜单上。我们的处理器也定义类似的机理。
建立一个目录(硬编码为C:/MyStartMenu),用加到菜单的快捷方式填充。除了这些动态项,我
们的‘开始’菜单总是包含一个‘固定的’命令以恢复前面的设置和初始菜单。点击快捷方式将调用目
标文件,点击固定项引起处理器卸载。
下面的函数GetMenuHandle()建立由新‘开始’按钮显示的菜单。它扫视C:/MyStartMenu目录
搜索LNK文件,解析之,并添加相关图标和名字到菜单中。
HMENU GetMenuHandle(LPTSTR szPath)
{
LPMENUSTRUCT lpms;
int iItemID = 1;
// 这些全程变量提示菜单绘制现在开始
g_bAlreadyDrawn = FALSE; // 没开始绘制
g_bFirstTime = TRUE; // 第一次进入
// 建立空菜单
HMENU hmenu = CreatePopupMenu();
// 滤波串 *.lnk
TCHAR szDir[MAX_PATH] = {0};
lstrcpy(szDir, szPath);
if(szDir[lstrlen(szDir) - 1] != '//')
lstrcat(szDir, __TEXT("//"));
TCHAR szBuf[MAX_PATH] = {0};
wsprintf(szBuf, __TEXT("%s*.lnk"), szDir);
// 搜索.lnk
WIN32_FIND_DATA wfd;
HANDLE h = FindFirstFile(szBuf, &wfd);
while(h != INVALID_HANDLE_VALUE)
{
// 解析快捷方式
SHORTCUTSTRUCT ss;
ZeroMemory(&ss, sizeof(SHORTCUTSTRUCT));
wsprintf(szBuf, __TEXT("%s//%s"), szDir, wfd.cFileName);
SHResolveShortcut(szBuf, &ss);
// 用ID,描述和目标文件构造每一个项
lpms = reinterpret_cast<LPMENUSTRUCT>(GlobalAllocPtr(GHND,
sizeof(MENUSTRUCT)));
lpms->iItemID = iItemID;
if(!lstrlen(ss.pszDesc))
lstrcpy(lpms->szText, wfd.cFileName);
else
lstrcpy(lpms->szText, ss.pszDesc);
lstrcpy(lpms->szFile, ss.pszTarget);
// 添加菜单项
AppendMenu(hmenu, MF_OWNERDRAW, iItemID++, reinterpret_cast<LPTSTR>(lpms));
// 下一个循环
if(!FindNextFile(h, &wfd))
{
FindClose(h);
break;
}
}
// 添加分隔线和‘恢复’项
AppendMenu(hmenu, MF_OWNERDRAW | MF_SEPARATOR, 0, NULL);
lpms = reinterpret_cast<LPMENUSTRUCT>(
GlobalAllocPtr(GHND, sizeof(MENUSTRUCT)));
lpms->iItemID = ID_FILE_EXIT;
lstrcpy(lpms->szText, __TEXT("Restore Previous Settings"));
lstrcpy(lpms->szFile, "");
AppendMenu(hmenu, MF_OWNERDRAW, ID_FILE_EXIT,reinterpret_cast<LPTSTR>(lpms));
return hmenu;
}
这个函数引进两个全程布尔变量。g_bAlreadyDrawn用于记住是否图像已经绘制到了垂直带上,因为我们
仅仅需要绘制一次。g_bFirstTime则用于记住项是否头一次绘制在菜单中。如果这个变量是TRUE,菜单
项矩形的顶部边缘被保存以确定菜单的高度。在下面的函数中将看到这些值的变化。
菜单项从顶到底顺序绘制,而且最后的项在这个实现中由ID确定—它是固定项,用于卸载这个
处理器。这个项存在于DLL的资源中,有一个32x32像素的图标,和标识符ID_FILE_EXIT。其它菜单项都
在于调用函数SHResolveShortcut()获得,这个我们在前一章中已经说明了。
设置尺寸
自绘制资源引发两个消息发送给他们的父窗口过程,在这种情况下,这些消息将到达我们的新
‘开始’按钮过程,它们是:
WM_MEASUREITEM
WM_DRAWITEM
第一个消息用于获得单个菜单项的宽度和高度(像素单位),我们必须填写与消息同来的结构。第二个要
求做所需的绘制工作。下面是处理WM_MEASUREITEM消息的函数:
// 这些是绝对常量(像素单位表示的)定义绘制项的尺寸
const int DEFBITMAPSIZE = 32; // 32 x 32 是保留给图像的区域
const int DEFBANDSIZE = 25; // 垂直带的宽度
const int DEFSEPSIZE = 6; // 保留给分隔线区域的高度
const int DEFBORDERSIZE = 2; // 项文字与菜单边缘的空隙
void MeasureItem(HWND hwnd, LPMEASUREITEMSTRUCT lpmis)
{
SIZE size;
int iItemID = lpmis->itemID;
LPMENUSTRUCT lpms = reinterpret_cast<LPMENUSTRUCT>(lpmis->itemData);
// 计算菜单项串尺寸
HDC hdc = GetDC(hwnd);
GetTextExtentPoint32(hdc, lpms->szText, lstrlen(lpms->szText), &size);
ReleaseDC(hwnd, hdc);
// 设置项的宽度和高度
lpmis->itemWidth = DEFBITMAPSIZE + DEFBANDSIZE + size.cx;
// 分隔线的ID = 0
if(iItemID)
lpmis->itemHeight = DEFBITMAPSIZE;
else
lpmis->itemHeight = DEFSEPSIZE;
}
WM_MEASUREITEM消息的lParam变量指向一个MEASUREITEMSTRUCT结构,其itemHeight和 itemWidth字段必
须用项的实际尺寸填写。在上面代码中,高度设置为32像素,宽度依赖于文字的长度,为图像(图标)保
留的空间,和菜单边缘的带宽(例如Windows98的标志)。
注意,这里显式地使用了常量,所以‘开始’菜单的外观保持相同,无论其项设置是什么。
关于这里采用的结构和自绘制机理的更多信息请参看MSDN库的官方资料。
绘制菜单项
每当Windows需要描绘给定的菜单项时都发送WM_DRAWITEM消息。这个消息的lParam变量指向
DRAWITEMSTRUCT结构,它提供了绘制操作所需要的所有信息。基本上我们需要菜单窗口左边有一个垂直
带,然后每一个项有一个图标和一个字符串,左边的区域将由一个图像充填。
绘制图标和字符串可以直接用通用API函数完成,如,DrawIcon()和ExtTextOut()。在绘制菜单项时
,我们在一项上操作,仅能看到菜单窗口的一部分。在沿菜单窗口的边缘绘制图像时,过程有点不同。
在选择改变时,逐项调用绘制过程,而我们需要找出一种仅绘制一次图像的方法,使用全程变量记住图
像已经被绘制就是我们的解决方案。然而对于绘制图像,有更多的工作要做。
怎样绘制图像,使用BitBlt()或许是一个好方法。Windows使用从上到下的逻辑描绘它的自绘制菜单
。所以,如果传递(0,0)作为目的关联设备原点,图像将在菜单顶部排列。如果察看Windows95,98和NT
的‘开始’菜单,你将发现,图像总是排列在菜单底部,这就使问题复杂化了—传递给BitBlt()的正确
坐标是什么?x-坐标是0,或一个相对左边缘的绝对偏移值,y-坐标应该由菜单窗口的高度减去我们使用
的图像高度给出。因为BitBlt()从顶到底绘制,所以,图像将排列在底部。对这个问题,有一个找出菜
单窗口高度的简单方法,我们知道,DRAWITEMSTRUCT结构中包含了当前项的矩形,所以如果记住了第一
个元素的顶部和最后一个元素的底部,窗口的高度必然是二者之差。
如此,我们知道了图像的高度,以及窗口的高度。这就使得为BitBlt()函数确定正确的y坐标变得容
易了。显示现在应该与标准的‘开始’菜单一样了。下面给出必要的代码:
void DrawItem(LPDRAWITEMSTRUCT lpdis)
{
TCHAR szItem[ITEMSIZE] = {0};
TCHAR szFile[MAX_PATH] = {0};
COLORREF crText, crBack;
HICON hIcon = NULL;
LPMENUSTRUCT lpms = reinterpret_cast<LPMENUSTRUCT>(lpdis->itemData);
int iItemID = lpdis->itemID;
int iTopEdge = 0;
// 保存项文字和目标文件
if(lpms)
{
lstrcpy(szItem, lpms->szText);
lstrcpy(szFile, lpms->szFile);
}
// 管理绘制操作
if(lpdis->itemAction & (ODA_DRAWENTIRE | ODA_SELECT))
{
COLORREF clr;
RECT rtBand, rtBmp, rtText, rtItem, rt;
SIZE size;
// 定义将要使用的矩形:
// lpdis->rcItem 是菜单项的矩形
// rtBand: 菜单项的垂直带区域部分
// rtBmp: 菜单项的图标区域部分
// rtText: 菜单项的文字区域部分
CopyRect(&rt, &(lpdis->rcItem));
CopyRect(&rtBand, &rt);
rtBand.right = rtBand.left + DEFBANDSIZE;
CopyRect(&rtBmp, &rt);
rtBmp.left = rtBand.right + DEFBORDERSIZE;
rtBmp.right = rtBmp.left + DEFBITMAPSIZE;
CopyRect(&rtText, &rt);
rtText.left = rtBmp.right + 2 * DEFBORDERSIZE;
CopyRect(&rtItem, &rt);
rtItem.left += DEFBANDSIZE + DEFBORDERSIZE;
// 如果是第一项,保存y坐标
if(g_bFirstTime)
{
iTopEdge = rtBand.top;
g_bFirstTime = FALSE;
}
// 绘制带矩形和垂直图像
if(!g_bAlreadyDrawn)
{
// 带区域为蓝色
clr = SetBkColor(lpdis->hDC, RGB(0, 0, 255));
ExtTextOut(lpdis->hDC, 0, 0,
ETO_CLIPPED | ETO_OPAQUE, &rtBand, NULL, 0, NULL);
SetBkColor(lpdis->hDC, clr);
// 如果最后一项,确定菜单高度,装入和绘制图像
if(iItemID == ID_FILE_EXIT)
{
int iMenuHeight = rtBand.bottom - iTopEdge;
HBITMAP hbm = LoadBitmap(g_hInstance, MAKEINTRESOURCE(IDB_LOGO));
DrawBitmap(lpdis->hDC, 0, iMenuHeight, hbm);
DeleteObject(hbm);
g_bAlreadyDrawn = TRUE;
}
}
// 到目前为止选择状态没有影响到任何事情。
// 绘制图标,文字以及相关的背景色
if(lpdis->itemState & ODS_SELECTED)
{
crText = SetTextColor(lpdis->hDC, GetSysColor(COLOR_HIGHLIGHTTEXT));
crBack = SetBkColor(lpdis->hDC, GetSysColor(COLOR_HIGHLIGHT));
}
// 应正确的背景色清除区域
ExtTextOut(lpdis->hDC, rtText.left, rtText.left,
ETO_CLIPPED | ETO_OPAQUE, &rtItem, NULL, 0, NULL);
// 获得要绘制的图标,如果是最后一项,从资源中装入。
// 否则从快捷方式的目标文件中确定系统图标。
if(iItemID == ID_FILE_EXIT)
hIcon = LoadIcon(g_hInstance, MAKEINTRESOURCE(iItemID));
else{
SHFILEINFO sfi;
ZeroMemory(&sfi, sizeof(SHFILEINFO));
SHGetFileInfo(szFile, 0, &sfi, sizeof(SHFILEINFO), SHGFI_ICON);
hIcon = sfi.hIcon;
}
// 绘制图标(自动透明)
if(hIcon)
{
DrawIcon(lpdis->hDC, rtBmp.left, rtBmp.top, hIcon);
DestroyIcon(hIcon);
}
// 绘制文字(一行垂直居中)
if(!iItemID)
{
// 是一个分隔线
rt.top++;
rt.bottom = rt.top + DEFBORDERSIZE;
rt.left = rt.left + DEFBANDSIZE + DEFBORDERSIZE;
DrawEdge(lpdis->hDC, &rt, EDGE_ETCHED, BF_RECT);
}else{
// 取得对应字体的文字尺寸
GetTextExtentPoint32(lpdis->hDC, szItem, lstrlen(szItem), &size);
// 垂直居中
int iy = ((lpdis->rcItem.bottom - lpdis->rcItem.top) - size.cy) / 2;
iy = lpdis->rcItem.top + (iy >= 0 ? iy : 0);
rtText.top = iy;
DrawText(lpdis->hDC, szItem, lstrlen(szItem),
&rtText, DT_LEFT | DT_EXPANDTABS);
}
}
}
上面这个相对直接的大函数处理了文字和图标的绘制,但是,它把绘制垂直标记图(这是一个25像素宽的
资源IDB_LOGO)的工作留给了下一个例程, DrawBitmap():
void DrawBitmap(HDC hdc, int x, int iHeight, HBITMAP hbm)
{
// 这个函数计算基于覆盖区域高度的y坐标图像将与底部的一起排列
BITMAP bm;
// 建立存储关联设备选择其中的图像
HDC hdcMem = CreateCompatibleDC(hdc);
HBITMAP hOldBm = static_cast<HBITMAP>(SelectObject(hdcMem, hbm));
// 获得图像信息
GetObject(hbm, sizeof(BITMAP), &bm);
// 确定y坐标
int y = iHeight - bm.bmHeight;
y = (y < 0 ? 0 : y);
// 转换图像从存储DC到菜单DC
BitBlt(hdc, x, y, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
// 释放存储DC
SelectObject(hdcMem, hOldBm);
DeleteDC(hdcMem);
}
最后修正按钮子类化窗口过程,使它能正确地构建我们的客户菜单,和处理WM_MEASUREITEM环和
WM_DRAWITEM消息:
switch(uMsg)
{
case WM_SETCURSOR:
SetCursor(LoadCursor(g_hInstance, MAKEINTRESOURCE(IDC_HANDY)));
return 0;
case WM_MEASUREITEM:
MeasureItem(HWND_DESKTOP, reinterpret_cast<LPMEASUREITEMSTRUCT>(lParam));
break;
case WM_DRAWITEM:
DrawItem(reinterpret_cast<LPDRAWITEMSTRUCT>(lParam));
break;
case WM_CONTEXTMENU:
return 0;
case BM_SETSTATE:
case WM_LBUTTONDOWN:
{
WNDCLASS wc;
GetClassInfo(NULL, "Button", &wc);
CallWindowProc(wc.lpfnWndProc, hwnd, BM_SETSTATE, TRUE, 0);
STARTMENUPOS smp;
GetStartMenuPosition(&smp);
HMENU hmnuPopup = GetMenuHandle("c://myStartMenu");
int iCmd = TrackPopupMenu(hmnuPopup,
smp.uFlags | TPM_RETURNCMD | TPM_NONOTIFY,
smp.ix, smp.iy, 0, hwnd, NULL);
// 处理用户鼠标点击
HandleResults(hmnuPopup, iCmd);
// 释放内存
DestroyMenu(hmnuPopup);
CallWindowProc(wc.lpfnWndProc, hwnd, BM_SETSTATE, FALSE, 0);
return 0;
}
}
执行命令
现在菜单是完整的和可操作的,但是其唯一的缺点是菜单上没有任何一项可以有实际的动作。
从上面的清单中你可以看到答案,HandleResults()函数应该做某些工作,问题是菜单上我们选择的是哪
一种项。他们正好是应用命令,还是文档和程序的快捷方式?
当然这最终依赖于你的需求。我们有选择地读出磁盘目录中的内容和动态地安排菜单。(当添
加了快捷方式到‘开始’或‘程序’菜单时Shell确实做这个工作。)。 如前所述,假设这个处理器查找
文件对象的快捷方式,然后解析它,并附加到菜单中,最后加一个分隔线和标准的‘退出’项。
快捷方式的描述变成了菜单项的文字,如果快捷方式没有描述(一个普通情况),则使用文件名
。当这个菜单项被选中时,处理器模块简单地调用快捷方式指向的文件。
void HandleResults(HMENU hmenu, int iCmd)
{
MENUITEMINFO mii;
LPMENUSTRUCT lpms;
if(iCmd <= 0)
return;
if(iCmd == ID_FILE_EXIT)
{
UninstallHandler();
return;
}
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_DATA;
GetMenuItemInfo(hmenu, iCmd, FALSE, &mii);
lpms = reinterpret_cast<LPMENUSTRUCT>(mii.dwItemData);
ShellExecute(NULL, __TEXT("open"), lpms->szFile, NULL, NULL, SW_SHOW);
}
如果点击的项是‘恢复前一个设置’则调用UninstallHandler()函数退出。对于任何其它的选择,都从
项数据中抽取执行文件路径,然后调用ShellExecute()API函数,以执行这个文件。我们的客户菜单到此
就全部完成了。
浏览器辅助对象
SHLoadInProc()是一个桥梁,它允许你的程序插入COM对象到Shell中。我们已经使用了一个最
小的COM对象演示了这种操作。你当然也可以使用正常的COM对象做这些。要指出的是,要开发这个功能
,你不必是一个专门的COM程序员。只是要求你所构建的程序本身能表述为COM对象:它必须有一个CLSID
,必须被注册,和必须实现任何COM服务器的最小功能。不要求你实现任何接口,也不禁止你做需要做的
操作。 相反,浏览器辅助对象则是一个完整的进程内COM服务器,IE(和探测器)在创建自己的新实例
时加载这个对象。注意,这些对象总是需要一个浏览器实例来打开才能活动。后面的活动原理一节将有
简短的说明。
使用SHLoadInProc(),是你的程序决定什么时候进入探测器的地址空间,以及是否应该阻止进
入探测器地址空间。使用浏览器扶助对象的最大差别在于是浏览器(探测器或IE)自动加载在注册表特定
区域注册的所有模块。
就象它们的名字提示的那样,浏览器辅助对象仅影响探测器的特定部分—浏览器部分,让你浏
览文件和文件夹的那部分。
现在你可以在两个互补的方法之间进行选择—由你来决定两个选择中最能适合特殊需要的方法
。为了帮助你选择,我们将探讨两种方法各自的优缺点。主要的不同点是:
向后兼容性
活动原理
注册
COM对象结构
与主程序的通讯
用途
记住这两种选择都是加载COM对象到Shell存储空间的有效方法,我们也将以这样的术语来评价它们。从
技术角度讲,这两者完全不同:SHLoadInProc()是一个函数,而浏览器辅助对象是一个COM对象。
向后兼容性
在4.00版本以后的Shell中支持SHLoadInProc(),而浏览器辅助对象则特指到Shell4.71版—它们
都与IE4.0一道出现。二者在除了WindowsCE以外的所有Win32平台上都能很好工作。
记住,Shell4.71版是指你必须有IE4.0或更高版和活动桌面。在Windows98中包括了这二者。
活动机理
从这个观点上看,两种方法是很不相同。SHLoadInProc()允许你的应用编程地加载COM对象到
Shell的关联空间。相反,浏览器辅助对象是注册对象,在IE和探测器每次启动新实例时加载到内存中的
你不能控制浏览器辅助对象的加载时间。
为了使辅助对象活动,你必须打开探测器或浏览器的一个实例。进一步,一个辅助对象的实例
与探测器或浏览器关联—一旦相关联的实例关闭,辅助对象就被卸载。
注册
SHLoadInProc()可以装入任何正确注册定COM对象。浏览器辅助对象必须注册在指定的注册表
路径上,此时探测器或浏览器才能看到它(参考注册辅助对象一节)。
COM对象结构
正如上面所看到的,SHLoadInProc()可以管理和成功地加载任何COM对象—甚至是假冒的,没
有实现任何接口的对象。浏览器辅助对象必须有一个明确定义的,由浏览器(IE或探测器)验证的格式。
也就是实现IObjectWithSite接口的规则。
与主程序通讯
经由SHLoadInProc()加载的对象不接受指向Shell的IUnknown接口的指针。这可能是一个有意
义的限制,因此如果你的目标仅是简单地子类化Shell对象,则不需要那样的指针。子类化,指的是允许
你使用强制手段修改和滤波对象(如,‘开始’按钮)行为的任何技术,是一种对象‘不知道’你所做活
动的方法。
反过来,当主环境的对象有一个引用时,允许通过公共编程接口关联它们,这是一种简洁而安
全得多的方法。这也开启了探索功能的新途径,其中的事件处理是最有用的。通过浏览器加载的辅助对
象能够接受指向IWebBrowser2的指针,和处理所有浏览器引起的事件。这种通讯为IObjectWithSite接口
所支持。
使用方法
SHLoadInProc()有可以加载任何对象,包括假冒对象的优点,原理上,你也可以用
SHLoadInProc()加载辅助对象。不幸的是它并不允许你使用IUnknown接口与Shell通讯。所以在这方面,
浏览器辅助对象更通用,尽管它不能编程地加载。SHLoadInProc()仅与探测器一起工作,而辅助对象可
以与IE和探测器二者一起工作。然而,SHLoadInProc()不需要探测器或IE的实例。
我们已经有了一个假冒的COM模块,现在试着把它注册为辅助对象,它能很好地工作。在这种
情况下,这个‘最小’COM对象与使用SHLoadInProc()加载时有相同的工作方式:它输出一个总是被调用
的函数DllGetClassObject()。
注册辅助对象
浏览器辅助对象是一个COM模块,它必须自注册到下面的路径上:
HKEY_LOCAL_MACHINE
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
/Browser Helper Objects
所有被允许模块的CLSIDs都列出在‘Browser Helper Objects’键下。探测器(和IE)逐一加载它们。记
住当你打开‘回收站’或‘打印机’文件夹时,也建立浏览器的新实例。也就是说辅助对象经常获得调
用—至少比期望的要频繁(留心查看对话框或模式窗口…)。注册表中这个辅助对象的列表是不被缓存的
,总是重新从磁盘读出,所以只需花费一点时间就能清除掉那些不再有用的模块—你只需删除注册表中
对应的CLSID行。幸运的是,从这个子树删除对象并不影响这个服务器的全程注册状态。别的应用仍然可
以在CLSID键下用和以前一样的方法找到它。
IObjectWithSite 接口
使用SHLoadInProc(),模块可以加载到探测器地址空间,但是它没有基于COM的连接。换言之
,他不能接受浏览器的IUnknown指针,它也不能访问这个目标模型。辅助对象通过实现IObjectWithSite
接口,修补了这个不足。
当浏览器加载一个注册表中列出的COM服务器时,它查询IObjectWithSite接口,如果找到,则经由
SetSite()方法传递指向浏览器IUnknown接口的指针到这个模块。IObjectWithSite仅包含附加IUnknown
的两个方法SetSite()和GetSite()。
HRESULT IObjectWithSite::SetSite(IUnknown* pUnkSite);
HRESULT IObjectWithSite::GetSite(REFIID riid, void** ppvSite);
SetSite()方法由浏览器调用,并作为入口点。GetSite()方法工作与QueryInterface()十分相象。返回
由SetSite()最后在这里设置的特定接口指针。
编写辅助对象
如果你计划写一个浏览器辅助对象,ATL可以提供重要的帮助。一旦使用ATL COM大师建立了一
个框架,你就可以使用对象大师添加新对象和从IObjectWithSiteImpl导出它。所有其余的工作就是使用
辅助逻辑编写SetSite()方法的实体。
为了说明这一点,我们将重写按下指定键建立新文件夹的工具为一个辅助对象。浏览器辅助对
象比普通的Shell对象扩展更适用于建立增强探测器的小实用程序,所以浏览器扶助对象似乎就是添加新
探测器加速器的理想方法。我们不再需要应用注入代码到探测器关联空间,相反是必须建立实现
IObjectWithSite接口的COM对象。下面两点是要考虑的:
找出探测器窗口的Handle
感觉这个加速器的键盘钩子
我们前面的方案是基于窗口建立的全程钩子。当钩子过程感觉到一定类型(ExploreWClass)的窗口建立时
,它在键盘的活动上安装一个局部钩子。当F12按下时,探测器窗口接收命令消息引起建立新文件夹。相
反,辅助对象在探测器窗口已经存在时被加载。然而,FindWindow()并不是一个查找探测器窗口必然正
确的函数,因为它返回指定类的顶层窗口Handle。因此,如果有多个探测器的副本在同时运行,我们就
不能保证返回的就是我们的窗口。
如果多个探测器副本同时运行,每一个都在其自己的线程中运行。对于浏览器辅助对象,找出
探测器窗口Handle的较好方法是枚举当前线程所拥有的窗口,代码如下:
EnumThreadWindows(GetCurrentThreadId(), WndEnumProc,
reinterpret_cast<LPARAM>(&m_hwndExplorer));
if(!IsWindow(m_hwndExplorer))
return E_FAIL;
EnumThreadWindows()函数是一个枚举由指定线程建立的所有窗口的API函数。每一个窗口都作为第二个
变量传递给回调函数进行处理。下面是回调函数WndEnumProc()的处理过程:
BOOL CALLBACK CNewFolder::WndEnumProc(HWND hwnd, LPARAM lParam)
{
TCHAR szClassName[MAX_PATH] = {0};
GetClassName(hwnd, szClassName, MAX_PATH);
if(!lstrcmpi(szClassName, __TEXT("ExploreWClass")))
{
HWND* phWnd = reinterpret_cast<HWND*>(lParam);
*phWnd = hwnd;
return FALSE;
}
return TRUE;
}
EnumThreadWindows()的第三个参数是一个32位值,这个值可以被调用者用于任何目的。我们需要一种使
得探测器窗口(如果有一个)Handle被返回的方法。因此使用第三个参数传递一个指向HWND变量的指针。
当WndEnumProc()找到了一个类型为ExploreWClass的窗口时,它就拷贝这个Handle到指针中,然后通过
返回FALSE停止枚举过程。
无论外观如何,探测器窗口实际上由整个窗口栈组成,下图将显示一个轮廓概念,详细请参看
Spy++中精确的窗口类和风格。
每次敲击键盘都根据焦点输入窗口进行不同的处理。由于安装了局部键盘钩子,我们可以在击键进入窗
口的传统通道之前对其进行处理。
ATL COM对象
让我们看一下浏览器辅助对象的源码,这里已经使用ATL COM大师生成了代码的框架。一个新
的‘简单对象’NewFolder被加入。newfolder.h头文件如下形式:
#ifndef __NEWFOLDER_H_
#define __NEWFOLDER_H_
#include "resource.h" // main symbols
///
// 常量
const int NEWFOLDERMSG = 29281; // WM_COMMAND to send
const int NEWFOLDERKEY = VK_F12; // Key to detect
/
// CNewFolder
class ATL_NO_VTABLE CNewFolder :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CNewFolder, &CLSID_NewFolder>,
public IObjectWithSiteImpl<CNewFolder>,
public IDispatchImpl<INewFolder, &IID_INewFolder, &LIBID_OBJFOLDERLib>
{
public:
CNewFolder()
{
m_bSubclassed = false;
}
~CNewFolder();
DECLARE_REGISTRY_RESOURCEID(IDR_NEWFOLDER)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CNewFolder)
COM_INTERFACE_ENTRY(INewFolder)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY_IMPL(IObjectWithSite)
END_COM_MAP()
// INewFolder
public:
STDMETHOD(SubclassExplorer)(bool bSubclass);
// IObjectWithSite
public:
STDMETHOD(SetSite)(IUnknown* pUnkSite);
private:
bool m_bSubclassed;
HWND m_hwndExplorer;
// 回调函数
static BOOL CALLBACK WndEnumProc(HWND, LPARAM);
static LRESULT CALLBACK KeyboardProc(int, WPARAM, LPARAM);
static LRESULT CALLBACK NewExplorerWndProc(HWND, UINT, WPARAM, LPARAM);
};
#endif //__NEWFOLDER_H_
我们已经从ATL提供的标准实现中为IObjectWithSite接口导出了NewFolder类。唯一要做的就是重载
SetSite()方法。这是辅助对象的关键函数。下面代码说明哪一个是探测器窗口并安装键盘钩子。尽管对
于这个示例并不严格地需要,我们还是子类化了探测器窗口。所以代码进一步增强了。
#include "stdafx.h"
#include "ObjFolder.h"
#include "NewFolder.h"
// 这些常量在类的静态成员中使用
static WNDPROC g_pfnExplorerWndProc = NULL;
static HHOOK g_hHook = NULL;
static HWND g_hwndExplorer;
///
// CNewFolder
CNewFolder::~CNewFolder()
{
if(m_bSubclassed)
{
SubclassExplorer(false);
m_bSubclassed = false;
}
}
/*----------------------------------------------------------------*/
// SetSite
// 由探测器/IE调用来获得接触
/*----------------------------------------------------------------*/
STDMETHODIMP CNewFolder::SetSite(IUnknown* pUnkSite)
{
HRESULT hr = SubclassExplorer(true);
if(SUCCEEDED(hr))
m_bSubclassed = true;
return S_OK;
}
/*----------------------------------------------------------------*/
// SubclassExplorer
// 子类化探测器窗口和安装键盘钩
子/*----------------------------------------------------------------*/
STDMETHODIMP CNewFolder::SubclassExplorer(bool bSubclass)
{
// 获得探测器窗口的HWND
EnumThreadWindows(GetCurrentThreadId(), WndEnumProc,
reinterpret_cast<LPARAM>(&m_hwndExplorer));
if(!IsWindow(m_hwndExplorer))
return E_FAIL;
else
g_hwndExplorer = m_hwndExplorer;
// 子类化探测器窗口
if(bSubclass && !m_bSubclassed)
{
g_pfnExplorerWndProc = reinterpret_cast<WNDPROC>(SetWindowLong(
m_hwndExplorer, GWL_WNDPROC,
reinterpret_cast<LONG>(NewExplorerWndProc)));
// 设置键盘钩子来感觉F12
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc,
NULL, GetCurrentThreadId());
}
// 非子类化探测器窗口
if(!bSubclass && m_bSubclassed)
{
SetWindowLong(m_hwndExplorer, GWL_WNDPROC,
reinterpret_cast<LONG>(g_pfnExplorerWndProc));
// 删除钩子
UnhookWindowsHookEx(g_hHook);
}
return S_OK;
}
/*----------------------------------------------------------------*/
// WndEnumProc
// 枚举线程窗口的静态成员
/*----------------------------------------------------------------*/
// 将上面讨论给出的代码插入
/*----------------------------------------------------------------*/
// NewExplorerWndProc
// 置换探测器窗口过程的静态成员
/*----------------------------------------------------------------*/
LRESULT CALLBACK CNewFolder::NewExplorerWndProc(HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
// 不做任何事情,只是调用标准过程
return CallWindowProc(g_pfnExplorerWndProc, hwnd, uMsg, wParam, lParam);
}
/*----------------------------------------------------------------*/
// KeyboardProc
// 处理键的静态成员
/*----------------------------------------------------------------*/
LRESULT CALLBACK CNewFolder::KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
// 任何钩子的典型开始段
if(nCode < 0)
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
// 处理键仅一次(按下和松开)
if((lParam & 0x80000000) || (lParam & 0x40000000))
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
if(wParam == NEWFOLDERKEY)
PostMessage(g_hwndExplorer, WM_COMMAND, NEWFOLDERMSG, 0);
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
在编写辅助对象时另一个要做的是使它完全自注册。为了正确注册一个浏览器辅助对象,需要把下述代
码加到RGS脚本中:
HKLM
{
SOFTWARE
{
Microsoft
{
Windows
{
CurrentVersion
{
Explorer
{
'Browser Helper Objects'
{
{B4F8DE53-65F4-11D2-BC00-B0FB05C10627}
}
}
}
}
}
}
}
浏览器辅助对象的一个问题是它们并不是完全没有说明资料,它们确有一定的资料说明。在MSDN库中将
能找到说明文章。
这就完成了这个工程(project)的代码,现在我们可以编译和连接这个工程,注册这个浏览器辅助对象。
安装对象之后,在任何可唤醒的探测器新实例中都将有一个键盘钩子,处理F12在显示的目录下依次建立
新文件夹。
Windows NT下的辅助对象
浏览器辅助对象在WindowsNT下与在Windows9x下的工作方式相同。注册过程也一致,遵循一样
的设计逻辑。只有一个要避免地缺陷:Uncode。在WindwosNT下,辅助对象确实需要Uncode模块。如果不
是,代码仍然可以工作,但是某些字符串在探测器的用户界面下将被截断。
幸运的是由于我们使用了ATL,为Uncode重编译只是从Build菜单的Active Configuration组框
中选择适当的设置就可以了。然后需要对于浏览器辅助对象,建立和发布两个不同的版本:Windows9x
的 ANSI版和WindowsNt的Unicode版。
进入Shell的技术术语表
现在我们已经探讨了三种访问Shell地址空间的方法。下表是技术摘要,使你能交叉地引用这些技术。
因素
强制方式
SHLoadInProc()
辅助对象
向后兼容性
Shell 4.00
Shell 4.00
Shell 4.71
活动机理
Programmatically
Programmatically
Shell自动加载
注册表冲突
None
一般COM对象注册
一般COM对象注册加辅助对象特殊注册
代码结构
基于全程钩子
具有特殊接口的COM对象
实现IObjectWithSite
接口的COM对象
与主程序的通讯
通过子类化
通过子类化
通过主程序的IUnknown接口
要求的知识
Win32 编程
Win32编程和最小COM能力
Win32编程和良好的COM知识
小结
在这一章中我们探讨了各种侵入Shell领地和修改其行为的方法,并查看了Shell的变化。从通
知对象开始,它使探测器知道文件系统的变化,而后涉及了Shell通知,这是一个达到相同效果的更普通
的方法(事实上它们是相当不同的,但是它们的目标是有共同点的)。
其次,我们探讨了进程内通讯的课题,研究了子类化和钩子,并且给出了实用程序范例—探测
器键盘加速器,这个程序允许通过敲击单个键建立新文件夹。我们还展示了使用单个Shell API函数将代
码引入Shell的关连空间的方法。而后,我们看到了对‘开始’按钮的替换和对Win32程序的界面的改变
。包括自绘制菜单控件,工具标签和按钮风格。最后,我们介绍了浏览器辅助对象—一种增强探测器和
浏览器行为的方法。概括地说我们解释了:
怎样获得文件系统的通知
怎样进入Shell地址空间
怎样子类化‘开始’按钮
怎样实现完全客户化的菜单
SHLoadInProc()和浏览器辅助对象之间的差异
========