在 OnClick 中释放列表中的按钮

Freeing buttons in a list in OnClick

我试图以简化的形式实现的是创建一个动态创建的按钮列表。当单击其中一个按钮时,它应该从列表中删除并且它的对象应该被释放。我的方法是:

当我点击其中一个动态创建的按钮时,我得到一个 "Segmentation Fault"。我怀疑这是因为我在它自己的 OnClick 处理程序中释放 TButton 对象,而 class 试图在我的处理程序之后用它做一些其他事情。

我已经在 Android 上测试过了。我假设在 iOS 或任何其他 ARC 平台上也会发生同样的情况。

有没有 better/right 方法可以做到这一点,或者我应该遵循另一种方法来让它按我想要的方式工作?

这是一些示例代码。它适用于带有一个设计时按钮 (Button1) 的表单。重复单击此按钮会动态创建新按钮并将它们添加到列表中。

unit Unit2;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  FMX.Controls.Presentation, FMX.StdCtrls, System.Generics.Collections;

type
  TForm2 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    ButtonList : TList<TButton>;
    procedure ButtonClick(Sender: TObject);
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.fmx}

procedure TForm2.ButtonClick(Sender: TObject);
var
    pos : Integer;
begin
    pos := ButtonList.IndexOf(TButton(Sender));
    TButton(Sender).Parent := nil;
    ButtonList.Delete(pos);
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
    ButtonList := TList<TButton>.Create;
end;

procedure TForm2.Button1Click(Sender: TObject);
var
    pos : Integer;
begin
    pos := ButtonList.Add(TButton.Create(nil));
    ButtonList.Items[pos].Parent := Form2;
    ButtonList.Items[pos].Position.Y := 50 * ButtonList.Count;
    ButtonList.Items[pos].OnClick := ButtonClick;
end;

end.

When I click on one of the dynamically created buttons, I get a "Segmentation Fault". I suspect it is because I am freeing the TButton object in its own OnClick handler and the class is trying to do some other stuff with it after my handler.

这正是正在发生的事情。事件处理程序退出后,RTL 仍需要访问按钮对象以完成处理点击和消息处理。从一个 UI 对象自身的事件中销毁它是不安全的。所以你必须确保对象在事件处理期间保持活动状态。

I have tested this on Android. I assume the same will happen on iOS, or any other ARC platform for that matter.

是的。如果您尝试明确 Free 按钮,它也会在非 ARC 平台上发生,例如:

procedure TForm2.ButtonClick(Sender: TObject);
var
  btn: TButton;
begin
  btn := TButton(Sender);
  ButtonList.Remove(btn);
  {$IFDEF AUTOREFCOUNT}
  btn.Parent := nil;
  {$ELSE}
  btn.Free;
  {$ENDIF}
end;

Is there a better/right way to do this, or another approach I should be following to get it working the way I want?

您可以让 OnClick 处理程序 post 向主线程发送异步消息(例如通过在 10.2 Tokyo 及更高版本中调用 TThread.Queue() inside of TThread.CreateAnonymousThread()/TTask.Run(), or using TThread.ForceQueue()),然后立即退出,让消息处理程序在以后不再使用按钮时释放按钮,例如:

procedure TForm2.ButtonClick(Sender: TObject);
var
  btn: TButton
begin
  btn := TButton(Sender);
  ButtonList.Remove(btn);

  TThread.CreateAnonymousThread(
    procedure
    begin
      TThread.Queue(nil, btn.DisposeOf);
    end
  ).Start;

  { or:
  TThread.ForceQueue(nil, btn.DisposeOf);
  }
end;

或者,您可以将按钮对象移动到另一个列表,然后启动一个短计时器(或使用 TThread.(Force)Queue() 消息)以 运行 通过该列表释放其对象,例如:

unit Unit2;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  FMX.Controls.Presentation, FMX.StdCtrls, System.Generics.Collections;

type
  TForm2 = class(TForm)
    Button1: TButton;
    Timer1: TTimer;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private declarations }
    ButtonList : TList<TButton>;
    DisposeList : TList<TButton>;
    procedure ButtonClick(Sender: TObject);
    procedure DisposeOfButtons;
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.fmx}

procedure TForm2.ButtonClick(Sender: TObject);
var
  btn: TButton;
begin
  btn := TButton(Sender);
  ButtonList.Remove(btn);
  DisposeList.Add(btn);

  Timer1.Enabled := true;

  { or:
  TThread.CreateAnonymousThread(
    procedure
    begin
      TThread.Queue(nil, DisposeOfButtons);
    end
  ).Start;
  }

  { or:
  TThread.ForceQueue(nil, DisposeOfButtons);
  }
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
  ButtonList := TList<TButton>.Create;
  DisposeList := TList<TButton>.Create;
end;

procedure TForm2.Button1Click(Sender: TObject);
var
  btn: TButton;
begin
  btn := TButton.Create(nil);
  ButtonList.Add(btn);
  btn.Parent := Self;
  btn.Position.Y := 50 * ButtonList.Count;
  btn.OnClick := ButtonClick;
end;

procedure TForm2.DisposeOfButtons;
var
  btn: TButton;
begin
  for btn in DisposeList do
    btn.DisposeOf;
  DisposeList.Clear;
end;

procedure TForm2.Timer1Timer(Sender: TObject);
begin
  Timer1.Enabled := False;
  DisposeOfButtons;
end;

end.