Освобождение кнопок в списке в OnClick

В упрощенной форме я пытаюсь создать список динамически создаваемых кнопок. При нажатии на одну из кнопок она должна быть удалена из списка, а ее объект должен быть освобожден. Мой подход:

  • Создать TList<TButton>
  • Создать пару TButton объекты и добавить их в TList<TButton>
  • Назначьте Form как Parent для каждого из созданных TButton объекты
  • Назначить Position для каждого из созданных TButton объекты
  • Назначить OnClick метод-обработчик для каждого из созданных TButton объекты
  • OnClick Обработчик устанавливает SenderTButton"s Parent в nil и удаляет его из TList<TButton>так что ARC может освободить TButton объект, на который нажали.

Когда я нажимаю на одну из динамически созданных кнопок, я получаю "Ошибка сегментации". Я подозреваю, что это потому, что я освобождаю TButton объект в своем собственном OnClick обработчик и класс пытается сделать что-то еще с этим после моего обработчика.

Я проверил это на Android. Я предполагаю, что то же самое произойдет на iOS или любой другой платформе ARC в этом отношении.

Есть ли лучший / правильный способ сделать это или другой подход, которому я должен следовать, чтобы заставить его работать так, как я хочу?

Вот пример кода. Это для формы с одной кнопкой времени разработки (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.

1 ответ

Решение

Когда я нажимаю на одну из динамически созданных кнопок, я получаю "Ошибка сегментации". Я подозреваю, что это потому, что я освобождаю объект TButton в своем собственном обработчике OnClick, и класс пытается сделать с ним некоторые другие вещи после моего обработчика.

Это именно то, что происходит. После выхода из обработчика событий RTL по-прежнему требуется доступ к объекту кнопки, чтобы завершить обработку щелчка и обработку сообщения. Никогда не безопасно уничтожать объект пользовательского интерфейса из его собственных событий. Таким образом, вы должны убедиться, что объект остается живым во время обработки события.

Я проверил это на Android. Я предполагаю, что то же самое произойдет на iOS или любой другой платформе ARC в этом отношении.

Да. И это также произойдет на не-ARC платформах, если вы попытаетесь Free кнопка явно, например:

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;

Есть ли лучший / правильный способ сделать это или другой подход, которому я должен следовать, чтобы заставить его работать так, как я хочу?

Вы могли бы иметь OnClick обработчик отправляет асинхронное сообщение в основной поток (например, путем вызова TThread.Queue()Внутри TThread.CreateAnonymousThread() или же TTask.Run()) и затем немедленно завершите работу, позволяя обработчику сообщений освободить кнопку в более позднее время, когда кнопка больше не используется, например:

procedure TForm2.ButtonClick(Sender: TObject);
var
  btn: TButton
begin
  btn := TButton(Sender);
  ButtonList.Remove(btn);
  TThread.CreateAnonymousThread(
    procedure
    begin
      TThread.Queue(nil,
        procedure
        begin
          btn.DisposeOf;
        end
      );
    end
  ).Start;
end;

Или вы можете переместить объект кнопки в другой список, а затем запустить короткий таймер, чтобы пройти по этому списку, освобождая его объекты, например:

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
    ButtonList : TList<TButton>;
    DisposeList : TList<TButton>;
    procedure ButtonClick(Sender: TObject);
    { Private declarations }
  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;
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.Timer1Timer(Sender: TObject);
var
  btn: TButton;
begin
  Timer1.Enabled := False;
  for btn in DisposeList do
    btn.DisposeOf;
  DisposeList.Clear;
end;

end.
Другие вопросы по тегам