1
votes

Using Delphi 10.2 (under Windows 10 "19H2"), I can create a new app, drop a single panel on it, and an action list with two items. Both items call the same routine whose purpose is to remove any buttons on the panel, and then add the new ones in:

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

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

(I've simplified to just use one button here.) Remove the existing button, add the new button in. If I call DisposeOf (to free up the button's memory), the Window object becomes unresponsive (can't resize, move, close) until I shift focus away from the app.

I've included the entire code below:

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.

Here's the form:

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
2
Please show all code relevant to this, especially declarations.. I have no idea what NvPanelButtons, NavPanel, MakeButton are or do..John Easley
Included those and simplified MakeButton.user3810626
I can use RemoveObject but the memory is still being taken up. I'm trying to dispose of the memory, but any steps I take to do so result in this behavior.user3810626
Please provide a minimal reproducible example that reproduces the issue. You've posted two procedures entirely out of context. A minimal reproducible example would include the full source (including the text content of the .fmx file) and the source .pas file. It should be able to be copied and pasted directly into the IDE and run in order to reproduce the problem you've described.Ken White
OK. Here's the whole thing.user3810626

2 Answers

1
votes

The problem with your code is that you are deleting the button who's action is currently running. When the action returns, the button doesn't exist anymore, on Windows it is freed by DisposeOf() and on mobile platforms it is in a "zombie" state.

The cure is to delay the deletion of buttons until the action has ended. In a standard Windows application I would post a message to myself, to assure the action has ended before I receive the message and can call CreateNavPanelButtons(). But I'm not sure if that would work on all other platforms.

The following should work on any platform.

Add a TTimer, Enabled = False, Interval = 1. Then declare a private field of the form, Action: TAction.

Change any action handlers that changes the NavPanelButtons like this:

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

And add the OnTimer event

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

Update that avoids TTimer

Another solution with no need for messages or timers would be to create all buttons up front and not dispose of them at all during program run.

They could be grouped into TButtonList lists that would hold the buttons that are related and shown simultaneously.

When a TButtonList needs to be shown, the old buttons in the NavPanel would only need to be removed (no B.DisposeOf) from the panel by NavPanel.RemoveObject(B) in a loop.

Finally the new button list would be added to the panel by for b in ButtonList do NavPanel.AddObject(b).

The downside of this is bigger memory usage, in case it matters.

0
votes

Tom has the right answer. I didn't like the user of a timer, because I don't like interrupting the flow of the code unless I have to, so I worked out a two panel system:

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

I put all the buttons on the BackPanel after freeing what's there, then move it to the front/unhide it. (This code's really designed for more than one button, so it's a bit more complicated.)

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;

But this increases the complexity both in terms requiring two panels and positioning them over each other on the form, etc., which is arguably more convoluted than using a timer. So I may just use the Timer solution. I post this just as an alternative.