At the moment I'm doing something like this:
var
Files: TArray<String>;
if IncludeSubDirs then
Files := TDirectory.GetFiles(Path, '*.exe', TSearchOption.soAllDirectories)
else
Files := TDirectory.GetFiles(Path, '*.exe', TSearchOption.soTopDirectoryOnly);
Path is a user defined String that can point to any existing directory. For "big" directories with a ton of files and with IncludeSubDirs = True (C:\Windows\ for example) GetFiles takes a very long time (like 30 secs).
What would be the fastest way to list all the exe files in a "big" directory under Windows with Delphi (if any)?
CodePudding user response:
I did some benchmarking and for a huge directory FindFirst / FindNext is about 1.5 to 3% faster than using TDirectory. I would say than both are equivalent in speed (for my use case I saved about 1 sec per minute). I ended up using FindFirst / FindNext since you get results progressively and not all at once, memory management seemed better and it's easier to cancel midway. I also used a TThread to avoid blocking my UI.
This is what I ended up with:
procedure TDirectoryWorkerThread.AddToTarget(const Item: String);
begin
if (not Self.Parameters.DistinctResults) or (Self.Target.IndexOf(Item) = -1) then
Self.Target.Add(Item);
end;
procedure TDirectoryWorkerThread.ListFilesDir(Directory: String);
var
SearchResult: TSearchRec;
begin
Directory := IncludeTrailingPathDelimiter(Directory);
if FindFirst(Directory '*', faAnyFile, SearchResult) = 0 then
begin
try
repeat
if (SearchResult.Attr and faDirectory) = 0 then
begin
if (Self.Parameters.AllowedExtensions = nil) or (Self.Parameters.AllowedExtensions.IndexOf(ExtractFileExt(SearchResult.Name)) <> -1) then
AddToTarget(Directory SearchResult.Name);
end
else if Self.Parameters.IncludeSubDirs and (SearchResult.Name <> '.') and (SearchResult.Name <> '..') then
ListFilesDir(Directory SearchResult.Name);
until Self.Terminated or (FindNext(SearchResult) <> 0);
finally
FindClose(SearchResult);
end;
end;
end;
CodePudding user response:
The fastest way is to virtualize the list and use the Windows API. This excerpt of code only shows the file name. Tested with D11. "ListView" must have the property "OwnerData = True", use the metod "onData" and "ViewStyle = vsReport". In "Edit1.Text" there is the folder to show.
USES ShlObj, ComObj, ActiveX
TYPE PShellItem = ^TShellItem;
TShellItem = Record
FullID : PItemIDList;
ID : PItemIDList;
Empty : Boolean;
DisplayName : String;
TypeName : String;
ImageIndex : Integer;
Size : Integer;
Attributes : Integer;
ModDate : String;
End;
TForm1 = class(TForm)
Button1: TButton;
ListView: TListView;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
procedure ListViewData(Sender: TObject; Item: TListItem);
private
FIDesktopFolder : IShellFolder;
FIShellFolder : IShellFolder;
FIDList : TList;
PROCEDURE SetPath(ID : PItemIDList);
PROCEDURE PopulateIDList(ShellFolder : IShellFolder);
FUNCTION ShellItem(Index : Integer) : PShellItem;
public
end;
[...]
procedure TForm1.Button1Click(Sender: TObject);
VAR P : PWideChar;
NewPIDL : PItemIDList;
Flags : LongWord;
NumChars : LongWord;
begin
OLECheck(SHGetDesktopFolder(FIDesktopFolder));
FIDList := TList.Create;
NumChars := Length(Edit1.Text);
Flags := 0;
P := StringToOleStr(Edit1.Text);
OLECheck(FIDesktopFolder.ParseDisplayName(Application.Handle,nil,P,NumChars,NewPIDL,Flags));
SetPath(NewPIDL);
end;
PROCEDURE TForm1.SetPath(ID: PItemIDList);
VAR NewShellFolder : IShellFolder;
BEGIN
OLECheck(FIDesktopFolder.BindToObject(ID,nil,IID_IShellFolder,Pointer(NewShellFolder)));
ListView.Items.BeginUpdate;
Try PopulateIDList(NewShellFolder);
If ListView.Items.Count>0 Then
Begin
ListView.Selected := ListView.Items[0];
ListView.Selected.Focused := True;
ListView.Selected.MakeVisible(False);
End;
Finally
ListView.Items.EndUpdate;
End;
END;
PROCEDURE TForm1.PopulateIDList(ShellFolder: IShellFolder);
CONST Flags = SHCONTF_FOLDERS or SHCONTF_NONFOLDERS or SHCONTF_INCLUDEHIDDEN; //cartelle
Flags1 = SHCONTF_NONFOLDERS or SHCONTF_INCLUDEHIDDEN; //senza cartelle
VAR ID : PItemIDList;
EnumList : IEnumIDList;
NumIDs : LongWord;
SaveCursor : TCursor;
ShellItem : PShellItem;
function GetDisplayName(ShellFolder : IShellFolder;
PIDL : PItemIDList): string;
var StrRet : TStrRet;
P : PChar;
Flags : Integer;
begin
Result := '';
Flags := SHGDN_FORPARSING; //per vedere le estensioni anche quando sono nascoste
ShellFolder.GetDisplayNameOf(PIDL, Flags, StrRet);
case StrRet.uType Of
STRRET_CSTR : SetString(Result, StrRet.cStr, Length(StrRet.cStr));
STRRET_OFFSET : Begin
P:[email protected][StrRet.uOffset-SizeOf(PIDL.mkid.cb)];
SetString(Result,P,PIDL.mkid.cb-StrRet.uOffset);
End;
STRRET_WSTR : Result:=StrRet.pOleStr;
end;
Result := ExtractFileName(Result); //solo il nome
end;
BEGIN
SaveCursor := Screen.Cursor;
Try Screen.Cursor := crHourglass;
OleCheck(ShellFolder.EnumObjects(Application.Handle,Flags1,EnumList));
FIShellFolder := ShellFolder;
While EnumList.Next(1,ID,NumIDs)=S_OK do
Begin
ShellItem := New(PShellItem);
ShellItem.ID := ID;
ShellItem.DisplayName := GetDisplayName(ShellFolder,ID);
ShellItem.Empty := True;
FIDList.Add(ShellItem);
End;
ListView.Items.Count := FIDList.Count;
ListView.Repaint;
Finally
Screen.Cursor:=SaveCursor;
End;
END;
FUNCTION TForm1.ShellItem(Index : Integer) : PShellItem;
BEGIN
Result := PShellItem(FIDList[Index]);
END;
function IsFolder(ShellFolder: IShellFolder; ID: PItemIDList): Boolean;
var Flags : UINT;
begin
Flags := SFGAO_FOLDER;
ShellFolder.GetAttributesOf(1, ID, Flags);
Result := SFGAO_FOLDER and Flags <> 0;
end;
procedure TForm1.ListViewData(Sender: TObject; Item: TListItem);
var Attrs : string;
begin
if (Item.Index>FIDList.Count) then Exit;
with ShellItem(Item.Index)^ do
begin
Item.Caption := DisplayName;
Item.ImageIndex := ImageIndex;
if ListView.ViewStyle<>vsReport then Exit;
if not IsFolder(FIShellFolder, ID)
then Item.SubItems.Add(IntToStr(Size) ' Kb')
else Item.SubItems.Add('');
Item.SubItems.Add(TypeName);
try Item.SubItems.Add(ModDate);
except
end;
if Bool(Attributes and FILE_ATTRIBUTE_READONLY) then Attrs:=Attrs 'R';
if Bool(Attributes and FILE_ATTRIBUTE_HIDDEN) then Attrs:=Attrs 'H';
if Bool(Attributes and FILE_ATTRIBUTE_SYSTEM) then Attrs:=Attrs 'S';
if Bool(Attributes and FILE_ATTRIBUTE_ARCHIVE) then Attrs:=Attrs 'A';
end;
Item.SubItems.Add(Attrs);
end;
