Mohammadreza
Mohammadreza

Reputation: 15

Error on Close Form when open Query in Thread (Delphi)

I have a Query and open it in my Thread. It works correctly and I don't want to use Synchronize, because Synchronize makes main Form don't response while the Query not complete fetch. When close the Form blow error shown:

System Error. Code: 1400. Invalid window handle

type
  TMyThread = class(TThread)
  public
    procedure Execute; override;
    procedure doProc;
  end; { type }
.
.
.
procedure TMyThread.doProc;
begin
  Form1.Query1.Open;
end;

procedure TMyThread.Execute;
begin
  inherited;

  doProc;
end;
.
.
.
procedure TForm1.Button1Click(Sender: TObject);
begin
  thrd := TMyThread.Create(True);
  thrd.FreeOnTerminate := True;
  thrd.Resume;
end;

Note : Query has a lot of record.

Upvotes: 1

Views: 3110

Answers (2)

Johan
Johan

Reputation: 76641

The problem is that the VCL is not thread safe.
In order to have the query execute in parallel to all other things going on you'll have to decouple it from the Form.

That means you'll have to create the Query at runtime using code:

type
  TMyThread = class(TThread)
  private
    FQuery: TQuery;
    FOnTerminate: TNotifyEvent;
  public
    constructor Create(AQuery: TQuery);
    destructor Destroy; override;
    procedure Execute; override;
    procedure doProc;
    //Add an event handler to do cleanup on termination.
    property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
  end; { type }

constructor TMyThread.Create(AQuery: TQuery);
begin
  inherited Create(True);
  FQuery:= AQuery;
end;

procedure TMyThread.doProc;
begin
  FQuery1.Open;
  Synchronize(
    //anonymous method, use a separate procedure in older Delphi versions
    procedure
    begin
      Form1.Button1.Enabled:= true;  //reenable the button when we're done.
    end
  );
end;

procedure TMyThread.Execute;
begin
  inherited;
  doProc;
end;

destructor TMyThread.Destroy;
begin
  if Assigned(FOnterminate) then FOnTerminate(Self);
  inherited;
end;

In the OnClick for Button1 you'll do the following:

type 
  TForm1 = class(TForm)
  private
    AQuery: TQuery;
    ...
  end; {type}

procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Enabled:= false;  //disable the button so it cannot be started twice.
  thrd.Free;
  AQuery:= TQuery.Create;
  AQuery.SQL.Text:= .....
  thrd := TMyThread.Create(AQuery);
  thrd.OnTerminate:= MyTerminationHandler;
  thrd.FreeOnTerminate:= False;
  thrd.Resume;
end;

Finally assign cleanup code to the termination handler of the thread. If you destroy the Query in the thread then you cannot use FreeOnTerminate:= true, but you'll have to Free the thread yourself.

procedure TForm1.MyTerminationHandler(Sender: TObject);
begin
  FreeAndNil(AQuery);
end;

Warning
This code will only work if you start 1 thread.
If you want start this thread multiple times (i.e. run multiple queries at the same time), you'll have to create an array of threads e.g.:

TQueryThreads = record
  MyThread: TMyThread;
  MyQuery: TQuery;
  constructor Create(SQL: string);
end; {record}

TForm1 = class(TForm)
private
  Threads: array of TQueryThreads;
 ....
end; {TForm1}     

Note that this code will not work in the BDE, because that library does not support multiple running queries at the same time
If you want to do that you'll have to use ZEOS or something like that.

As per TLama's suggestion:
I would suggest switching the BDE TQuery component to ADO, or downloading something like ZEOS components. The BDE is very outdated and has a lot of quirks that will never get fixed because it is no longer maintained.

The only issue that remains is cleaning up the connection if Form1 is closed.
If it's your main form it really does not matter because your whole application will go down.
If it's not your main form than you'll need to disable closing the form by filling the OnCanClose handler.

TForm1.CanClose(Sender: TObject; var CanClose: boolean);
begin
  CanClose:= thrd.Finished;
end;

Upvotes: 3

Sir Rufo
Sir Rufo

Reputation: 19106

You should prevent any action (user and program) in the MainThread without blocking it. This can easily be done by a modal form, that cannot be closed by the user.

The thread can do anything as long as it takes and the final (synchronized) step is to close that modal form.

procedure OpenDataSetInBackground( ADataSet : TDataSet );
var
  LWaitForm : TForm;
begin
  LWaitForm := TForm.Create( nil );
  try
    LWaitForm.BorderIcons := []; // no close buttons

    TThread.CreateAnonymousThread(
      procedure
      begin
        try
          ADataSet.Open;
        finally
          TThread.Synchronize( nil, 
            procedure
            begin
              LWaitForm.Close;
            end );
        end;
      end );
    try
      LWaitForm.ShowModal;
    finally
      LWorkThread.Free;
    end;
  finally
    LWaitForm.Free;
  end;
end;

But you have to be careful with this and you should never try do start more than one parallel thread with this code unless you really know, what you are doing.

Upvotes: 1

Related Questions