首页  编辑  

关于Windows2000的Bug和文件类型

Tags: /超级猛料/Stream.File.流、文件和目录/Shell操作/   Date Created:

关于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分!!