Watch, Follow, &
Connect with Us

For forums, blogs and more please visit our
Developer Tools Community.


Welcome, Guest
Guest Settings
Help

Thread: Adding TPanel to TForm from TThread - problem


This question is answered. Helpful answers available: 0. Correct answers available: 1.


Permlink Replies: 3 - Last Post: Mar 30, 2017 1:55 AM Last Post By: Grzegorz Żochow...
Grzegorz Żochow...

Posts: 5
Registered: 9/30/16
Adding TPanel to TForm from TThread - problem  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Mar 29, 2017 5:51 AM
Hello!

I have a complex problem with creating visual components in therad. I hope I prepared the simplest demo of a core of the problem.
Demo application has main form and 3 buttons. Main form has defined on-user-message event:
public
 procedure OnAddPanelMessage(var Msg: TMessage); message WM_ADD_PANEL_MESSAGE;
 
...
 
implementation
 
procedure Tfrm_Main.OnAddPanelMessage(var Msg: TMessage);
var
	panel: TPanel;
	PS, PS1: PString;
begin
	PS := Pointer(Msg.WParam); // Panel name
	PS1 := Pointer(Msg.LParam); // Panel caption value
 
	// Creatig panel
	panel := TPanel.Create(Self);
	panel.Parent := Self;
	panel.Align := alBottom;
	panel.Name := PS^;
	panel.Caption := PS1^;
	panel.Height := 30;
  panel.Visible := True;
 
	Msg.Result := 0;
end;


First button fires OnAddPanelMessage using OnClick event:
procedure Tfrm_Main.btn_1Click(Sender: TObject);
var
	ls1, ls2: longint;
	s1, s2: string;
begin
	// preparing variables to send message
	ls1 := Integer(@s1);
	ls2 := Integer(@s2);
	s1 := 'PanelName1';
	s2 := 'PanelCaption';
 
	// Sending 'Add Panel' order
	Perform(WM_ADD_PANEL_MESSAGE, ls1, ls2);
end;


It works fine. Second button runs Thread:
procedure Tfrm_Main.btn_3Click(Sender: TObject);
begin
  MainThread := TMainThread.Create;
  MainThread.FreeOnTerminate := True;
end;


and thread executes thoose lines:
procedure TMainThread.Execute;
var
	ls1, ls2: longint;
	s1, s2: string;
begin
	inherited;
 
	// preparing variables to send message
	ls1 := Integer(@s1);
	ls2 := Integer(@s2);
	s1 := 'PanelName2';
	s2 := 'PanelCaption';
 
	// Sending 'Add Panel' order
	frm_Main.Perform(WM_ADD_PANEL_MESSAGE, ls1, ls2);
end;


I use Perform to send creating and VCL manipulating code to application main thread.
The only one difference between TMainThread.Execute and Tfrm_Main.btn_1Click is the panel name.
If I click first button, everything is as I expect. The panel appears. Second button doesn't create visible component but does something, because if I clicked first second button and next first button, there was a space between a bottom form border and visible panel.

Third button has onClick procedure:
procedure Tfrm_Main.btn_2Click(Sender: TObject);
var
	p: TPanel;
begin
	p := TPanel(FindComponent('PanelName1'));
	p.Caption := '123';
	ShowMessage(p.Caption);
	p := TPanel(FindComponent('PanelName2'));
	p.Caption := '123';
	ShowMessage(p.Caption);
end;

The messages show: '123' and ''.

Second problem is error ERROR_INVALID_WINDOW_HANDLE during closing program. In my basic application it happends too. Components created inside thread by SendMessage() or Perform() are invalid if I use them inside main application thread.

What is wrong? Both problems exist in my application and are important to solve.

Thank you in advance for help.
Paul TOTH

Posts: 196
Registered: 1/2/17
Re: Adding TPanel to TForm from TThread - problem
Helpful
Click to report abuse...   Click to reply to this thread Reply
  Posted: Mar 29, 2017 6:07 AM   in response to: Grzegorz Żochow... in response to: Grzegorz Żochow...
VCL is not ThreadSafe use SendMessage instead of Perform

Perform bypass the Windows message queue and send a message directly
to the control's window procedure, so it's executed from the thread and
not the MainThread

Le 29/03/2017 à 14:51, Grzegorz Żochowski a écrit :
Hello!

