关于Windows2000的Bug和文件类型
来自:HD_Copy, 时间:2002-5-19 18:59:00, ID:1110450 [显示:小字体 | 大字体]
在Windows资源管理器右边的文件列表中,所显示的每个文件都有"类型"这一项,如果是系统能够识别的
文件,如:CONFIG.SYS,则类型中显示"系统文件";如果是系统不可识别的文件,如:CONFIG.XYZ,则
类型中显示"XYZ文件"。
我想通过程序将一个目录下的所有文件的"文件类型"在ListBox控件中列出来,我是这样做的,新建一
工程,在Form上分别放置ListBox1、Button1、Button2这三个控件,下面分别是Delphi和C++Builder的
实现代码:(代码很少,也很简单)
Delphi代码:
//====================================================================================
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls ,ShellAPI , FileCtrl;
type
TForm1 = class(TForm)
ListBox1: TListBox;
Button1: TButton;
Button2: TButton;
procedure Button2Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
procedure SearchFiles(CurrentPath : AnsiString);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.SearchFiles(CurrentPath : AnsiString);
var
sr : TSearchRec;
FileInfo : TSHFileInfo;
iFound : Integer;
begin
iFound := FindFirst(CurrentPath + '*.*', faAnyFile, sr);
while iFound=0 do
begin
if (sr.Attr<>faDirectory)and(sr.Name<>'.')and(sr.Name<>'..') then
begin
SHGetFileInfo(PChar(CurrentPath+sr.Name), 0, FileInfo, SizeOf(FileInfo), SHGFI_TYPENAME);
ListBox1.Items.Add(FileInfo.szTypeName);
end;
iFound := FindNext(sr);
end;
FindClose(sr);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
ListBox1.Items.Clear;
SearchFiles('C:\Winnt\system32\');
end;
procedure TForm1.Button1Click(Sender: TObject);
var
SelectPath : AnsiString;
begin
SelectPath := '';
if SelectDirectory('请您选择分类库所对应的路径:', '', SelectPath) then
begin
if Length(SelectPath) <> 3 then
SelectPath := SelectPath + '\';
ListBox1.Items.Clear;
SearchFiles(SelectPath);
end;
end;
end.
//=====================================================================================
好,下面怪问题就来了,运行程序,如果你先点击Button1,也就是通过选择路径的方式,没有任何问题,
接下来,不论你在单击Button1还是Button2,都没问题(我说的没问题是指能在ListBox中正确显示包
括那些不可识别的文件在内的所有文件的文件类型)
但是反过来,在程序刚运行后,你先单击了Button2,也就是通过直接传递路径参数的方式,那些不可
识别的文件的文件类型就显示不出来了,然后,你无论是再按那个按钮,都不能正确的显示了
再说得明白一点,就是在程序运行后,你先单击Button1,就没问题;先单击Button2,不可识别的文件
类型就显示成空,这是多么奇怪的事啊!!哪位大侠帮我解释一下原因,并给出解决方法(必有重谢!!)
来自:aimingoo, 时间:2002-5-24 14:16:00, ID:1121501
HDCopy, 我用了很长的时间来查找(而不是分析)代码,哈哈,因为我认为应该不是你的程序
的问题。现在算是有些眉目了。
首先,我将Button1.onClick中调用的SelectDirectory()从FileCtrl.pas中提取出来,然后
将其中无用(不造成影响)的代码逐行清除。最后,我得到了这样的一个过程:
procedure SelectDirectory;
var
BrowseInfo: TBrowseInfo;
begin
FillChar(BrowseInfo, SizeOf(BrowseInfo), 0);
ShBrowseForFolder(BrowseInfo);
end;
接下来,我只要在Button2.onClick中调用一下这个过程,而不管操作中选择"确定"或者
"取消",Button2中的SearchFiles('C:\WinNT\System32')调用的结果都是正常的。
而ShBrowseForFolder的实现在ShlObj.pas中就只有一行:
function SHBrowseForFolder; external shell32 name 'SHBrowseForFolderA';
于是,我开始认定这不是一般的程序/流程错误,可能是OS有关。
所以,接下来的工作我就是开始查Google了。
我找到了这样的一份文档:
http://www.dcjournal.com/gooey/sysimage.htm
从它的下载包中,我找到了一个可执行程序,用来实现HDCopy要的功能,这个下载包在:
http://www.dcjournal.com/ftp/gooey/sysimage.zip
然而,我在Windows 2K中运行这个DEMO,我发现与HDCopy出现了完全同样的错误,接下
来,我又在vmWare的虚拟Windows 98中测试,发现一切又正常了。参见下面这两个图:
http://aiming.ynxx.com/myUpload/Test_Win98.jpg
http://aiming.ynxx.com/myUpload/Test_Win2K.jpg
哈哈,我想我不需要再解释,至少原因已经找到了,这个Windows OS版本间的Shell32.DLL
不兼容导致的。
就目前而言,只能分析到这儿,下面我去查查MSDN的新版本,在Windows 2000的Shell32.DLL
说明中应该有相关的内容。
HDCopy可以自己查查,我手边没有新版的MSDN,我只能查MSDN Online。:(
来自:chji, 时间:2002-5-24 20:13:00, ID:1122292
没问题!
你的系统有问题吧!入安全模式再运行程序试试
来自:skyweb, 时间:2002-5-25 11:11:00, ID:1123083
微软的知识库(Q236376)中提到,在NT4和Windows 2000上,ShGetFileInfoA调用
ShGetFileInfoW,但是在把ShFileInfoA结构转换到ShFileInfoW结构时没能正确
地把dwAttributes转换过去,dwAttributes是一个未初始化的值。稍微联想一下,
可能在某个特定情况下,文件类型说明也没能正确转换过去(谁知道呢?:)。
微软提出的解决办法是直接调用ShGetFileInfoW。试一下吧,还不行的话只好简
单地绕过这个问题了:)。
微软的知识库文章(Q236376)地址:
http://support.microsoft.com/default.aspx?scid=kb;en-us;Q236376
来自:OopsWare, 时间:2002-6-22 9:50:00, ID:1176786
to all: 终于解决了!有兴趣的话看看下面的代码,
to Hd_copy:
你可以不再使用FindFirst取文件了,试试下面的完全的Shell编程方式。
我也说不太清原因,直接获得文件的PItemIDList并不能得到特殊文件的类型说明
而绕个弯路,先取其所在路径,用路径的PItemIDList枚举出文件的PItemIDList
就没有问题!?
implementation
uses
FileCtrl, ShellAPI, ActiveX, ShlObj, ComObj;
{$R *.DFM}
var
ShellMalloc: IMalloc;
DesktopFolder: IShellFolder;
CS : TRTLCriticalSection;
FileInfo: TSHFileInfo;
function NextPIDL(PIDL: PItemIDList): PItemIDList;
begin
Result := PIDL;
Inc(PChar(Result), PIDL^.mkid.cb);
end;
function GetPIDLSize(PIDL: PItemIDList): Integer;
begin
Result := 0;
if Assigned(PIDL) then begin
Result := SizeOf(PIDL^.mkid.cb);
while PIDL^.mkid.cb <> 0 do begin
Result := Result + PIDL^.mkid.cb;
PIDL := NextPIDL(PIDL);
end;
end;
end;
function CreatePIDL(Size: Integer): PItemIDList;
begin
try
Result := nil;
Result := ShellMalloc.Alloc(Size);
if Assigned(Result) then FillChar(Result^, Size, 0);
finally
end;
end;
procedure FreePIDL(PIDL: PItemIDList);
begin
if PIDL <> nil then ShellMalloc.Free(PIDL);
end;
function ConcatPIDLs(PIDL1, PIDL2: PItemIDList): PItemIDList;
var cb1, cb2: Integer;
begin
if Assigned(PIDL1) then cb1 := GetPIDLSize(PIDL1) - SizeOf(PIDL1^.mkid.cb) else cb1 := 0;
cb2 := GetPIDLSize(PIDL2);
Result := CreatePIDL(cb1 + cb2);
if Assigned(Result) then begin
if Assigned(PIDL1) then CopyMemory(PChar(Result), PIDL1, cb1);
CopyMemory(PChar(Result) + cb1, PIDL2, cb2);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
PIDL, RelativePIDL, NewPIDL: PItemIDList;
Eaten, Attributes: Cardinal;
path: string;
P: PWideChar;
ShellFolder: IShellFolder;
EnumIDList: IEnumIDList;
NumIDs: Cardinal;
begin
// 设置要显示的路径,取其 PItemIDList
path := 'C:\Documents and Settings\Administrator\My Documents\Delphi_Tmp';
Eaten := Length(path);
P := StringToOleStr(path);
Attributes := 0;
OLECheck(DesktopFolder.ParseDisplayName(Application.Handle, nil, P, Eaten, PIDL, Attributes));
// 根据 PItemIDList 得到 IShellFolder
// 可以直接取文件的PIDL,但是存在与以前一样的现象,现在取其目录
DesktopFolder.BindToObject(PIDL, nil, IID_IShellFolder, Pointer(ShellFolder));
// 枚举目录下的文件到 EnumIDList
ShellFolder.EnumObjects(Application.Handle, SHCONTF_NONFOLDERS, EnumIDList);
NewPIDL := nil;
while EnumIDList.Next(1, RelativePIDL, NumIDs) = S_OK do
try
// 将列出的文件PIDL与目录合并得到文件的 PItemIDList
NewPIDL := ConcatPIDLs(PIDL, RelativePIDL);
// 根据合并出的完整文件PItemIDList取文件名和属性!
FillChar(FileInfo, SizeOf(FileInfo), 0);
SHGetFileInfo(PChar(NewPIDL), 0, FileInfo, SizeOf(FileInfo), SHGFI_PIDL or SHGFI_DISPLAYNAME or SHGFI_TYPENAME);
ListBox1.Items.Add( FileInfo.szDisplayName + ' - ' + FileInfo.szTypeName );
finally
FreePIDL(NewPIDL);
FreePIDL(RelativePIDL);
end;
end;
initialization
OLECheck(SHGetDesktopFolder(DesktopFolder));
InitializeCriticalSection(CS);
OleInitialize(nil);
OLECheck(ShGetMalloc(ShellMalloc));
finalization
DesktopFolder := nil;
ShellMalloc := nil;
OleUninitialize;
DeleteCriticalSection(CS);
end.
来自:HD_Copy, 时间:2002-6-23 5:40:00, ID:1177792
to OopsWare:
老大,你们的项目完工了吗?多谢你能在百忙当中还抽时间多次关注我的问题!
不过,你的解答还不能令我满意!你的代码和Delphi自带的VirtualListView例子基本上差不多,只不
过它是显示某个目录时需要遍历一下,而你这里显示某个具体文件也要遍历一下。
其实,你应该对我这个问题的细节和要求很清楚啊,我们在MSN上讨论过的呀,这里,应该对其他朋友
说声对不起了,因为我在这个帖子中没有把一些更深的细节说出来。实际上,我是要在ListView控件中显
示硬盘中的文件,就象资源管理器右边的文件列表一模一样。有时,要显示某个目录下的所有文件;有时,
要显示从数据库中搜索出来的一些文件(这些文件可能分布在硬盘不同的分区、不同的目录中,有点象Win2000
下资源管理器中的"搜索",但又有所不同,资源管理器搜索特定的文件,肯定需要遍历目录;而我这里
并不需要,因为我从数据库中取出的文件的全路径和文件名都是已知的了)。
所以说,OopsWare上面的代码也仅仅是个变通的方法,我们姑且称为[变通方法一]。而我目前使用的
方法是下面的代码:
/* --- 取文件类型 --- */
TempStr = FileInfo.szTypeName;
if( TempStr.Length() == 0 ) //如果是系统不可识别的文件类型
{
TempStr = ExtractFileExt( FileWholeName );
if( TempStr.Length() == 0 ) //如果文件没有扩展名,则......
GetItem(i)->strType = "文件";
else
GetItem(i)->strType = UpperCase(TempStr.Delete(1,1)) + " 文件";
}
else
GetItem(i)->strType = TempStr;
我们也姑且称之为[变通方法二],这样,我就要考虑一下到底[变通方法一]和[变通方法二]哪个效率高呀?
我觉得还是[变通方法二]要高一些。其实,还有另外一种变通的方法,姑且称之为[变通方法三],就是在
调用SHGetFileInfo之前,不管是执行SelectDirectory()也好、还是SHBrowseForFolder也罢、或者是别的
什么,只要不在屏幕上显示出来,而且不会造成系统混乱、或其他什么不良的后果,只是让它初始化一下,
然后就不管它了,我觉得这种变通的方法倒是可以接受,其实,好像OopsWare也曾经说过的。当然,最好
还是有人知道怎样从根本上进行初始化。
我现在希望:
1. 有人能给出[变通方法三]。
2. 哪位英语好的朋友将我这个问题翻译过来,在诸如www.experts-exchange.com这样的地方问一下,那种
地方的高手深不可测,应该会有人能解答出来。
3. 有人能给出根本的解决方法。
在没解决之前,我目前用的方法就是我自己的[变通方法二]
再次感谢OopsWare!!
来自:OopsWare, 时间:2002-6-23 9:31:00, ID:1177867
to HD-copy:
用你的方法多次遍历一个目录的时间是一定的,而我的方法遍历一个目录第一次的
效率与你的差不多,但多次使用的速度就很快,因为IMalloc已经将这些信息记入了
Shell管理的区域。用FindFirst,FindNext可以取道文件系统的所有物理文件,但
我的方法还可以取道所有的名字空间(如网上邻居...)。
我的这段代码应该是适合所有版本的Win32的,Windows捆绑IE4以后Shell就完全是
COM了,应该更多的使用COM的思想去处理Shell操作。所谓的初始化的那段操作也就是
我代码中的 ShellFolder.EnumObjects(Application.Handle, SHCONTF_NONFOLDERS, EnumIDList);
测试一下这段代码:
function MrsGetFileType(const strFilename: string): string;
var
FileInfo: TSHFileInfo;
begin
FillChar(FileInfo, SizeOf(FileInfo), #0);
SHGetFileInfo(PChar(strFilename), 0, FileInfo, SizeOf(FileInfo), SHGFI_TYPENAME);
Result := FileInfo.szTypeName;
end;
procedure TForm1.Button3Click(Sender: TObject);
var
DesktopFolder: IShellFolder;
EnumIDList: IEnumIDList;
begin
OLECheck(SHGetDesktopFolder(DesktopFolder));
DesktopFolder.EnumObjects(Application.Handle, SHCONTF_NONFOLDERS, EnumIDList);
Caption := MrsGetFileType('C:\Documents and Settings\Administrator\My Documents\Delphi_Tmp\A.CPI')
end;
也就证实了SelectDir所初始化的内容。
Shell使用的内存是由 IMalloc 统一管理的。也许是为了节省内存的存储空间,多数
重复的内容在他管理下,仅保留一个副本,直接取得文件的PIDL应该可以获得文件的
全部属性。但是这时win2k可能忘记了将文件的说明信息存入IMalloc的共享内存
区域。换一个思路,去取文件所在目录的PIDL,再枚举目录下的所有文件,就得到了所
需要的信息。也就是说,ShBrowseForFolder(BrowseInfo);也同样使用了枚举文件的
用法,从而将文件的说明信息存入了IMalloc的管理区域。也就就像前面李颖的的贴子
中提到的"初始化了一些信息"。这应该是windows的shell编程的正常用法(我的代码多数
借见了Delphi的ShellCtrls.pas,这在D6好像只有.dcu,请到Borland的CodeCenter下载源
程序,使用D5的用户只能想其他的方法了,例如ShellPack的控件)。
你说的[变通方法三]代码如下:
var
DesktopFolder: IShellFolder;
EnumIDList: IEnumIDList;
initialization
OLECheck(SHGetDesktopFolder(DesktopFolder));
DesktopFolder.EnumObjects(Application.Handle, SHCONTF_NONFOLDERS, EnumIDList);
OleInitialize(nil);
finalization
DesktopFolder := nil;
OleUninitialize;
end.
其实我前一贴的大宗代码是希望你完全将 Shell 编程的思想,用在你的程序中。
来自:HD_Copy, 时间:2002-6-23 15:34:00, ID:1178276
to OopsWare:
哈哈哈,:D :D :D I love you , baby , mum,mum,mum,mum,mum......
说句急功近利的话,当我看到你这个帖子中在OLECheck(SHGetDesktopFolder(DesktopFolder));的后面
马上马上马上马上马上就用了EnumObjects()!!我的眼睛都发亮光了!!我的天啊,问题出在这里呀!这
就是初始化啊!!5~~~~~~~~~~~~~~ 看来,我对Shell编程还知道的不够全面,不够深刻,先前也仅仅是
模仿啊!5~~~~~~~~~~~
不过,
>>用你的方法多次遍历一个目录的时间是一定的,而我的方法遍历一个目录第一次的
>>效率与你的差不多,但多次使用的速度就很快,因为IMalloc已经将这些信息记入了
>>Shell管理的区域。
我并不这么认为。就是说,在实现我上面所说的"要显示从数据库中搜索出来的一些文件"时,你的
这种仍然需要遍历的方法,总觉得...... 因为你不能仅仅这样说"IMalloc已经将这些信息记入了Shell管
理的区域。",其实,我们都知道Windows的其它操作也都是利用缓存技术的,用我的方法在执行第二遍时
也比第一遍快的。由于在已知文件的全路径名时,通过Shell编程是不能直接取得这个文件的PIDL的,所以,
要用Shell编程,也只能遍历。
但我现在又想问一个很基本的问题了,就是Windows系统是怎样定位一个已知全路径名的文件呢?如果也是
在后台用了遍历,那我就彻底向你"投降",改用Shell编程了 :P
至此,这个问题基本解决了,非常感谢OopsWare!!!
请OopsWare到http://www.delphibbs.com/delphibbs/dispq.asp?lid=1178271来拿300分!!