第一章 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版本之间的差异
第二章Shell的结构
组
|
功能
|
一般Windows函数
Shell内部函数
任务条函数
文件函数
文件夹函数
|
涉及到屏幕保护,控制面板脚本程序,联机帮助,以及Shell拖拽(不是OLE拖拽)
访问探测器地址空间的函数,获得Shell存储分配器的函数,导出可执行程序的函数以及感觉用户接口改变的函数。
涉及到托盘域的函数和与Windows任务条通讯的函数
操作文件的函数,他们执行如‘拷贝’,‘移动’,‘删除’和‘取得信息’等操作的系统活动,和添加文件到特殊的系统文件夹如‘最近文档’等。
操作文件夹的函数,使用这些函数,你可以浏览文件夹,获得系统文件夹的路径,发现文件夹的设置。
|
组
|
功能
|
图标函数
环境函数
Shell 轻量级API函数
|
从执行文件中抽取图标的函数
处理环境变量的函数
容易地访问注册表的函数,读写注册表函数,处理路径名函数,和处理字符串函数。
|
函数
|
描述
|
DragAcceptFiles()
|
标记允许窗口认可拖拽操作。
|
DragFinish()
|
从Shell中释放移动文件名列表所分配的内存
|
DragQueryFile()
|
从Shell处理拖拽而分配的内存块中抽取文件名
|
DragQueryPoint()
|
获得拖拽发生的点位置
|
CPlApplet()
|
控制面板脚本小程序的主程序
|
GetMenuContextHelpId()
|
返回关联于给定菜单的帮助ID
|
GetWindowContextHelpId()
|
返回关联于给定窗口的帮助ID
|
SetMenuContextHelpId()
|
设置关联于给定菜单的帮助ID
|
SetWindowContextHelpId()
|
设置关联于给定窗口的帮助ID
|
WinHelp()
|
打开帮助文件
|
ShellAbout()
|
显示默认和特定客户化的‘关于’信息框
|
|
|
函数
|
描述
|
ShellExecute()
|
在指定的文件上执行特殊操作
|
ShellExecuteEx()
|
与上面函数相同,但是有更多的选择
|
SHChangeNotify()
|
通过这个函数程序能够让Shell知道什么变化了,以及要求它刷新它所保有的信息
|
SHGetInstanceExplorer()
|
返回探测器IUnknown接口指针
|
SHGetMalloc()
|
返回一个指向Shell存储分配器的指针
|
SHLoadInProc()
|
装载指定的COM对象到探测器地址空间
|
函数
|
描述
|
Shell_NotifyIcon()
|
显示和管理靠近时钟的托盘区域的图标
|
SHAppBarMessage()
|
发送消息到系统的任务条
|
|
|
函数
|
描述
|
版本
|
FindExecutable()
|
返回指定文件名注册的可执行文件路径
|
所有版本
|
SHAddToRecentDocs()
|
把给定文件的连接加到系统的‘最近文档’文件夹中。
|
所有版本
|
SHFileOperation()
|
用于拷贝、移动、删除或重命名一个或多个文件。
|
所有版本
|
SHFreeNameMappings()
|
释放SHFileOperation()函数在特定情况下返回的存储结构
|
|
SHGetFileInfo()
|
返回给定文件的各种信息块
|
所有版本
|
SHGetNewLinkInfo()
|
建立新的快捷方式名
|
4.71
|
函数
|
描述
|
版本
|
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
|
|
|
|
函数
|
描述
|
ExtractIcon()
|
返回可执行文件的图标Handle
|
ExtractIconEx()
|
与上函数相同,但是有更多的选择。
|
ExtractAssociatedIcon()
|
基于文件类,返回指定文件的图标Handle
|
组
|
接口
|
Shell 扩展
|
涉及到所有Shell活动的COM接口,从图标到关联菜单,从UI活动到文件观察
|
Namespace 扩展
|
涉及到命名空间扩展的COM接口
|
钩子
|
能够钩住某些东西的接口,特别是程序执行,URL转换和建立Internet快捷方式
|
杂项接口
|
一些零碎接口,如客户化任务条的接口,与打开对话框通讯的接口和对‘我的公文包’编程的接口
|
接口
|
描述
|
版本
|
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
|
用于为指定文件类增加属性页到‘属性’对话框。
|
所有版本
|
|
|
|
接口
|
描述
|
版本
|
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
|
|
|
|
接口
|
描述
|
版本
|
ICopyHook
|
能钩住Shell中的所有文件操作(拷贝、移动、删除、重命名)。
|
所有版本
|
IURLSearchHook
|
使你能够探知探测器正在试图转换一个不可知的URL协议。
|
4.71
|
INewShortcutHook
|
使你能够探知探测器正在试图建立新的Internet快捷方式。
|
4.71
|
IShellExecuteHook
|
能够钩住通过ShellExecute()或ShellExecuteEx()导出的所有新进程的启动。
|
所有版本
|
|
|
|
接口
|
描述
|
版本
|
INotifyReplica,
IReconcilableObject,
IReconcileInitiator
|
所有这些接口都涉及到文件调整过程。最终都产生同一个文档的更新版本。
|
所有版本
|
ICommDlgBrowser
|
当客户文件夹嵌入到通用对话框中时,提供特殊的浏览行为。
|
所有版本
|
ITaskbarList
|
允许在系统任务条中加入新的按钮。
|
4.71
|
|
|
|
第三章操作文件
我依然清楚地记得,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()
最普遍的编程错。
这个函数在资料方面的短缺
怎样利用文件名映射的优点