我依然清楚地记得,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(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast(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(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
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(&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 ? "" : 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(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast(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(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(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(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(pshNM->pszOldPath),
MAX_PATH, szOldPath, MAX_PATH, NULL, NULL);
WideCharToMultiByte(CP_ACP, 0, reinterpret_cast(pshNM->pszNewPath),
MAX_PATH, szNewPath, MAX_PATH, NULL, NULL);
}else{
lstrcpy(szOldPath, pshNM->pszOldPath);
lstrcpy(szNewPath, pshNM->pszNewPath);
}
// 保存列表观察Handle
HWND hwndListView = reinterpret_cast(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()
最普遍的编程错。
这个函数在资料方面的短缺
怎样利用文件名映射的优点