Search code examples
delphifiremonkey

Create and free FMX components inside a TPanel at runtime


The below errors happen only when testing under MacOS. Under Windows it seems to work. I'm trying to display a dynamic list of strings into a TPanel, each inside it's own TPanel. The list of strings is kept in a TStringList object and everytime a new one is added or deleted, I'm calling a method to update the display by clearing what exists and then re-build the visual components. The update panel display method looks like this:

procedure TTestsScreen.UpdatePanelDisplay3;
var
  i : Integer;
  myPanel : TPanel;
  myLabel : TLabel;
  myImage : TImage;
begin
  //dispose of existig entities
  for i := (Length(search_entities_labels) - 1) downto 0 do
  begin
    search_entities_labels[i].Free;
    search_entities_labels[i]:= nil;
  end;
  for i := (Length(search_entities_images) - 1) downto 0 do
  begin
    search_entities_images[i].Free;
    search_entities_images[i]:= nil;
  end;
  for i := (Length(search_entities_panels) - 1) downto 0 do
  begin
    search_entities_panels[i].Free;
    search_entities_panels[i]:= nil;
  end;
  //adjust dynamic arrays
  SetLength(search_entities_images,main_search_entities_string_list.Count);
  SetLength(search_entities_labels,main_search_entities_string_list.Count);
  SetLength(search_entities_panels,main_search_entities_string_list.Count);
  //rebuild
  for i := 0 to main_search_entities_string_list.Count-1 do
  begin
    myPanel:= TPanel.Create(Self);
    myPanel.Parent := Panel1;
    myPanel.Height:= 30;
    myPanel.Width:= Panel1.Width;
    myPanel.Position.X:= 0;
    myPanel.Position.Y:= i*30;
    search_entities_panels[i]:= myPanel;
    //
    myLabel:= TLabel.Create(Self);
    myLabel.Parent:= myPanel;
    myLabel.Text:= main_search_entities_string_list[i];
    myLabel.Width:= (3*myPanel.Width) / 4;//display left
    search_entities_labels[i]:= myLabel;
    //
    myImage:= TImage.Create(Self);
    myImage.Parent:= myPanel;
    myImage.Tag:= i;
    myImage.Width:= 15;
    myImage.Height:= 15;
    myImage.Position.X:= myPanel.Width - myImage.Width;//display right
    myImage.MultiResBitmap.Assign(AppResources.delete_white_image.MultiResBitmap);
    myImage.OnClick:= DeleteSearchEntity;
    search_entities_images[i]:= myImage;
  end;
  //
  Panel1.Height:= 30 * main_search_entities_string_list.Count;
end;

I'm adding new elements into the panel like this:

procedure TTestsScreen.AddClick(Sender: TObject);
begin
  main_search_entities_string_list.Add(DateTimeToStr(now));
  UpdatePanelDisplay3;
end;

The adding process works fine, with no errors. The problem is that when I try to delete an element like this

procedure TTestsScreen.DeleteSearchEntity(Sender: TObject);
begin
  main_search_entities_string_list.Delete((Sender as TImage).Tag);
  UpdatePanelDisplay3;
end;

I'm randomly getting

Runtime error 231 

or

Exception EAccessViolation in module TestApp at 00039F33.
Access violation at address 00E7BBBC, accessing address 00000158.
Runtime error   0 at 00039F33 

Inside the FormCreate event I'm doing

procedure TTestsScreen.FormCreate(Sender: TObject);
begin
  SetLength(search_entities_labels,0);
  SetLength(search_entities_images,0);
  SetLength(search_entities_panels,0);
  main_search_entities_string_list:= TStringList.Create;
end;

The arrays are declared as

search_entities_labels : array of TLabel;
search_entities_images : array of TImage;
search_entities_panels : array of TPanel;

Solution

  • You should not be Freeing a component from within an event handler that is being called by the component itself. In this case you are Freeing a TImage from within its OnClick event handler.

    You have to split this operation in two phases.

    Phase 1

    In the event handler you either enqueue a message for the form or you just flag the component set (Image, Label and Panel) as "To Be Deleted". You can do this many ways... by keeping a list of components to be deleted... or of indexes into the array or TObjectList that contains them.

    Phase 2

    In the form, when you have time, you finally Free the unneeded components. You can do this when you receive the enqueued message (if you chose that technique) or you can use Application.OnIdle (if you just flagged them for deletion).

    A further notice... I spoke of enqueued messages, which is IMHO the best solution. Beware that standard FireMonkey messages are not enqueued in an asynchronous way, but are immediately dispatched in a synchronous way. So they cannot solve your problem... you have to use TThread.Queue to snap out of the empasse.