0

Was ich versuche, in vereinfachter Form zu erreichen, besteht darin, eine Liste dynamisch erstellter Schaltflächen zu erstellen. Wenn Sie auf einen der Knöpfe klicken, sollte er aus der Liste entfernt werden und sein Objekt sollte freigegeben werden. Mein Ansatz ist:Freigeben von Schaltflächen in einer Liste in OnClick

  • erstellen TList<TButton>
  • Erstellen Sie ein paar TButton Objekte und fügen sie dem TList<TButton>
  • Weisen Sie die Form als Parent für jede der erstellten TButton Objekte
  • Vergeben Sie einen Position Für jedes der erstellten TButton Objekte
  • Weisen Sie eine OnClick Handler-Methode zu jedem der Cre TButton ated Objekte
  • Der OnClick Handler setzt die SenderTButton ‚s Parent-nil und löscht sie aus dem TList<TButton>, so dass ARC das TButton Objekt befreien können, die auf angeklickt wurde.

Wenn ich auf eine der dynamisch erstellten Schaltflächen klicke, bekomme ich einen "Segmentation Fault". Ich vermute, dass es ist, weil ich das TButton Objekt in seinem eigenen OnClick Handler befreie und die Klasse versucht, einige andere Sachen damit nach meinem Handler zu machen.

Ich habe dies auf Android getestet. Ich gehe davon aus, dass das auch auf iOS oder einer anderen ARC-Plattform passieren wird.

Gibt es einen besseren/richtigen Weg, dies zu tun, oder einen anderen Ansatz, dem ich folgen sollte, damit es so funktioniert, wie ich es möchte?

Hier ist ein Beispielcode. Es ist für ein Formular mit einem Design-Time-Button (Button1) darauf. Durch wiederholtes Klicken auf diese Schaltfläche werden neue Schaltflächen dynamisch erstellt und zur Liste hinzugefügt.

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. 

Antwort

3

Wenn ich auf eine der dynamisch erstellten Schaltflächen, erhalte ich eine „Segmentation Fault“. Ich vermute, dass es daran liegt, dass ich das TButton-Objekt in seinem eigenen OnClick-Handler freigebe und die Klasse versucht, nach meinem Handler einige andere Dinge damit zu tun.

Das ist genau das, was passiert. Nachdem der Ereignishandler beendet wurde, benötigt die RTL weiterhin Zugriff auf das Schaltflächenobjekt, um die Verarbeitung von Klicks und Nachrichten zu beenden. Es ist niemals sicher, ein UI-Objekt innerhalb seiner eigenen Ereignisse zu zerstören. Sie müssen also sicherstellen, dass das Objekt während der Ereignisbehandlung am Leben bleibt.

habe ich das auf Android getestet. Ich gehe davon aus, dass das auch auf iOS oder einer anderen ARC-Plattform passieren wird.

Ja.Und es würde passieren, auch auf nicht-ARC-Plattformen, wenn Sie versucht haben, auf die Schaltfläche explizit Free, zB:

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; 

Gibt es einen besseren/richtigen Weg, dies zu tun, oder einen anderen Ansatz, den ich folgendes sollte zu erhalten funktioniert es so, wie ich es will?

könnten Sie haben die OnClick Handler eine asynchrone Nachricht an den Haupt-Thread posten (wie durch TThread.Queue() innerhalb von TThread.CreateAnonymousThread() oder TTask.Run() Aufruf) und dann sofort verlassen, die Nachrichtenhandler auf die Schaltfläche zu einem späteren Zeitpunkt frei zu lassen, wenn die Taste wird nicht mehr verwendet werden, zum Beispiel:

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; 

Oder Sie auf die Schaltfläche Objekt in einer anderen Liste verschieben könnten und dann einen kurzen Timer starten durch diese Liste zu befreien ihre Objekte zu laufen, zum Beispiel:

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. 
+0

Vielen Dank Remy. Ihr erster Ansatz, 'TThread.Queue' in' TThread.CreateAnonymousThread' zu verwenden, funktionierte perfekt und ich bevorzugte diesen Ansatz bei der Verwendung eines 'TTimer'. Ich musste jedoch einen Aufruf von "Start()" hinzufügen, da AnonymousThread in einem Suspended-Zustand erstellt wurde. –

+0

Ich habe eine Frage über Ihre Verwendung von 'DisposedOf()' obwohl. Was ich stattdessen tue, ist, das 'Elternteil' des 'TButton'-Objekts auf' Null 'zu setzen. Wie ich verstehe, dekrementiert dies den "RefCount" in diesem Fall auf 1 und sobald das Objekt dann den Gültigkeitsbereich verlässt, zerstört ARC das Objekt und gibt den Speicher frei. Nach meinem Verständnis zerstört 'DisposedOf()' das Objekt, indem es den Destruktor aufruft, lässt aber immer noch den Speicher frei für ARC, was nur passiert, wenn 'RefCount' Null erreicht. Gibt es einen bestimmten Grund, warum Sie stattdessen "DisposedOf()" gewählt haben? –

+0

@EdreanErnst es macht den Code klarer, was seine Absicht ist. Der Destruktor wird die "Parent" -Referenz trotzdem entfernen und den Refcount dekrementieren. –

Verwandte Themen