Recent

Author Topic: FreeNotification method: I need help  (Read 1697 times)

zoltanleo

  • Sr. Member
  • ****
  • Posts: 486
FreeNotification method: I need help
« on: September 23, 2021, 12:30:49 pm »
Hi folks

I need to track an event in the parent form at the moment when the child form is destroyed. I am using code like this

Code: Pascal  [Select][+][-]
  1. { TfrmMain }
  2.  
  3. procedure TfrmMain.btnCreateChildFormClick(Sender: TObject);
  4. var
  5.   tmpFrm: TfrmChild;
  6. begin
  7.   Inc(FCnt);//this is counter, initialized to zero in the onCreateForm event
  8.   tmpFrm:= TfrmChild.Create(Self);
  9.   tmpFrm.Show;
  10. end;
  11.  
  12. procedure TfrmMain.Notification(AComponent: TComponent; Operation: TOperation);
  13. begin
  14.   inherited Notification(AComponent, Operation);
  15.  
  16.   if (AComponent.InheritsFrom(TfrmChild) and (Operation = opRemove)) then
  17.   ShowMessage(Format('%s has been removed',[TfrmChild(AComponent).Name]));//this procedure is called twice
  18. end;
  19.  
  20. { TfrmChild }
  21.  
  22. procedure TfrmChild.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  23. begin
  24.   CloseAction:= caFree;
  25. end;
  26.  
  27. procedure TfrmChild.FormCreate(Sender: TObject);
  28. const
  29.   offset = 50;
  30. begin
  31.   Self.Name:= 'frmChild_' + IntToStr(frmMain.Cnt);
  32.   Self.Left:= frmMain.Left + frmMain.Cnt * 50 + offset;
  33.   Self.Top:= frmMain.Top + frmMain.Cnt * 50 + + offset;
  34.   Self.FreeNotification(frmMain);
  35. end;
                                     

But I see some incomprehensible side effects:
1 showmessage procedure is called twice
2. if I close the parent form, I see a notification about closing the "phantom" form frmChild_0

What am I doing wrong?

gifs:
https://i.imgur.com/mW3iJAb.mp4
https://i.imgur.com/BNmQZz3.gif
Win10 LTSC x64/Deb 11 amd64(gtk2/qt5)/Darwin Cocoa (Monterey):
Lazarus x32/x64 2.3(trunk); FPC 3.3.1 (trunk), FireBird 3.0.10; IBX by TonyW

Sorry for my bad English, I'm using translator ;)

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 9791
  • Debugger - SynEdit - and more
    • wiki
Re: FreeNotification method: I need help
« Reply #1 on: September 23, 2021, 01:24:14 pm »
TForm is a TComponent.
And TComponent has an owner.

If I create a component with an owner, then the Owner will set a FreeNotification on the component.

Code: Pascal  [Select][+][-]
  1. MyComp.Create(AOwner)
AOwner will do MyComp.FreeNotification(self);

Thus the Owner will know, if one of its owned components is freed.


Your TfrmChild is owned by FrmMain.
So FreeNotification is set up already. You then add a 2nd FreeNotification in your code.

Either create the child as
Code: Pascal  [Select][+][-]
  1.  tmpFrm:= TfrmChild.Create(NIL);

Or do not call child.FreeNotification(main), as you will receive the notification through the owning dependency.

balazsszekely

  • Guest
Re: FreeNotification method: I need help
« Reply #2 on: September 23, 2021, 01:32:53 pm »
I did a quick debug(breakpoint + the call stack window) and I came to the same conclusion as @Martin_fr. Please see attached images.
Using Notification is an overkill in my opinion. A simple TNotifyEvent in the child form will do the job, just subscribe to the event from main form. For more details please check attached application.

zoltanleo

  • Sr. Member
  • ****
  • Posts: 486
Re: FreeNotification method: I need help
« Reply #3 on: September 23, 2021, 02:39:59 pm »
@Martin_fr
@GetMem

Thank a lot for your responces. It's helps me to better understand the LCL inner workings.

@Martin_fr

