Is there any way to control the order of which a Delphi form is destroying its components?
I got AVs when the form is destroying because it is destroying a component before the other component which accesses the first component.
Currently, I have no way to avoid AVs except free the first component in finalization section
You will have to call FreeNotification() on the control being referenced and override the protected Notfication() method of your control that is referencing it.
Say you have a property that links to another component (say, a TEdit):
property EditControl: TEdit read FEdit write SetEdit;
Then, if your property is set to link to such a component, you tell it that you want to be notified when it is freed, by calling its FreeNotification() method:
procedure TMyControl.SetEdit(Value: TEdit);
begin
if FEdit <> Value then
begin
if FEdit <> nil then
FEdit.RemoveFreeNotification(Self);
FEdit := Value;
if FEdit <> nil then
FEdit.FreeNotification(Self);
end;
end;
This means that your Notification() procedure will be called when the TEdit is destroyed. You will have to override it:
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
...
procedure TMyControl.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) and (AComponent = FEdit) then
FEdit := nil;
end;
That way, you will know when you can access the TEdit component, and when not to anymore. If FEdit is nil, you should not access it.
Examples taken from:
http://mc-computing.com/Languages/Delphi/ComponentNotification.html
Documentation:
http://docwiki.embarcadero.com/Libraries/Seattle/en/System.Classes.TComponent.FreeNotification
I did some search and some tests and found how to control the Delphi form components destroy order.
There are two ways, at design time and at runtime:
At design time, Edit the form's DFM, move the first component's object block as first block before any other object blocks of any other components that must be freed after the first one.
At runtime, in form OnCreate, Change ComponentIndex property of first component to any value lower than the other components which depend on it, Say 0, this will make it last one to be freed.
Example: FirstComp.ComponentIndex := 0;
That's it!
As you can see, Delphi frees components in the opposite order of which they were added to the form (LIFO), But we can alter this order as I explained.
Thanks everyone for your valuable help. Appreciate it.
Related
I have created and implemented a New Component and inside this created component, there is a procedure InitCombo that needs to be invoked in the implementation.
How will I do that?
Here's the procedure InitCombo inside the New Component:
procedure TNewComponent.InitCombo; //TComboBox ancestor
begin
FStoredItems.OnChange := nil;
StoredItems.Assign(Items);
AutoComplete := False;
FStoredItems.OnChange := StoredItemsChange;
doFilter := True;
StoredItemIndex := -1;
end;
Here's my attempt to invoke but returns an error message:
procedure TfrmMain.FormActivate(Sender: TObject);
begin
TNewComponent.InitCombo;
end;
Error Messages
[dcc32 Error] makeasale_u_v1.pas(84): E2076 This form of method call only allowed for class methods or constructor
Please note that compiling, building, the installation went well and it is working. Except only on how to invoke the procedure inside the component?
Based only on the following portion of the first paragraph of your question
there is a procedure InitCombo that needs to be invoked in the implementation.
It appears you're confused about at least a couple of things about writing components.
First, you initialize the component properties either in its constructor to initialize things, or in an overridden Loaded method, which is called after the component is streamed in from the .dfm file where the component was used. Note that Loaded changes should not touch properties or events that the user can set in the Object Inspector, because doing so will prevent the user's settings from being used.
constructor TNewComponent.Create(AOwner: TComponent);
begin
inherited;
// Do your stuff here
end;
procedure TNewComponent.Loaded;
begin
// Do your stuff here
end;
Second, events that are published (that can be seen in the Events tab of the Object Inspector) belong to the programmer that is using the component, not the component author. Never do anything to those event handlers. Your component should never touch those events except to call them if the end user has assigned a handler. So the code below is absolutely incorrect, because the OnChange event belongs to the user of your component, not your component code:
procedure TNewComponent.InitCombo; //TComboBox ancestor
begin
FStoredItems.OnChange := nil;
...
FStoredItems.OnChange := StoredItemsChange;
end;
If you absolutely must do this, you need to do it properly by saving any event handler the end user has assigned, and restore it afterward:
procedure TNewComponent.InitCombo;
var
OldOnChange: TNotifyEvent;
begin
OldOnChange := Self.OnChange;
// Do your stuff here
Self.OnChange := OldOnChange;
end;
Third, unless you're using a class procedure or class function, you cannot call a method on a class itself (in other words, you cannot use TNewComponent.DoSomething). You call methods or access properties on an instance of the component itself. In your component code, that would be done by using Self, which refers to the current implemented component, as in Self.DoSomething.
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.
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;
We have many forms in our application, and I need a global event handler to detect when one of the forms is being destroyed (and then take some action).
p.s: I want to avoid adding code to each form that will need to send a message to the main form when it's about to destroy. also most of the forms are created and destroyed dynamicaly at run-time.
I was thinking about maybe use a global TApplicationEvents.
What is the best approach for this?
Contrary to David's answer, there is a suitable framework. It's built in higher up in the class hierarchy at TComponent. Sir Rufo is on the right track, but you don't need to force your forms to be owned by this object.
You're welcome to write any number of classes that can take specialised action when a form (or any other component for that matter) is destroyed. E.g.
TDestroyedFormLogger = class(TComponent)
protected
{ Write to log file when forms are destroyed. }
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
end;
TMenuManager = class(TComponent)
protected
{ Remove/hide a menu item corresponding to the form that has been destroyed. }
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
end;
Now whenever you create a form, simply set a notification as follows (assuming you have given yourself access to suitable instances of the above objects):
LForm := TMyForm.Create(Application);
LForm.FreeNotification(DestroyedFormLogger);
LForm.FreeNotification(MenuManager);
This approach is better than using the OnDestroy event because that permits only 1 observer, whereas FreeNotification permits any number of observers.
NOTE: As with any useful technique, don't force-fit a problem to the technique. There may be a more appropriate technique to your specific problem. E.g. The MenuManager idea might be better solved by using the global Screen object to iterate forms OnPopup.
EDIT: Explanation of Observer Pattern
The TComponent notification mechanism is a built-in implementation of the Observer Pattern for when a component is destroyed. FreeNotification (perhaps not ideally named) is the equivalent of registerObserver and RemoveNotification the equivalent of unregisterObserver.
The whole point of the observer pattern is that the subject being observed (sometimes called publisher) has no type-specific knowledge of the objects that are observing it (sometimes called subscribers). Publishers only know that they are able to call a generic notification method on each registered subscriber (observer). This allows objects to be loosely coupled from those that are watching it. In fact the publisher doesn't even need to be observed at all. Obviously the registration method needs to be called either from the subscribers themselves or from a third-party - otherwise the decoupling objective is defeated.
Observers can be implemented at varying degrees of complexity. The simplest being an event or callback. The most complex being a dispatcher that manages registrations in-between and independent of both publishers and subscribers. The dispatcher might even implement thread switching so that publishers don't even get impacted by performance side-effects of slow subscribers.
TComponent's observer implementation has a limitation that both the publisher and subscriber must inherit from TComponent. Basically any component can register with another component to be notified of its destruction.
Perhaps the most common use of this feature in Delphi is: When component A has a reference to component B; If component B is destroyed, component A is notified so that it can set its reference to nil.
What you are wanting is for the framework to fire an event when a form is destroyed. When a form is destroyed, its destructor is run. So, in order for the framework to fire such an event, it would need to be implemented from within the form's destructor. If you take a look inside TCustomForm.Destroy, you will find that there is not such event.
From this we can conclude that there can be no application wide event fired whenever a form is destroyed. This means that you will have to implement a solution yourself. One obvious way to make this happen is to introduce a common base class for all your forms. Ensure that every form in your program derives ultimately from this common base class. Then arrange for the base class to surface an event that is fired whenever an instance is destroyed.
There seems to be some mis-understanding about what I am saying above. Craig demonstrates how to subscribe to notification of a single form's destruction. The ability to do that does not contradict what I am saying. My point is that there is no mechanism in place that allows you to subscribe to receive notification when any form is destroyed.
A constraint on modifying code in existing forms, or creation of forms, as can be seen from other answers and comments, leaves hacks and hooks. A local CBT hook, f.i., would be a little work but probably work fine. Below is one of the simpler hacky solutions.
Screen global object holds a list of forms at all times via a regular TList. TList has a virtual Notify procedure which is called every time an item is added/removed. The idea is to employ a TList derivative that overrides this method and use it in the Screen object.
type
TNotifyList = class(TList)
protected
procedure Notify(Ptr: Pointer; Action: TListNotification); override;
end;
procedure TNotifyList.Notify(Ptr: Pointer; Action: TListNotification);
begin
inherited;
if (Action = lnDeleted) and (csDestroying in TForm(Ptr).ComponentState) and
(TForm(Ptr) <> Application.MainForm) then
// do not use ShowMessage or any 'TForm' based dialog here
MessageBox(0,
PChar(Format('%s [%s]', [TForm(Ptr).ClassName, TForm(Ptr).Name])), '', 0);
end;
Testing for csDestroying is required because the Screen adds/removes forms to its list not only when forms are created/destroyed but also when they are activated etc..
Then make the Screen use this list. This requires an "accessing private fields" hack, as the FForms list is private. You can read about this hack on Hallvard Vassbotn's blog. It also requires "changing the class of an object at run time" hack. You can read about this hack on Hallvard Vassbotn's blog.
type
THackScreenFForms = class
{$IF CompilerVersion = 15}
Filler: array [1..72] of Byte;
{$ELSE}
{$MESSAGE ERROR 'verify/modify field position before compiling'}
{$IFEND}
Forms: TList;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
PPointer(THackScreenFForms(Screen).Forms)^ := TNotifyList;
end;
Note that the notification will be called for every form destruction. This also includes forms created through MessageDlg, ShowMessage etc..
This is not the best practice (have a look at David's answer) but a way to go.
Since every form can have an owner (type TComponent) and this owner gets notified, if a child component is destroyed, just create a global form owner and pass this as the owner of every created form you want to get notified on destroy.
You have to override the TComponent.Notification method and do what you have to (e.g. fire an event)
unit GlobalViewHolder;
interface
uses
Forms,
Classes;
type
TComponentNotificationEvent = procedure( Sender : TObject; AComponent : TComponent; Operation : TOperation ) of object;
TGlobalViewHolder = class( TComponent )
private
FOnNotification : TComponentNotificationEvent;
protected
procedure Notification( AComponent : TComponent; Operation : TOperation ); override;
public
property OnNotification : TComponentNotificationEvent read FOnNotification write FOnNotification;
end;
// small and simple singleton :o)
function ViewHolder : TGlobalViewHolder;
implementation
var
_ViewHolder : TGlobalViewHolder;
function ViewHolder : TGlobalViewHolder;
begin
if not Assigned( _ViewHolder )
then
_ViewHolder := TGlobalViewHolder.Create( Application );
Result := _ViewHolder;
end;
{ TGlobalViewHolder }
procedure TGlobalViewHolder.Notification( AComponent : TComponent; Operation : TOperation );
begin
inherited;
if Assigned( OnNotification )
then
OnNotification( Self, AComponent, Operation );
end;
end.
The main form owner is always Application but there is no need to track this.
Personally I'd prefer David Heffernan's solution since all my forms are allways based on a template and it would be the cleanest and easiest to implement way.
But coming from you demand
p.s: I want to avoid adding code to each form that will need to send a message to the main form when it's about to destroy. also most of the forms are created and destroyed dynamicaly at run-time.
you would be able to patch Destroy to an own method.
I'd take the latest called destructor in the chain and patch TObject.Destroy to TMyClass.Destroy. The place to implement should be the project.
The code for patching is taken from David Heffernan 's answer on Patch routine call in delphi and only included to keep the answer complete, credits regarding this code go there.
program AInformOnCloseForms;
uses
Forms,
Classes,
Windows,
Dialogs,
Unit3 in 'Unit3.pas' {Mainform},
Unit4 in 'Unit4.pas' {Form2};
{$R *.res}
// PatchCode and RedirectProcedure are taken from David Heffernans answer
// https://stackoverflow.com/a/8978266/1699210
// on "Patch routine call in delphi" , credits regarding this code go there
procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
OldProtect: DWORD;
begin
if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
begin
Move(NewCode, Address^, Size);
FlushInstructionCache(GetCurrentProcess, Address, Size);
VirtualProtect(Address, Size, OldProtect, #OldProtect);
end;
end;
type
PInstruction = ^TInstruction;
TInstruction = packed record
Opcode: Byte;
Offset: Integer;
end;
procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
NewCode: TInstruction;
begin
NewCode.Opcode := $E9;//jump relative
NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;
type
TMyClass=Class(TObject) // Dummy to handle "events"
public
Destructor Destroy;override;
End;
destructor TMyClass.Destroy;
begin
// pervent recursion from call to Showmessage
if (Self.InheritsFrom(TCustomForm)) and (Self.ClassName<>'TTaskMessageDialog') then
Showmessage(Self.ClassName);
end;
begin
RedirectProcedure(#TObject.Destroy,#TMyClass.Destroy);
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TMainform, Mainform);
Application.CreateForm(TForm2, Form2);
Application.Run;
end.
As per Vlad's request this expands on my original answer by explaining how to register all forms owned by Application without any changes to the construction of each form. I.e. forms created using TMyForm.Create(Application); and by implication also Application.CreateForm(TMyForm, MyForm);.
The original answer doesn't specify any particular means of registering for FreeNotification because the options vary according to how forms are created. Since the question answered did not put any constraints on how the forms are created, the original answer is more appropriate in the general case.
If we could ensure that Application referred to a custom subclass of TApplication, the problem would be fairly easy to solve by overriding TApplication.Notification;. That's not possible, so this special case leverages the fact that the component ownership framework notifies all owned components when another component is added or removed. So basically all we need is a component tracker also owned by Application and we can react on its "sibling" notifications.
The following test case will demonstrate that new notifications work.
procedure TComponentTrackerTests.TestNewNotifications;
var
LComponentTracker: TComponentTracker;
LInitialFormCount: Integer;
LForm: TObject;
begin
LComponentTracker := TComponentTracker.Create(Application);
try
LComponentTracker.OnComponentNotification := CountOwnedForms;
LInitialFormCount := FOwnedFormCount;
LForm := TForm.Create(Application);
CheckEquals(LInitialFormCount + 1, FOwnedFormCount, 'Form added');
LForm.Free;
CheckEquals(LInitialFormCount, FOwnedFormCount, 'Form removed');
finally
LComponentTracker.Free;
end;
end;
procedure TComponentTrackerTests.CountOwnedForms(AComponent: TComponent; AOperation: TOperation);
begin
if (AComponent is TCustomForm) then
begin
case AOperation of
opInsert: Inc(FOwnedFormCount);
opRemove: Dec(FOwnedFormCount);
end;
end;
end;
TComponentTracker is implemented as follows:
TComponentNotificationEvent = procedure (AComponent: TComponent; AOperation: TOperation) of object;
TComponentTracker = class(TComponent)
private
FOnComponentNotification: TComponentNotificationEvent;
procedure SetOnComponentNotification(const Value: TComponentNotificationEvent);
procedure DoComponentNotification(AComponent: TComponent; AOperation: TOperation);
protected
procedure Notification(AComponent: TComponent; AOperation: TOperation); override;
public
property OnComponentNotification: TComponentNotificationEvent read FOnComponentNotification write SetOnComponentNotification;
end;
procedure TComponentTracker.DoComponentNotification(AComponent: TComponent; AOperation: TOperation);
begin
if Assigned(FOnComponentNotification) then
begin
FOnComponentNotification(AComponent, AOperation);
end;
end;
procedure TComponentTracker.Notification(AComponent: TComponent; AOperation: TOperation);
begin
inherited Notification(AComponent, AOperation);
DoComponentNotification(AComponent, AOperation);
end;
procedure TComponentTracker.SetOnComponentNotification(const Value: TComponentNotificationEvent);
var
LComponent: TComponent;
begin
FOnComponentNotification := Value;
if Assigned(Value) then
begin
{ Report all currently owned components }
for LComponent in Owner do
begin
DoComponentNotification(LComponent, opInsert);
end;
end;
end;
WARNING
You could implement anything you choose in the OnComponentNotification event handler. This would include logging that the form is "destroyed". However, such a simplistic approach would actually be flawed because TComponent.InsertComponent allows a component's owner to be changed without destroying it.
Therefore to accurately report destruction, you would have to combine this with using FreeNotification as in my first answer.
This is quite easily done by setting LComponentTracker.OnComponentNotification := FDestructionLogger.RegisterFreeNotification; where RegisterFreeNotification is implemented as follows:
procedure TDestructionLogger.RegisterFreeNotification(AComponent: TComponent; AOperation: TOperation);
begin
if (AComponent is TCustomForm) then
begin
case AOperation of
opInsert: AComponent.FreeNotification(Self);
end;
end;
end;
A very simple approach could be keeping track of the Form count. When it lowers, then there is a Form destroyed. Check in Application.OnIdle:
procedure TMainForm.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
begin
if Screen.CustomFormCount < FFormCount then
FormDestroyed;
if FFormCount <> Screen.CustomFormCount then
FFormCount := Screen.CustomFormCount;
end;
Depending on what action should be taken, you can loop through Screen.CustomForms to determine which Form was destroyed.
I have a form containing a TFrame. The TFrame contains a ComboBox that is dynamically populated. Each ComboBox entry has an associated object. By the time the overridden destructor for the TFrame is called, the Items in the ComboBox have already been cleared without freeing their associated objects. This happens whether I drop the ComboBox on the form in designer view, or dynamically create it in code with either nil or the TFrame as its owner. I currently use the OnDestroy event of the containing TForm to call a clean-up procedure of the contained TFrame.
Is there a better way that would not need an explicit procedure call by the TFrame's container? Where ideally should the objects added dynamically to the ComboBox be freed?
You say that when the destructor for the TFrame is called, the Items of the ComboBox have already been cleared. That's not the case, ComboBox items are never cleared. When Items is destroyed by the ComboBox, they've got a count of only 0.
When you exit your application and the VCL destroys the form containing the frame and the ComboBox, the native ComboBox control is also destroyed by the OS since it is placed in a window being destroyed. When you later access the items to be able to free your objects in the frame destructor, the VCL have to recreate a native ComboBox control, having an item count of 0.
The solution I'd propose is easy. Don't leave freeing your frame to the framework, instead, destroy your frame in the OnDestroy event of your form. That would be before the underlying window of the form is destroyed, hence you'll be able to access your objects.
form unit
procedure TMyForm.FormDestroy(Sender: TObject);
begin
MyFrame.Free;
end;
frame unit
destructor TMyFrame.Destroy;
var
i: Integer;
begin
for i := 0 to ComboBox1.Items.Count - 1 do
ComboBox1.Items.Objects[i].Free;
inherited;
end;
You could utilize the TFrame's WM_DESTROY handler like this:
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Classes, Controls, Forms, StdCtrls;
type
TFrame1 = class(TFrame)
ComboBox1: TComboBox;
private
procedure WMDestroy(var Msg: TWMDestroy); message WM_DESTROY;
procedure FreeComboBoxItems;
public
constructor Create(AOwner: TComponent); override;
end;
implementation
{$R *.dfm}
constructor TFrame1.Create(AOwner: TComponent);
begin
inherited;
// Add some object items to the ComboBox
ComboBox1.AddItem('a', TButton.Create(nil));
ComboBox1.AddItem('b', TMemoryStream.Create);
ComboBox1.AddItem('c', TList.Create);
end;
procedure TFrame1.WMDestroy(var Msg: TWMDestroy);
begin
// Make sure the TFrame is actually destroying - not recreated
if (csDestroying in ComponentState) then
FreeComboBoxItems;
inherited;
end;
procedure TFrame1.FreeComboBoxItems;
var
I: Integer;
begin
OutputDebugString('TFrame1.FreeComboBoxItems');
with Self.ComboBox1 do
for I := 0 to Items.Count - 1 do
begin
OutputDebugString(PChar(Items.Objects[I].ClassName + '.Free'));
Items.Objects[I].Free;
end;
end;
end.
Another option is to create a Base ancestor TAppBaseForm class and a TAppBaseFrame for the entire application, and derive all your Forms as TAppBaseForm and all Frames as TAppBaseFrame.
This way the TAppBaseForm could notify all it's child TAppBaseFrame that the owner Form is destroyed on TAppBaseForm.FormDestroy event handler. At that point the ComboBox items are still valid (as described by Sertac Akyuz's answer).
Your question isn't really usefull, because - generally speaking - it is discouraged to store data (or objects in your case) in a GUI control. See also David's comment on how to change your design.
What makes the question kind of interresting to answer though is the difference between the combo box being a child of a form directly and being a child of another child of the form (your frame in this case). Apparently, the combo box items are destroyed before the destructor of that frame is called. Obvious alternatives to explore are then: overriding Frame.BeforeDestruction, overriding Frame.DestroyWindowHandle, overriding Frame.DestroyWnd, or catching WM_DESTROY in an overridden Frame.WndProc, but none of them is called before the items are already gone.
The next thing to try is to repeat this for the combo box. It turns out that when WM_DESTROY arrives at the combo box that the items are still there. However, beware of catching that message ónly when the control really is being destroyed, because the VCL might recreate a combo box frequently. Implement it using an interposing class for TComboBox, as follows:
unit Unit2;
interface
uses
Windows, Messages, Classes, Controls, Forms, StdCtrls;
type
TComboBox = class(StdCtrls.TComboBox)
protected
procedure WndProc(var Message: TMessage); override;
end;
TFrame1 = class(TFrame)
ComboBox1: TComboBox;
end;
implementation
{$R *.dfm}
{ TComboBox }
procedure TComboBox.WndProc(var Message: TMessage);
var
I: Integer;
begin
if (Message.Msg = WM_DESTROY) and (csDestroying in ComponentState) then
for I := 0 to Items.Count - 1 do
Items.Objects[I].Free;
inherited WndProc(Message);
end;
end.
Now, to answer your question: "Is this a better way?"
Yes it is, because it offers assurance of the object's destruction at the frame's level. In other words: you don't have to remember to deal with this for every instance seperately.
And no it is not, because this solution requires that the objects in the combo box are allowed to be freed in whatever circumstance which restricts usage to an unnecessary extra boundary.
So, is this answer usefull? Well, if it prevents you from using your current approach, then it is.
Besides, I also found another alternative by setting the frame's Parent property to nil in the containing form OnDestroy handler:
procedure TForm2.FormDestroy(Sender: TObject);
begin
Frame1.Parent := nil;
end;
In this case, you can safely destroy the objects stored in the combo box within the frame's destructor. But this solution is even worse than your current one, because it is not descriptive. Then Frame1.FreeComboObjects is much better.
freeing Combobox.Items.Objects in destructor is too late.
so, according to previous answers it is better and safe to do that this way:
procedure TMyFrame.ClearCBObjects;
var
i: Integer;
begin
for i := 0 to ComboBox1.Items.Count - 1 do
ComboBox1.Items.Objects[i].Free;
end;
destructor TMyFrame.Destroy;
begin
//Free none Component objects
inherited;
end;
destructor TMyForm.Destroy;
begin
MyFrame.ClearCBObjects;
inherited;
end;
This will sound against the nature of MDI.. I need to show a MDI form (FormStyle=fsMdiChild) as modal sometimes. And I also need to access the part between Application.CreateForm and OnShow event of another MDI form, i.e.
Application.CreateForm(Form2,TForm2); // but don't set form2's visible property true.
Form2.caption:='not working example';
Form2.SomeMagicToSetVisibleTrue;
Any ideas?
For your first problem: Add another constructor, for example CreateAsMDI, like this:
constructor TModalAndMDIForm.CreateAsMDI(AOwner: TComponent);
begin
f_blChild := true;
GlobalNameSpace.BeginWrite;
try
inherited CreateNew(AOwner);
if(not(csDesigning in ComponentState)) then begin
Include(FFormState, fsCreating);
try
FormStyle := fsMDIChild;
if(not(InitInheritedComponent(self, TForm))) then
raise Exception.CreateFmt('Can't create %s as MDI child', [ClassName]);
finally
Exclude(FFormState, fsCreating);
end;
end;
finally
GlobalNameSpace.EndWrite;
end;
end;
In the normal constructor just set the variable f_blChild to false and call the inherited create.
You need two more things, rather self explaining:
procedure TModalAndMDIForm.Loaded;
begin
inherited;
if(f_blChild) then
Position := poDefault
else begin
Position := poOwnerFormCenter;
BorderStyle := bsDialog;
end;
end;
//-----------------------------------------------------------------------------
procedure TModalAndMDIForm.DoClose(var Action: TCloseAction);
begin
if(f_blChild) then
Action := caFree;
inherited DoClose(Action);
end;
Now you can call the form modal, if created with the standard constructor, and as MDI child, if created with CreateAsMDI.
If you include this in your form's declaration
property IsChild: boolean read f_blChild;
you can even do things depending on whether the form is an MDI child or not, just interrogating the isChild property.
As for your second problem: do not use Application.CreateForm, but create your form yourself:
Here the two creations for modal and MDI:
//Modal
frmDialog := TMyForm.Create(self);
// Your Code
frmDialog.ShowModal;
frmDialog.Release;
//MDI-Child
frmDialog := TMyForm.CreateChild(self);
// Your code
frmDialog.Show;
I have translated this answer form an article on the site DelphiPraxis.
The simplest method is to create a trivial subclass of the form, and set
FormStyle = fsMDIChild
AND
Form.Visible = False
in the property inspector. This is tried and tested!
At least for Delphi 2007 and 2009 creating the MDI child form invisible is easy. For the early Delphi versions (where it was impossible to set Visible to False in the property inspector) you just have to provide a handler for the OnCreate event and access a protected field of the class:
procedure TMDIChild.FormCreate(Sender: TObject);
begin
FFormState := FFormState - [fsVisible];
end;
This will disable the automatic showing of the MDI child. After you are done with your other initialisations you can simply Show it or set Visible to True.
I will not try to answer your question about modal MDI child forms, as this violates the conventions of the Windows platform.
No answers above actually does the job required. The best answer is wrong as well, because of following:
Opening the first form;
Maximize it;
Now say this form calls another form (mdi);
When it is constructed the same way you will get buggy layout... (it will mdi child, but not maximized, however the first will be still maximized). So a buggy state.
If you really need to decide in run-time whether it's a fsMDIChild or fsNormal you need to apply following approach.
You have form which is stored as fsNormal in design (or vice-versa);
Override the InitializeNewForm method;
Set the value of FFormStyle to fsMDIChild (as shown below).
...
TYourForm = class(TForm)
...
protected
procedure InitializeNewForm; override;
...
procedure TYourForm.InitializeNewForm;
var
FS: ^TFormStyle;
begin
inherited;
if DoYourCheckForMDI then
begin
FS := #FormStyle;
FS^ := fsMDIChild;
end;
end;