How to show a "Please wait " modal form from a TThread and free it when the job is done?

Viewed 752

I've been working just for a while trying to make a modal form to inform the user to wait until the job is ends. This is a simple example of what I'm trying to do:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
  TLoader = class(TThread)
    private
      FStrings: TStrings;
      procedure ShowWait;
      procedure EndsWait;
    public
      Constructor Create(AStrings: TStrings);
      Destructor Destroy; override;
      procedure Execute; override;
  end;
var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
Var
  List: TStrings;
begin
  List := TStringList.Create;
  try
    // Load Some Data here
    TLoader.Create(List);
  finally
    List.Free;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin

end;

{ TLoader }

constructor TLoader.Create(AStrings: TStrings);
begin
  inherited Create;
  FreeOnTerminate:= True;
  FStrings:= TStringList.Create;
  FStrings.AddStrings(AStrings);
end;

destructor TLoader.Destroy;
begin
  FStrings.Free;
  inherited;
end;

procedure TLoader.EndsWait;
begin
  TForm(Application.FindComponent('FWait')).Free;
end;

procedure TLoader.Execute;
begin
  inherited;
  Synchronize(ShowWait);
  // Do Some Job while not terminated
  Sleep(1000);
  // Free Wait Form
  // This part is not working
  Synchronize(EndsWait);
end;

procedure TLoader.ShowWait;
begin
  With TForm.Create(Application) do
    begin
      // Some code
      Name:= 'FWait';
      ShowModal;
    end;
end;

end.

Everything is working as I expected, except Synchronize(EndsWait); which did not close and free the modal form.

How can I display a modal form while a TThread is running and free it when the TThread terminated?


UPDATE:

I've try to do as Remy suggest as the following:

type
  TForm2 = class(TForm)
    procedure FormShow(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
  TLoader = class(TThread)
    protected
      procedure DoTerminate; override;
      procedure DoCloseModal;
    public
      constructor Create;
      procedure Execute; override;
  end;
var
  Form2: TForm2;

implementation

{$R *.dfm}

{ TLoader }

constructor TLoader.Create;
begin
  inherited Create;
  FreeOnTerminate:= True;
end;

procedure TLoader.DoCloseModal;
begin
  Form2.ModalResult:= mrOk;
end;

procedure TLoader.DoTerminate;
begin
  inherited DoTerminate;
  Synchronize(DoCloseModal);
end;

procedure TLoader.Execute;
begin
  inherited;
  Sleep(200);
end;

procedure TForm2.FormShow(Sender: TObject);
begin
  TLoader.Create;
end;

end.

The main form button click event handler:

procedure TForm1.Button1Click(Sender: TObject);
begin
  with TForm2.Create(nil) do
    try
      ShowModal;
    finally
      Free;
    end;
end;
2 Answers

You have two choices:

  1. Do not use a modal form to begin with. TThread.Synchronize() blocks your thread until the synced method exits, but TForm.ShowModal() blocks that method until the Form is closed. Use TThread.Synchronize() (or better, TThread.Queue()) to Create()+Show() (not ShowModal()) the Wait Form, then return to the thread and let it do its work as needed, then Synchronize()/Queue() again (or, use the thread's OnTerminate event) to Close()+Free() the Wait Form when done.

  2. alternatively, if you want to use a modal Wait Form, then do not let the thread manage the Wait Form at all. Have your button OnClick handler Create()+ShowModal() (not Show()) the Wait Form, and Free() it when ShowModal() exits. Have the Wait Form internally create and terminate the thread when the Wait Form is shown and closed, respectively. If the thread ends before the Wait Form is closed, the thread's OnTerminate handler can Close() the Form (which simply sets the Form's ModalResult) so that ShowModal() will exit in the OnClick handler.

After a while, I made a uWaitForm.pas unit:

unit uWaitForm;

interface

uses
  System.Classes, Vcl.Controls, Vcl.Forms;

type
  TWaitForm = class(TForm)
    private
      FThread: TThread;
    protected
      procedure Activate; override;
      procedure DoClose(var Action: TCloseAction); override;
    public
      constructor Create(AOwner: TComponent); override;
      destructor Destroy; override;
  end;
  TWaitThread = class(TThread)
    private
      FForm: TWaitForm;
      FModalResult: TModalResult;
    protected
      procedure Execute; override;
      procedure DoSetModalResult;
    public
      constructor Create(AForm: TWaitForm);
      destructor Destroy; override;
  end;

implementation

{ TWaitForm }

procedure TWaitForm.Activate;
begin
  inherited;
  FThread:= TWaitThread.Create(Self);
end;

constructor TWaitForm.Create(AOwner: TComponent);
begin
  inherited CreateNew(AOwner);
  Name:= 'WaitForm';
  BorderStyle:= bsDialog;
  Caption:= 'Please wait...';
  Width:= 200;
  Height:= 150;
  Position:= poDesktopCenter;
end;

destructor TWaitForm.Destroy;
begin
  if Assigned(FThread) then
    FThread.Terminate;
  inherited;
end;

procedure TWaitForm.DoClose(var Action: TCloseAction);
begin
  inherited;
  if Assigned(FThread) then
    begin
      TWaitThread(FThread).FModalResult:= mrCancel;
      FThread.Terminate;
    end;

end; 


{ TWaitThread }

constructor TWaitThread.Create(AForm: TWaitForm);
begin
  inherited Create;
  FreeOnTerminate:= True;
  FForm:= AForm;
  FModalResult:= mrOk;
end;

destructor TWaitThread.Destroy;
begin
  if Assigned(FForm) then
    Synchronize(nil, DoSetModalResult);
  inherited;
end;

procedure TWaitThread.DoSetModalResult;
begin
  FForm.ModalResult:= FModalResult;
end;

procedure TWaitThread.Execute;
begin
  inherited;
  // Do the work while not terminated
  // You can check for Terminated here and set FModalResult accordingly to receive it from ShowModal
  // By default it's mrOk (Job is done, the thread doesn't cancled)
  Sleep(200);
end;

end.

Then use it

  with TWaitForm.Create(nil) do
    try
      //Get ModalResult here and do something accordingly
      // mrCancel means the dialog form closed and the the Thread job aborted
      // mrOk The job is done and the form closed
      ShowModal;
    finally
      Free;
    end;
Related