I have a complex problem with creating visual components in therad. I hope I prepared the simplest demo of a core of the problem.
Demo application has main form and 3 buttons. Main form has defined on-user-message event:
public
 procedure OnAddPanelMessage(var Msg: TMessage); message WM_ADD_PANEL_MESSAGE;
 
...
 
implementation
 
procedure Tfrm_Main.OnAddPanelMessage(var Msg: TMessage);
var
	panel: TPanel;
	PS, PS1: PString;
begin
	PS := Pointer(Msg.WParam); // Panel name
	PS1 := Pointer(Msg.LParam); // Panel caption value
 
	// Creatig panel
	panel := TPanel.Create(Self);
	panel.Parent := Self;
	panel.Align := alBottom;
	panel.Name := PS^;
	panel.Caption := PS1^;
	panel.Height := 30;
  panel.Visible := True;
 
	Msg.Result := 0;
end;


First button fires OnAddPanelMessage using OnClick event:
procedure Tfrm_Main.btn_1Click(Sender: TObject);
var
	ls1, ls2: longint;
	s1, s2: string;
begin
	// preparing variables to send message
	ls1 := Integer(@s1);
	ls2 := Integer(@s2);
	s1 := 'PanelName1';
	s2 := 'PanelCaption';
 
	// Sending 'Add Panel' order
	Perform(WM_ADD_PANEL_MESSAGE, ls1, ls2);
end;


It works fine. Second button runs Thread:
procedure Tfrm_Main.btn_3Click(Sender: TObject);
begin
  MainThread := TMainThread.Create;
  MainThread.FreeOnTerminate := True;
end;


and thread executes thoose lines:
procedure TMainThread.Execute;
var
	ls1, ls2: longint;
	s1, s2: string;
begin
	inherited;
 
	// preparing variables to send message
	ls1 := Integer(@s1);
	ls2 := Integer(@s2);
	s1 := 'PanelName2';
	s2 := 'PanelCaption';
 
	// Sending 'Add Panel' order
	frm_Main.Perform(WM_ADD_PANEL_MESSAGE, ls1, ls2);
end;


I use Perform to send creating and VCL manipulating code to application main thread.
The only one difference between TMainThread.Execute and Tfrm_Main.btn_1Click is the panel name.
If I click first button, everything is as I expect. The panel appears. Second button doesn't create visible component but does something, because if I clicked first second button and next first button, there was a space between a bottom form border and visible panel.

Third button has onClick procedure:
procedure Tfrm_Main.btn_2Click(Sender: TObject);
var
	p: TPanel;
begin
	p := TPanel(FindComponent('PanelName1'));
	p.Caption := '123';
	ShowMessage(p.Caption);
	p := TPanel(FindComponent('PanelName2'));
	p.Caption := '123';
	ShowMessage(p.Caption);
end;

The messages show: '123' and ''.

Second problem is error ERROR_INVALID_WINDOW_HANDLE during closing program. In my basic application it happends too. Components created inside thread by SendMessage() or Perform() are invalid if I use them inside main application thread.

What is wrong? Both problems exist in my application and are important to solve.

