从 TPanel 中删除按钮,Firemonkey 版

Deleting Buttons from TPanel, Firemonkey Edition

使用 Delphi 10.2(在 Windows 10 "19H2" 下),我可以创建一个新的应用程序,在上面放置一个面板,以及一个包含两个项目的操作列表。这两个项目调用相同的例程,其目的是删除面板上的任何按钮,然后添加新按钮:

procedure TForm1.CreateNavPanelButtons(Action: TAction);
begin
  NavPanel.RemoveObject(Btn);
  Btn.DisposeOf; //problem line

  Btn := MakeButton(Action);
  NavPanel.AddObject(Btn);
end;

(我在这里简化为只使用一个按钮。)删除现有按钮,添加新按钮。如果我调用 DisposeOf(以释放按钮的内存),Window 对象变为无响应(无法调整大小、移动、关闭),直到我将注意力从应用程序移开。

我在下面包含了完整的代码:

unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    NavPanel: TPanel;
    ActionList: TActionList;
    acNextMenu: TAction;
    acBackToMainMenu: TAction;
    procedure FormCreate(Sender: TObject);
    procedure acNextMenuExecute(Sender: TObject);
  private
    { Private declarations }
  public
    Btn: TButton;
    procedure CreateNavPanelButtons(Action: TAction);
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

function MakeButton(A: TAction): TButton;
begin
  Result := TButton.Create(nil);
  Result.Action := A;
  Result.Text := (A as TAction).Text;
end;

procedure TForm1.acNextMenuExecute(Sender: TObject);
begin
  CreateNavPanelButtons(acBackToMainMenu);
end;

procedure TForm1.CreateNavPanelButtons(Action: TAction);
begin
  NavPanel.RemoveObject(Btn);
  Btn.DisposeOf;

  Btn := MakeButton(Action);
  NavPanel.AddObject(Btn);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  CreateNavPanelButtons(acNextMenu);
end;

end.

表格如下:

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 480
  ClientWidth = 640
  FormFactor.Width = 320
  FormFactor.Height = 480
  FormFactor.Devices = [Desktop]
  OnCreate = FormCreate
  DesignerMasterStyle = 0
  object NavPanel: TPanel
    Align = Top
    Size.Width = 640.000000000000000000
    Size.Height = 73.000000000000000000
    Size.PlatformDefault = False
    TabOrder = 0
  end
  object ActionList: TActionList
    Left = 392
    Top = 192
    object acNextMenu: TAction
      Category = 'Navigation'
      Text = 'NextMenu'
      OnExecute = acNextMenuExecute
    end
    object acBackToMainMenu: TAction
      Category = 'Navigation'
      Text = 'Back To &Main Menu'
      OnExecute = FormCreate
    end
  end
end

您的代码存在问题,您正在删除当前正在执行操作的按钮 运行ning。当操作 returns 时,按钮不再存在,在 Windows 上它被 DisposeOf() 释放,在移动平台上它处于 "zombie" 状态。

解决方法是延迟删除按钮,直到操作结束。在标准 Windows 应用程序中,我会 post 给自己发送一条消息,以确保在我收到消息并可以调用 CreateNavPanelButtons() 之前操作已经结束。但我不确定这是否适用于所有其他平台。

以下应该适用于任何平台。

添加一个TTimerEnabled = FalseInterval = 1。然后声明一个私有字段,格式为 Action: TAction.

像这样更改任何更改 NavPanelButtons 的操作处理程序:

procedure TForm2.acNextMenuExecute(Sender: TObject);
begin
//  CreateNavPanelButtons(acBackToMainMenu);
  Action := acBackToMainMenu;
  Timer1.Enabled := True;
end;

并添加 OnTimer 事件

procedure TForm2.Timer1Timer(Sender: TObject);
begin
  Timer1.Enabled := False;
  if Action <> nil then
     CreateNavPanelButtons(Action);
end;

避免TTimer

的更新

另一种不需要消息或计时器的解决方案是预先创建所有按钮,并且在程序 运行 期间根本不处理它们 运行。

它们可以分组到 TButtonList 列表中,这些列表将包含相关的按钮并同时显示。

TButtonList 需要显示时,NavPanel 中的旧按钮只需要通过 NavPanel.RemoveObject(B) 从面板中删除(不需要 B.DisposeOf)在循环中。

最后,for b in ButtonList do NavPanel.AddObject(b) 将新的按钮列表添加到面板中。

这样做的缺点是内存使用量更大,以防万一。

汤姆答对了。我不喜欢定时器的用户,因为我不喜欢中断代码流,除非我不得不这样做,所以我设计了一个双面板系统:

  TForm1 = class(TForm)
    NavPanel1: TPanel;
    NavPanel2: TPanel;
. . .
    FrontPanel: TPanel;
    BackPanel: TPanel;

我在释放那里的东西后把所有的按钮都放在了 BackPanel 上,然后把它移到 front/unhide 上。 (这段代码确实是为多个按钮设计的,所以有点复杂。)

procedure TForm1.CreateNavPanelButtons(Action: TAction);
  procedure Swap;
  var P: TPanel;
  begin
    P := BackPanel;
    BackPanel := FrontPanel;
    FrontPanel := P;
    BackPanel.Visible := false;
    FrontPanel.Visible := true;
  end;
var P: TPanel;
    B: TButton;
    I: Integer;
begin
   for I := BackPanel.ChildrenCount-1 downto 0 do
   if BackPanel.Children[I] is TButton then
   begin
      B := BackPanel.Children[I] as TButton;
      BackPanel.RemoveObject(B);
      B.DisposeOf;
   end;
   BackPanel.AddObject(MakeButton(Action));
   Swap;
end;

但这增加了复杂性,无论是在需要两个面板还是在表单上将它们彼此重叠等方面,这可以说比使用计时器更复杂。所以我可能只使用 Timer 解决方案。我post这只是一个替代方案。