-1

I'm trying to search for all files in all subfolders so it takes long time and application stop responding, so I used Thread (it's first time work with Threads) I read about it and I found this way to create and execute threads, but nothing happen when I call the thread, and I don't understand why I couldn't use the added components on the main form, I had to re-declare it again?
what I miss here?

type
  TSearchThread = class(TThread)
  private
    { Private declarations }
  protected
    procedure Execute; override;
  end;

procedure AddAllFilesInDir(const Path: string; ListBox:TsListBox);
var
  SR: TSearchRec;
  I: Integer;
begin
  if FindFirst(IncludeTrailingBackslash(Path) + '*.*', faAnyFile or faDirectory, SR) = 0 then
    try
      repeat
        if (SR.Attr and faDirectory) = 0 then
            ListBox.Items.Add(Path+'\'+SR.Name)
        else if (SR.Name <> '.') and (SR.Name <> '..') then
          AddAllFilesInDir(IncludeTrailingBackslash(Path) + SR.Name, ListBox);
          Form1.sPanel2.Caption := Path+'\'+SR.Name;
          Form1.sPanel2.Refresh;
          ListBox.Refresh;
      until FindNext(Sr) <> 0;
    finally
      FindClose(SR);
    end;
end;

procedure TSearchThread.Execute;
var FileList: TsListBox;
    I: Integer;
    {Here I had to re-declare objects}
    sDirectoryEdit1: TsDirectoryEdit;
    sListBox1: TsListBox;
begin
      FileList := TsListBox.Create(nil);
      FileList.Parent := sListBox1;
      FileList.Visible := False;
      AddAllFilesInDir(sDirectoryEdit1.Text+'\', FileList);
      for I := 0 to FileList.Count -1 do
      if sListBox1.Items.IndexOf(FileList.Items.Strings[I]) = -1 then
      sListBox1.Items.Add(FileList.Items.Strings[I]);
      FileList.Clear;
end;


procedure TForm1.sDirectoryEdit1Change(Sender: TObject);
begin
    TSearchThread.Create(False);
end;
Harriv
  • 6,029
  • 6
  • 44
  • 76
Delphi Lover
  • 85
  • 1
  • 11
  • 3
    This has been covered here many times. You cannot access VCL objects from a thread. Use `TThread.Queue` or `TThread.Synchronize`. – David Heffernan Oct 20 '15 at 17:26
  • 1
    You need to learn about threads. You need to override TThread's Execute. – MartynA Oct 20 '15 at 17:27
  • Not another *let's violate all the rules by accessing UI controls from my thread directly* post. There are literally dozens of existing Delphi questions about how to use threads. Search for them, and find an example that shows you how to use them properly. Your first clue that things weren't right should have been when you had to redeclare all your UI controls inside the thread's `Execute` method; you should have immediately known then you were doing something very wrong. – Ken White Oct 20 '15 at 17:27
  • @JensBorrisholt the existing questions still haven't the _**required answer_** so I didn't close them, should I close the question even it have no answer? – Delphi Lover Oct 20 '15 at 17:36
  • 3
    @JensBorrisholt Please don't pester the asker in this way. Asker has asked 4 questions including this one. One question has an accepted answer. Another question has no answer. And the final question has answers but it's far from clear to me that they should be accepted. – David Heffernan Oct 20 '15 at 17:41
  • 2
    @Jens Perhaps you made a mistake. The few times in the past that I have tried to reason with you over your answers, you have not been keen to listen. So, it seems plausible to me that there is a deficiency in your answer that you cannot yet see. The last time I tried to engage with you you accused me of plagiarism and insulted me. – David Heffernan Oct 20 '15 at 17:48
  • If you have the time .. Go ahead. – Jens Borrisholt Oct 20 '15 at 17:50
  • 3
    @Jens: Where in the [help] does it say you have to close all existing questions before you can ask a new one? In fact, where does it say you have to close *any* questions at all? – Ken White Oct 20 '15 at 17:53
  • 1
    @JensBorrisholt thank you for your interesting but your comment doesn't help for anything, it's _**Useless**_ – Delphi Lover Oct 20 '15 at 17:55
  • @DavidHeffernan I will read about `TThread.Synchronize` and try it, thank you. – Delphi Lover Oct 20 '15 at 18:02
  • 1
    What you need to learn as well is the concept of layering. Your thread should know nothing of the UI. Define it in a separate layer, a low level layer. Put it in a separate unit. Design it to be re-usable. – David Heffernan Oct 20 '15 at 18:08
  • @KenWhite I read and watched few examples before asking my question but I didn't notice that we can't use VCL objects in threads , but if i had re-declare objects in thread there is no way to get thread output to real objects later? – Delphi Lover Oct 20 '15 at 18:09
  • As I said in my previous comment, search here for `delphi thread`. There are many previous examples that demonstrate the proper way to update GUI controls from a thread. – Ken White Oct 20 '15 at 18:11
  • 2
    @JensBorrisholt: I agree with DavidH, and you have a strange, hectoring attitude to people you answer (like berating them saying it's their fault if your "solution" doesn't work for them). Give it a rest and/or grow up. – MartynA Oct 20 '15 at 18:14
  • 2
    @DelphiLover You don't need, nor want, the thread to know about the UI. The thread should be given a specification of the task, the directory to start in, whether to recursive, what types of files to return etc. Then it should enumerate the files/dirs. When it finds a value it should yield it. That might involve queueing it to the UI. But that would be expensive. Better is to batch them up and only queue ever, say, 0.1s. – David Heffernan Oct 20 '15 at 18:33
  • @DavidHeffernan can you check my updated answer and if it's correct and didn't violates stackoverflow terms (even it never was) undelete it? – Master Oct 23 '15 at 22:25
  • @Master I cannot undelete it – David Heffernan Oct 23 '15 at 22:27
  • @DavidHeffernan OK thank you – Master Oct 23 '15 at 22:43

1 Answers1

1

Ok, let me give it a try:

First a new version of your thread:

uses
  IOUtils;

type
  TFileFoundEvent = procedure(const Path: string; const SearchRec: TSearchRec) of object;

  TSearchThread = class(TThread)
  private
    FPath: string;
    FSearchRec: TSearchRec;
    FFileFoundEvent: TFileFoundEvent;
  protected
    procedure Execute; override;
  public
    Constructor Create(const aPath: string; aFileFoundEvent: TFileFoundEvent); reintroduce;
  end;

  { TSearchThread }

constructor TSearchThread.Create(const aPath: string; aFileFoundEvent: TFileFoundEvent);
begin
  // Create the Thread non suspended
  inherited Create(false);

  // Copy parameters to local members.
  FFileFoundEvent := aFileFoundEvent;
  FPath := aPath;

  // Make the sure the thread frees itself after execution
  FreeOnTerminate := True;
end;

procedure TSearchThread.Execute;
var
  FilterPredicate: TDirectory.TFilterPredicate;
begin
  // FilterPredicate is an in-place anonymous method to be called each time the TDirectory.GetFiles finds a file
  FilterPredicate := function(const Path: string; const SearchRec: TSearchRec): Boolean
    begin
      // Since we can not access from within Synchronize we need to copy iot to a member of the class
      FSearchRec := SearchRec;

      // You cannot access VCL objects directly from a thread.
      // So you need to call Syncronize
      // For more info look in the online help
      // http://docwiki.embarcadero.com/Libraries/Seattle/en/System.Classes.TThread.Synchronize
      Synchronize(nil,
        procedure
        begin
          FFileFoundEvent(FPath, FSearchRec);
        end);

      Result := True;
    end;

  // Do the search
  TDirectory.GetFiles(FPath, TSearchOption.soTopDirectoryOnly, FilterPredicate)
end;

The main diffrence are that I pass a callback proceudre onto the constructor of the thread. And ofcause I uses TDirectory.GetFiles to search for files. You'll find TDirectory.GetFiles in IOUtils

Then you need to use it: Place a Listbox on your from and then call it like this :

Form definition:

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    procedure FormCreate(Sender: TObject);
  private
    procedure FileFoundEvent(const Path: string; const SearchRec: TSearchRec);
  public
    { Public declarations }
  end;

...

implementation

procedure TForm1.FileFoundEvent(const Path: string; const SearchRec: TSearchRec);
begin
  ListBox1.Items.Add(SearchRec.Name);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  TSearchThread.Create(ExtractFilePath(Application.ExeName), FileFoundEvent);
end;

If you don't want to see the ongoing results of the searching, but rather want some speed you can create a version of the searchthread that gives you the result all at once:

uses
  IOUtils;

type
  TSearchThread = class(TThread)
  private
    FSearchPath: String;
    FResultBuffer: TStrings;
  protected
    procedure Execute; override;
  public
    constructor Create(const aSearchPath: string; aResultBuffer: TStrings); overload;
  end;

constructor TSearchThread.Create(const aSearchPath: string; aResultBuffer: TStrings);
begin
  inherited Create(false);
  FSearchPath := IncludeTrailingPathDelimiter(aSearchPath);
  FResultBuffer := aResultBuffer;
  FreeOnTerminate := True;
end;

procedure TSearchThread.Execute;
var
  FBuffer: TStringlist;
  Filename: String;
begin
  Synchronize(nil,
    procedure
    begin
      FResultBuffer.Text := 'Searching ' + FSearchPath;
    end);

  FBuffer := TStringlist.Create;
  for Filename in TDirectory.GetFiles(FSearchPath, TSearchOption.soAllDirectories, nil) do
    FBuffer.Add(Filename);

  Synchronize(nil,
    procedure
    begin
      FResultBuffer.Assign(FBuffer);
    end);

  FreeAndNil(FBuffer);
end;

This thread you have to call in a bit diffent way.

The form setup i still the same as before: A Listbox on a Form.

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    procedure FormCreate(Sender: TObject);
  private
    Stopwatch: TStopwatch;
    procedure SearchThreadTerminate(Sender: TObject);
  public
    { Public declarations }
  end;

And then the implementation:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Stopwatch := TStopwatch.StartNew;

  with TSearchThread.Create('C:\Program Files (x86)\Embarcadero\', ListBox1.Items) do
    OnTerminate := SearchThreadTerminate;
end;

procedure TForm1.SearchThreadTerminate(Sender: TObject);
begin
  Stopwatch.Stop;
  Caption := 'Elapsed Milliseconds: ' + IntToStr(Stopwatch.ElapsedMilliseconds) + ' Files found: ' + IntToStr(ListBox1.Items.Count);
end;

The advantage of this version is speed. Updaing the screen is slow, and the first solution updated the screen for each file it found, while this one only updates the screen twice.

Try it out.

Jens Borrisholt
  • 6,174
  • 1
  • 33
  • 67
  • 1
    Um, is there any point in doing the work in a thread if you don't call Begin/EndUpdate on the ListBox's Items? – MartynA Oct 20 '15 at 18:48
  • @MartynA Um, keeping the application responsive while the search is in progress? – Jerry Dodge Oct 20 '15 at 20:17
  • Buffering for a short period before synchronizing data will prevent the main thread from choking if there are lots of files. – LU RD Oct 20 '15 at 20:27
  • 2
    @JerryDodge: Nope. There are much better (and simpler) ways to do it than in this answer. Imagine there are 100k+ files to add ... – MartynA Oct 20 '15 at 20:40
  • @MartynA correct if there are 100k+ files it will slow down the search process, but the GUI wil respond durring searching. I've made a updated vestion that only updates the GUI twice. – Jens Borrisholt Oct 21 '15 at 07:50