1. I noticed that the Notification procedure in the main form will be called anyway, even if I do so

Code: Pascal  [Select][+][-]
  1. procedure TfrmChild.FormCreate(Sender: TObject);
  2. const
  3.   offset = 50;
  4. begin
  5.   <skiped>
  6.   //Self.FreeNotification(frmMain);
  7. end;  

Why it happens?

2. If I don't create child forms, then after closing the main form, I still receive a notification about closing the "phantom" child form frmChild_0.

Why it happens?

3. If I create multiple unowned child forms, I have to take care of destroying them myself if I try to close the main form first. Where and how can I do this?

If I do this way

Code: Pascal  [Select][+][-]
  1. procedure TfrmMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  2. var
  3.   i: PtrInt;
  4.   childfrm: TfrmChild = nil;
  5. begin
  6.   for i := 0 to Pred(Screen.FormCount) do
  7.     if Screen.Forms[i].InheritsFrom(TfrmChild) then
  8.       begin
  9.         childfrm:= TfrmChild(Screen.Forms[i]);
  10.         if Assigned(childfrm) then FreeAndNil(childfrm);
  11.       end;

then I get the error
Code: Pascal  [Select][+][-]
  1. Project project1 raised exception class 'EListError' with message:
  2. List index (3) out of bounds
  3.  
  4.  At address 10003F020

@GetMem

A very interesting idea. I changed your code a bit to not see the notification from child forms if the main form is closed (I moved the notification to another event).

Code: Pascal  [Select][+][-]
  1. procedure TfrmChild.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  2. begin
  3.   CloseAction:= caFree;
  4.   if Assigned(FOnNotifyEvent) then
  5.     FOnNotifyEvent(Self, Self.Name + '(' + Self.ClassName + ')');
  6. end;

     
Win10 LTSC x64/Deb 11 amd64(gtk2/qt5)/Darwin Cocoa (Monterey):
Lazarus x32/x64 2.3(trunk); FPC 3.3.1 (trunk), FireBird 3.0.10; IBX by TonyW

Sorry for my bad English, I'm using translator ;)

Remy Lebeau

  • Hero Member
  • *****
  • Posts: 1312
    • Lebeau Software
Re: FreeNotification method: I need help
« Reply #4 on: September 23, 2021, 06:36:26 pm »
1. I noticed that the Notification procedure in the main form will be called anyway, even if I do so

Code: Pascal  [Select][+][-]
  1. procedure TfrmChild.FormCreate(Sender: TObject);
  2. const
  3.   offset = 50;
  4. begin
  5.   <skiped>
  6.   //Self.FreeNotification(frmMain);
  7. end;  

Why it happens?

As @Martin_fr told you earlier, this statement:

Code: Pascal  [Select][+][-]
  1. tmpFrm:= TfrmChild.Create(Self);

Will call FreeNotification() on the frmMain object for you, since the specified Owner is not nil.

2. If I don't create child forms, then after closing the main form, I still receive a notification about closing the "phantom" child form frmChild_0.

Why it happens?

You can't be notified of a child Form that doesn't exist.  So there must be a child Form being created that you don't know about.  Do you have a child Form set to be auto-created at startup?

3. If I create multiple unowned child forms, I have to take care of destroying them myself if I try to close the main form first. Where and how can I do this?

Wherever makes sense.  In the MainForm's OnClose or OnDestroy event would suffice.

Another option would be to simply not create them unowned.  If you don't want to use the MainForm as the Owner, simply pick another Owner.  For instance, the TApplication object would suffice.

If I do this way

Code: Pascal  [Select][+][-]
  1. procedure TfrmMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  2. var
  3.   i: PtrInt;
  4.   childfrm: TfrmChild = nil;
  5. begin
  6.   for i := 0 to Pred(Screen.FormCount) do
  7.     if Screen.Forms[i].InheritsFrom(TfrmChild) then
  8.       begin
  9.         childfrm:= TfrmChild(Screen.Forms[i]);
  10.         if Assigned(childfrm) then FreeAndNil(childfrm);
  11.       end;

