Nasreddine Galfout
Nasreddine Galfout

Reputation: 2591

Ownership principles when using tpagecontrol

Two days ago I gave an accepted answer for this question (that is what bothering me the most):

  newtabsheet:=ttabsheet.Create(PageControl1);
  NewTabSheet.PageControl := PageControl1;
  newtabsheet.Caption:='tab1';
  i1:=tabs.Count;
  tabs.Add(newtabsheet);

I have analysed this code the best I can, and these are my results.

Line 1: I create a ttabsheet with pagecontrol1 as the parent/owner (based on the constructor below).

constructor TTabSheet.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Align := alClient;
  ControlStyle := ControlStyle + [csAcceptsControls, csNoDesignVisible,
    csParentBackground, csPannable];
  Visible := False;
  FTabVisible := True;
  FHighlighted := False;
end;

Then I stored a reference to it in the variable Newtabsheet (this remarque is based on an answer @David Heffernan gave to one of my questions).

Line 2: I used the reference to assign pagecontrol1 to the property .pagecontrol.

And this is the code of the setting method:

procedure TTabSheet.SetPageControl(APageControl: TPageControl);
begin
  if FPageControl <> APageControl then
  begin
    if FPageControl <> nil then
      FPageControl.RemovePage(Self);
    Parent := APageControl;
    if APageControl <> nil then
      APageControl.InsertPage(Self);
  end;
end;

Based on this I think (maybe wrong) it is comparing the existing parent to the new passed parameter, if there is a different then if the parent<>nil removes this newtabsheet from the previous pagecontrol (visual affects) and assign it to the new parent/owner, then if the new parent<>nil insert it in the new pagecontrol(visual affects).

So this line is unnecessary (maybe wrong), because I already did that in the first line.

Line 3: I use the reference to assign the caption of the tabsheet with 'tab1'.

Line 5: I use the add method to store a reference to the tabsheet in the tabs list tabs:TList<ttabsheet>; a generic.

Here is where things start to fly over my head.

I think that the parent/owner is tabs, but the pagecontrol1 is still showing the tab (based on the accepted answer to this question changing the parent visually removes the tabsheet from pagecontrol1), but it does not.

Now this could be wrong, but again if it is just a reference then why when I delete the tabsheet from pagecontrol1 by doing PageControl1.ActivePage.free the tabs.count remains constant.

And if I delete the tabsheet from tabs then the tabsheet on the pagecontrol1 is not deleted (removed visually).

From this question I understood that generics becomes the parent/owner and that you do not need to worry about freeing tabsheet from pagecontrol1, because tabs is the parent and you only need to free it from tabs.

My question: What is happening in this code and why I'm facing this behavior?

Clarification

What is driving the question is when I delete the ttabsheet in the pagecontrol why it is not raising an error when I try to use the reference to it in the list are they two different objects.

Upvotes: 0

Views: 489

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 597215

The Owner is responsible for memory management. Assigning the TPageControl as the Owner of the TTabSheet means the TPageControl will destroy the TTabSheet when the TPageControl is destroyed. The Owner and ownee contain pointers to each other so they can notify each other of important events related to memory management.

The Parent is responsible for window management and visual presentation. Assigning the TPageControl as the Parent of the TTabSheet means the TTabSheet's window is a child of the TPageControl's window, and will be displayed inside the client area of the TPageControl's window. The Parent and child contain pointers to each other so they can notify each other of important events related to window management.

The Owner and Parent are two different things. They can be the same object, but they don't have to be (case in point - components created at design-time are always owned by the TForm, TFrame, or TDataModule that is being designed, but can be children of container components, like TPanel, etc).

The TList is responsible for nothing. It is just a dynamic array of arbitrary values, which in this case happen to be TTabSheet pointers. Nothing more. There is no Owner/Parent relationship between the TList and TTabSheet at all.

When the TTabSheet is destroyed, there are relational links between itself and its TPageControl. The TTabSheet removes itself from its TPageControl's management. It is no longer owned by the TPageControl, and is no longer a child of the TPageControl. The two objects clear their pointers to each other.

There is no relational link between the TTabSheet and the TList at all. The TTabSheet has no concept that the TList even exists. When the TTabSheet is added to and removed from the TList, the TList is simply adding/removing a pointer to the TTabSheet object. The TList does not notify the TTabSheet about the insertion/removal. And there is no pointer from the TTabSheet to the TList, so when the TTabSheet is destroyed, there is no mechanism in place that allows the pointer to that TTabSheet to be removed from the TList. The pointer remains in the list, and you can continue using the pointer (for comparisons, etc) as long as you do not dereference it to access the destroyed TTabSheet.

If you want to remove the TTabSheet pointer from the TList automatically when the TTabSheet is destroyed, you need to call the TTabSheet's FreeNotification() method. Your Notification() callback can then remove the TTabSheet pointer from the TList in response to opRemove notifications, eg:

TMyForm = class(TForm)
  ...
private
  tabs: TList<TTabSheet>;
  ...
protected
  procedure Notification(AComponent: TComponent; Operation: TOperation); override; // <-- ADD THIS
  ...
end;

...  

procedure TMyForm.DoSomething;
var
  NewTabSheet: TTabSheet;
  ...
begin
  ...
  NewTabSheet := TTabSheet.Create(PageControl1);
  NewTabSheet.PageControl := PageControl1;
  NewTabSheet.Caption := 'tab1';
  tabs.Add(NewTabSheet);
  NewTabSheet.FreeNotification(Self); // <-- ADD THIS
  ...
end;

procedure TMyForm.Notification(AComponent: TComponent; Operation: TOperation);
begin
  inherited;
  if (Operation = opRemove) and (AComponent is TTabSheet) then
    tabs.Remove(TTabSheet(AComponent)); // <-- HERE
end;

If you want the TTabSheet to be removed from its TPageControl automatically when its pointer is removed from the TList, switch to TObjectList instead. Its OwnsObjects property is true by default, so it will destroy the TTabSheet when the pointer is removed from the list using the Remove() or Delete() method. The Extract() method will not destroy the TTabSheet, so use that instead of Remove() in your Notification() callback, eg:

TMyForm = class(TForm)
  ...
private
  tabs: TObjectList<TTabSheet>;
  ...
end;

...  

procedure TMyForm.Notification(AComponent: TComponent; Operation: TOperation);
begin
  inherited;
  if (Operation = opRemove) and (AComponent is TTabSheet) then
    tabs.Extract(TTabSheet(AComponent)); // <-- HERE
end;

Upvotes: 4

Related Questions