Thank you in advance for help.
Remy Lebeau (Te...


Posts: 9,447
Registered: 12/23/01
Re: Adding TPanel to TForm from TThread - problem
Helpful
Click to report abuse...   Click to reply to this thread Reply
  Posted: Mar 29, 2017 11:43 AM   in response to: Grzegorz Żochow... in response to: Grzegorz Żochow...
Grzegorz wrote:

I have a complex problem with creating visual components in therad.

You should not be doing that in the first place. The VL is not thread-safe,
and cannot be used safely from outside of the main UI thread. If a worker
thread needs to touch the UI, it must synchronize with the main UI thread.

First button fires OnAddPanelMessage using OnClick event:

You should be using Native(U)Int instead of Integer, otherwise your code
will truncate the pointer values on 64bit systems. Besides, Perform() is
declared using Native(U)Int parameters anyway (actually it uses WPARAM and
LPARAM to match SendMessage(), and those types are alias for Native(U)Int).

In any case, a better design is to instead wrap the Panel code in its own
method, and then have the button click handler and message handler just call
that method:

private
  procedure AddPanel(const AName, ACaption: string);
 
public
  procedure OnAddPanelMessage(var Msg: TMessage); message WM_ADD_PANEL_MESSAGE;
  ...
 
procedure Tfrm_Main.AddPanel(const AName, ACaption: string);
var
  panel: TPanel;
begin
  panel := TPanel.Create(Self);
  try
    panel.Parent := Self;
    panel.Align := alBottom;
    panel.Name := AName;
    panel.Caption := ACaption;
    panel.Height := 30;
    panel.Visible := True;
  except
    panel.Free;
    raise;
  end;
end;
 
procedure Tfrm_Main.OnAddPanelMessage(var Msg: TMessage);
begin
  AddPanel(PString(Msg.WParam)^, PString(Msg.LParam)^);
end;
 
procedure Tfrm_Main.btn_1Click(Sender: TObject);
begin
  AddPanel('PanelName1', 'PanelCaption');
end;


MainThread := TMainThread.Create;
MainThread.FreeOnTerminate := True;

You should be setting FreeOnTerminate in the thread's constructor instead.
You should not be setting it externally like this, unless you create the
thread suspended first, and then resume it afterwards. A better design is
to not rely on FreeOnTerminate at all, especially since you are not nil'ing
your MainThread variable when the thread terminates.

and thread executes thoose lines:

That code is not thread-safe. You are (indirectly) calling OnAddPanelMessage()
in the context of the worker thread, not in the context of the main UI thread.
You need to use SendMessage() instead:

procedure TMainThread.Execute;
var
  s1, s2: string;
begin
  s1 := 'PanelName2';
  s2 := 'PanelCaption';
  SendMessage(frm_Main.Handle, WM_ADD_PANEL_MESSAGE, WPARAM(@s1), LPARAM(@s2));
end;


However, even that is not thread-safe as-is, either! That is because the
TWinControl.Handle property is not thread-safe. The VCL can destroy and
recreate a TWinControl's HWND at any time for any reason, and reading the
Handle property when no HWND is allocated causes a new HWND to be created
in the context of the calling thread. If that ends up being your worker
thread, you will effectively kill your UI! Your thread is not accounting
for these issues.

The safer approach is to use the TThread.Synchronize() method instead:

procedure TMainThread.AddPanel;
begin
  frm_Main.AddPanel('PanelName2', 'PanelCaption');
end;
 
procedure TMainThread.Execute;
begin
  Synchronize(@AddPanel);
end;


Or, in modern Delphi versions, you can use an anonymous procedure:

procedure TMainThread.Execute;
begin
  Synchronize(
    procedure
    begin
      frm_Main.AddPanel('PanelName2', 'PanelCaption');
    end
  );
end;


The alternative is to send your custom message to a persistent HWND that
is guaranteed to not be destroyed behind your back.

You can use the TApplication window for that:

private
  function AppMessage(var Message: TMessage): Boolean;
 
procedure Tfrm_Main.FormCreate(Sender: TObject);
begin
  Application.HookMainWindow(@AppMessage);
end;
 
procedure Tfrm_Main.FormDestroy(Sender: TObject);
begin
  Application.UnhookMainWindow(@AppMessage);
end;
 
function Tfrm_Main.AppMessage(var Message: TMessage): Boolean;
begin
  if Message.Msg = WM_ADD_PANEL_MESSAGE then
  begin
    AddPanel(PString(Message.WParam)^, PString(Message.LParam)^);
    Result := True;
  end;
end;
 
procedure TMainThread.Execute;
var
  s1, s2: string;
begin
  s1 := 'PanelName2';
  s2 := 'PanelCaption';
  SendMessage(Application.Handle, WM_ADD_PANEL_MESSAGE, WPARAM(@s1), LPARAM(@s2));
end;


Or, you can use AllocateHWnd():

private
  MyMsgWnd: HWND;
  procedure MyMsgWndProc(var Message: TMessage);
 
procedure Tfrm_Main.FormCreate(Sender: TObject);
begin
  MyMsgWnd := AllocateHWnd(MyMsgWndProc);
end;
 
procedure Tfrm_Main.FormDestroy(Sender: TObject);
begin
  DeallocateHWnd(MyMsgWnd);
end;
 
procedure Tfrm_Main.MyMsgWndProc(var Message: TMessage);
begin
  if Message.Msg = WM_ADD_PANEL_MESSAGE then
    AddPanel(PString(Message.WParam)^, PString(Message.LParam)^)
  else
    Message.Result := DefWindowProc(MyMsgWnd, Message.Msg, Message.WParam, 
Message.LParam);
end;
 
procedure TMainThread.Execute;
var
  s1, s2: string;
begin
  s1 := 'PanelName2';
  s2 := 'PanelCaption';
  SendMessage(frm_Main.MyMsgWnd, WM_ADD_PANEL_MESSAGE, WPARAM(@s1), LPARAM(@s2));
end;


I use Perform to send creating and VCL manipulating code to
application main thread.

No, you use Perform() to call your message handler directly. Perform() does
not go through the OS's inter-thread messaging system at all. That is why
you need to use SendMessage() instead of Perform() when the target HWND is
in a different thread than the sending thread.

The only one difference between TMainThread.Execute and
Tfrm_Main.btn_1Click is the panel name.

That is not the only difference.

Second problem is error ERROR_INVALID_WINDOW_HANDLE during
closing program.

That is because your Form is trying to destroy a child TPanel whose HWND
was created in a worker thread instead of the main UI thread. The only thread
that can destroy an HWND (as well as receive messages for it) is the same
thread that created it.

In my basic application it happends too. Components created inside
thread by SendMessage() or Perform() are invalid if I use them
inside main application thread.

You can't use UI-based components directly in worker threads. Using SendMessage()
to create and manipulate them is fine (as long as you use a thread-safe HWND
to receive the messages). Using Perform() is not thread-safe.

What is wrong?

You are not managing your UI correctly.

--
Remy Lebeau (TeamB)
Grzegorz Żochow...

Posts: 5
Registered: 9/30/16
Re: Adding TPanel to TForm from TThread - problem  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Mar 30, 2017 1:55 AM   in response to: Remy Lebeau (Te... in response to: Remy Lebeau (Te...
Thank you both for answers. Your posts (especially Remy's one) were amazing for me like discovering new world. I suppose you solve other enigmatic, accidental behaviors of my softwares too.
I was using Send- and PostMessage as way to manipulating VCL. I used Perform first time yesterday :) I was in panic.
Now I changed Perform and SendMessage to anonymous procedures and everything works very well.

Thank you

Remy Lebeau (TeamB) wrote:
Grzegorz wrote:

I have a complex problem with creating visual components in therad.

You should not be doing that in the first place. The VL is not thread-safe,
and cannot be used safely from outside of the main UI thread. If a worker
thread needs to touch the UI, it must synchronize with the main UI thread.

First button fires OnAddPanelMessage using OnClick event:

You should be using Native(U)Int instead of Integer, otherwise your code
will truncate the pointer values on 64bit systems. Besides, Perform() is
declared using Native(U)Int parameters anyway (actually it uses WPARAM and
LPARAM to match SendMessage(), and those types are alias for Native(U)Int).

In any case, a better design is to instead wrap the Panel code in its own
method, and then have the button click handler and message handler just call
that method:

private
  procedure AddPanel(const AName, ACaption: string);
 
public
  procedure OnAddPanelMessage(var Msg: TMessage); message WM_ADD_PANEL_MESSAGE;
  ...
 
procedure Tfrm_Main.AddPanel(const AName, ACaption: string);
var
  panel: TPanel;
begin
  panel := TPanel.Create(Self);
  try
    panel.Parent := Self;
    panel.Align := alBottom;
    panel.Name := AName;
    panel.Caption := ACaption;
    panel.Height := 30;
    panel.Visible := True;
  except
    panel.Free;
    raise;
  end;
end;
 
procedure Tfrm_Main.OnAddPanelMessage(var Msg: TMessage);
begin
  AddPanel(PString(Msg.WParam)^, PString(Msg.LParam)^);
end;
 
procedure Tfrm_Main.btn_1Click(Sender: TObject);
begin
  AddPanel('PanelName1', 'PanelCaption');
end;


MainThread := TMainThread.Create;
MainThread.FreeOnTerminate := True;

You should be setting FreeOnTerminate in the thread's constructor instead.
You should not be setting it externally like this, unless you create the
thread suspended first, and then resume it afterwards. A better design is
to not rely on FreeOnTerminate at all, especially since you are not nil'ing
your MainThread variable when the thread terminates.

and thread executes thoose lines:

That code is not thread-safe. You are (indirectly) calling OnAddPanelMessage()
in the context of the worker thread, not in the context of the main UI thread.
You need to use SendMessage() instead:

procedure TMainThread.Execute;
var
  s1, s2: string;
begin
  s1 := 'PanelName2';
  s2 := 'PanelCaption';
  SendMessage(frm_Main.Handle, WM_ADD_PANEL_MESSAGE, WPARAM(@s1), LPARAM(@s2));
end;


However, even that is not thread-safe as-is, either! That is because the
TWinControl.Handle property is not thread-safe. The VCL can destroy and
recreate a TWinControl's HWND at any time for any reason, and reading the
Handle property when no HWND is allocated causes a new HWND to be created
in the context of the calling thread. If that ends up being your worker
thread, you will effectively kill your UI! Your thread is not accounting
for these issues.

The safer approach is to use the TThread.Synchronize() method instead:

procedure TMainThread.AddPanel;
begin
  frm_Main.AddPanel('PanelName2', 'PanelCaption');
end;
 
procedure TMainThread.Execute;
begin
  Synchronize(@AddPanel);
end;


Or, in modern Delphi versions, you can use an anonymous procedure:

procedure TMainThread.Execute;
begin
  Synchronize(
    procedure
    begin
      frm_Main.AddPanel('PanelName2', 'PanelCaption');
    end
  );
end;


The alternative is to send your custom message to a persistent HWND that
is guaranteed to not be destroyed behind your back.

You can use the TApplication window for that:

private
  function AppMessage(var Message: TMessage): Boolean;
 
procedure Tfrm_Main.FormCreate(Sender: TObject);
begin
  Application.HookMainWindow(@AppMessage);
end;
 
procedure Tfrm_Main.FormDestroy(Sender: TObject);
begin
  Application.UnhookMainWindow(@AppMessage);
end;
 
function Tfrm_Main.AppMessage(var Message: TMessage): Boolean;
begin
  if Message.Msg = WM_ADD_PANEL_MESSAGE then
  begin
    AddPanel(PString(Message.WParam)^, PString(Message.LParam)^);
    Result := True;
  end;
end;
 
procedure TMainThread.Execute;
var
  s1, s2: string;
begin
  s1 := 'PanelName2';
  s2 := 'PanelCaption';
  SendMessage(Application.Handle, WM_ADD_PANEL_MESSAGE, WPARAM(@s1), LPARAM(@s2));
end;


Or, you can use AllocateHWnd():

private
  MyMsgWnd: HWND;
  procedure MyMsgWndProc(var Message: TMessage);
 
procedure Tfrm_Main.FormCreate(Sender: TObject);
begin
  MyMsgWnd := AllocateHWnd(MyMsgWndProc);
end;
 
procedure Tfrm_Main.FormDestroy(Sender: TObject);
begin
  DeallocateHWnd(MyMsgWnd);
end;
 
procedure Tfrm_Main.MyMsgWndProc(var Message: TMessage);
begin
  if Message.Msg = WM_ADD_PANEL_MESSAGE then
    AddPanel(PString(Message.WParam)^, PString(Message.LParam)^)
  else
    Message.Result := DefWindowProc(MyMsgWnd, Message.Msg, Message.WParam, 
Message.LParam);
end;
 
procedure TMainThread.Execute;
var
  s1, s2: string;
begin
  s1 := 'PanelName2';
  s2 := 'PanelCaption';
  SendMessage(frm_Main.MyMsgWnd, WM_ADD_PANEL_MESSAGE, WPARAM(@s1), LPARAM(@s2));
end;


I use Perform to send creating and VCL manipulating code to
application main thread.

No, you use Perform() to call your message handler directly. Perform() does
not go through the OS's inter-thread messaging system at all. That is why
you need to use SendMessage() instead of Perform() when the target HWND is
in a different thread than the sending thread.

The only one difference between TMainThread.Execute and
Tfrm_Main.btn_1Click is the panel name.

That is not the only difference.

Second problem is error ERROR_INVALID_WINDOW_HANDLE during
closing program.

That is because your Form is trying to destroy a child TPanel whose HWND
was created in a worker thread instead of the main UI thread. The only thread
that can destroy an HWND (as well as receive messages for it) is the same
thread that created it.

In my basic application it happends too. Components created inside
thread by SendMessage() or Perform() are invalid if I use them
inside main application thread.

You can't use UI-based components directly in worker threads. Using SendMessage()
to create and manipulate them is fine (as long as you use a thread-safe HWND
to receive the messages). Using Perform() is not thread-safe.

What is wrong?

You are not managing your UI correctly.

--
Remy Lebeau (TeamB)
Legend
Helpful Answer (5 pts)
Correct Answer (10 pts)

Server Response from: ETNAJIVE02