then I get the error
Code: Pascal  [Select][+][-]
  1. Project project1 raised exception class 'EListError' with message:
  2. List index (3) out of bounds
  3.  
  4.  At address 10003F020

That is because you are modifying the Screen.Forms list while you are iterating through it, changing the Count and order of the elements in it, so your loop index will not only go out of bounds eventually, but it will also skip elements.  To account for this, you need to either:

1. iterate backwards, not forwards:

Code: Pascal  [Select][+][-]
  1. procedure TfrmMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  2. var
  3.   i: Integer;
  4. begin
  5.   for i := Pred(Screen.FormCount) downto 0 do
  6.     if Screen.Forms[i] is TfrmChild then
  7.       Screen.Forms[i].Free;
  8. end;
  9.  

2. collect the TfrmChild objects into a local list first, don't free them yet.  Then iterate that list afterwards to free them:

Code: Pascal  [Select][+][-]
  1. procedure TfrmMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  2. var
  3.   i: Integer;
  4.   childs: TList;
  5. begin
  6.   childs := TList.Create;
  7.   try
  8.     for i := 0 to Pred(Screen.FormCount) do
  9.     begin
  10.       if Screen.Forms[i] is TfrmChild then
  11.         childs.Add(Screen.Forms[i]);
  12.     end;
  13.     for i := 0 to Pred(childs.Count) do
  14.       TForm(childs[i]).Free;
  15.   finally
  16.     childs.Free;
  17.   end;
  18. end;
  19.  

Alternatively:

Code: Pascal  [Select][+][-]
  1. procedure TfrmMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  2. var
  3.   i: Integer;
  4.   childs: TObjectList;
  5. begin
  6.   childs := TObjectList.Create(True);
  7.   try
  8.     for i := 0 to Pred(Screen.FormCount) do
  9.     begin
  10.       if Screen.Forms[i] is TfrmChild then
  11.         childs.Add(Screen.Forms[i]);
  12.     end;
  13.   finally
  14.     childs.Free; // will Free() each Form in the list for you...
  15.   end;
  16. end;
  17.  

I changed your code a bit to not see the notification from child forms if the main form is closed (I moved the notification to another event).

Code: Pascal  [Select][+][-]
  1. procedure TfrmChild.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  2. begin
  3.   CloseAction:= caFree;
  4.   if Assigned(FOnNotifyEvent) then
  5.     FOnNotifyEvent(Self, Self.Name + '(' + Self.ClassName + ')');
  6. end;

Why not just have the MainForm assign a handler to each child's OnDestroy event instead?  You don't need to create a new event for this.
Remy Lebeau
Lebeau Software - Owner, Developer
Internet Direct (Indy) - Admin, Developer (Support forum)

zoltanleo

  • Sr. Member
  • ****
  • Posts: 486
Re: FreeNotification method: I need help
« Reply #5 on: September 23, 2021, 10:19:51 pm »
Hi Remy

I express my deepest gratitude to you for the very detailed explanations. They were invaluable in helping me analyze my mistakes.

Quote from: Remy Lebeau
Do you have a child Form set to be auto-created at startup?

You are absolutely right. I overlooked this point when I was making a test case for posting.

Quote from: Remy Lebeau
That is because you are modifying the Screen.Forms list while you are iterating through it, changing the Count and order of the elements in it, so your loop index will not only go out of bounds eventually, but it will also skip elements.

Hmmm ... But this fact I didn't know before

In a real application, I use the generic TObjectList<T> types to store a list of open child forms. But I had no idea how to use it that way. Thank U for nice idea.

In the end I decided to use the method suggested by GetMem as the most elegant solution. However, valuable advice from all those who answered will add skill to my experience.

I'm really grateful to you. You are the best

Win10 LTSC x64/Deb 11 amd64(gtk2/qt5)/Darwin Cocoa (Monterey):
Lazarus x32/x64 2.3(trunk); FPC 3.3.1 (trunk), FireBird 3.0.10; IBX by TonyW

Sorry for my bad English, I'm using translator ;)

 

TinyPortal © 2005-2018