'Self deleting button

I have a TScrollBox with a bunch of TPanels with some TButtons generated at runtime. I need to delete the TPanel when one TButton is clicked but doing that in OnClick end in an access violation...

procedure TMainForm.ButanClick(Sender: TObject);
var
  vParentPanel: TPanel;
begin
  if (string(TButton(Sender).Name).StartsWith('L')) then
  begin
    TButton(Sender).Caption := 'YARE YARE DAZE';
  end
  else
  begin
    vParentPanel := TPanel(TButton(Sender).GetParentComponent());
    TheScrollBox.RemoveComponent(vParentPanel);
    vParentPanel.Destroy();
    // access violation but the panel is removed
  end;
end;

procedure TMainForm.Button3Click(Sender: TObject);
var
  i: Integer;
  vPanel: TPanel;
  vButton: TButton;
begin
  for i := 0 to 20 do
  begin
    vPanel := TPanel.Create(TheScrollBox);
    vPanel.Align := alTop;
    vPanel.Parent := TheScrollBox;

    vButton := TButton.Create(vPanel);
    vButton.Align := alLeft;
    vButton.Parent := vPanel;
    vButton.Name := 'L_butan' + IntToStr(i);
    vButton.OnClick := ButanClick;

    vButton := TButton.Create(vPanel);
    vButton.Align := alRight;
    vButton.Parent := vPanel;
    vButton.Name := 'R_butan' + IntToStr(i);
    vButton.OnClick := ButanClick;
  end;
end;


Solution 1:[1]

You cannot safely destroy the parent TPanel (or the TButton itself) from inside the TButton's OnClick event. The VCL still needs access to the TPanel/TButton for a beat after the event handler exits. So, you need to delay the destruction until after the handler exits. The easiest way to do that is to use TThread.ForceQueue() to call TObject.Free() on the TPanel, eg:

procedure TMainForm.ButanClick(Sender: TObject);
var
  vButton: TButton;
begin
  vButton := TButton(Sender);
  if vButton.Name.StartsWith('L') then
  begin
    vButton.Caption := 'YARE YARE DAZE';
  end
  else
  begin
    TThread.ForceQueue(nil, vButton.Parent.Free);
  end;
end;

The TPanel will remove itself from the TScrollBox during its destruction. You do not need to handle that step manually.

Solution 2:[2]

Solved with Renate Schaaf answer:

...
const
WM_REMOVEPANEL = WM_USER + 9001;
procedure ButanClick(Sender: TObject);
procedure OnCustomMessage(var Msg: TMessage); message WM_REMOVEPANEL;

...
procedure TMainForm.ButanClick(Sender: TObject);
var
  vParentPanel: TPanel;
begin
  if (string(TButton(Sender).Name).StartsWith('L')) then
  begin
    TButton(Sender).Caption := 'YARE YARE DAZE';
  end
  else
  begin
    // SendMessage = access violation again because it wait the return
    // while PostMessage return istantly
    PostMessage(Handle, WM_REMOVEPANEL, 0, THandle(@Sender));
  end;
end;

procedure TMainForm.OnCustomMessage(var Msg: TMessage);
var
  vButton: TButton;
begin
  if (Msg.Msg = WM_REMOVEPANEL) then
  begin
    vButton := TButton(Pointer(Msg.LParam)^);
    ShowMessage(vButton.Name);
    TheScrollBox.RemoveComponent(vButton.GetParentComponent());
    TPanel(vButton.GetParentComponent()).Destroy();
    Msg.Result := 1;
  end
  else
    Msg.Result := 0;
end;

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 k0tt