In some legacy D7 code I inherited and am migrating to XE5, I found the code below.
The comment states it's tricking windows into thinking it's a child form if it's created from a non-WinControl. There is one place in the code base where Create is called with AOwner as nil. (A form is available at the time of that call, so not sure why they did that...)
Any suggestions as to what the programmer's goal was?
private
FParentWinControl: TWinControl; {Don't mess with! Used in CreateParams}
procedure TFormX.CreateParams(var params: TCreateParams); override;
public
constructor TFormX.Create( AOwner: TComponent); reintroduce;
end;
constructor TFormX.Create( AOwner: TComponent);
begin
if AOwner IS TWinControl then
FParentWinControl := TWinControl(AOwner)
else
FParentWinControl := NIL;
inherited Create(AOwner);
end; { Create }
procedure TFormX.CreateParams(var params: TCreateParams);
begin
inherited CreateParams(params);
if (NOT fCreateParamsHasBeenRun) then
begin
fCreateParamsHasBeenRun := TRUE;
if Assigned(FParentWinControl) then
Params.WndParent := FParentWinControl.Handle; {tricks windows into thinking it's a child form}
end;
end;
This code predates and loosely mimics the PopupMode and PopupParent properties that were added to TCustomForm in Delphi 8. Assuming AOwner is another Form, use those properties in modern Delphi versions, eg:
constructor TFormX.Create( AOwner: TComponent);
begin
inherited Create(AOwner);
if AOwner Is TCustomForm then
PopupParent := TCustomForm(AOwner);
end;
Also, the use of fCreateParamsHasBeenRun was wrong. CreateParams() is called every time the Form's window is (re)created, so the WndParent needed to be applied every time, not conditionally. If you need to keep the CreateParams() logic (such as if AOwner is a non-TCustomForm windowed control), you need to remove fCreateParamsHasBeenRun.
Related
I want to create my own custom control. Let's say I want to initialize its graphic properties. Obviously I cannot do that in Create because a canvas/handle is not YET allocated.
The same if my custom control contains a subcomponent (and I also set its visual properties).
There are quite several places on SO that discuss the creation of a custom control. They don't really agree on it.
AfterConstruction is out of question because the handle is not ready yet.
CreateWnd seem ok but it actually can be quite problematic as it can be called more than once (for example when you apply a new skin to the program). Probably, some boolean variable should be used to check if CreateWnd was called more than once.
SetParent has the same issue: if you change the parent of your custom control, whatever code you put in its SetParent will be executed again. A bool variable should fix the problem.
Principles
First al all, most of the visual properties of a control do not require the control to have a valid window handle in order to be set. It is a false assumption that they do.
Once the object that constitutes a control is created, i.e. the constructor has been executed, normally all (visual) properties like size, position, font, color, alignment, etc. can be set. Or they should be able to, preferably. For sub controls, also the Parent ideally must be set as soon as the constructor has run. For the component itself, that constructor would be the inherited constructor during its own constructor.
The reason this works is that all these kind of properties are stored within the fields of the Delphi object itself: they are not immediately passed to the Windows API. That happens in CreateWnd but no sooner than when all necessary parent window handles are resolved and assigned.
So the short answer is: the initial setup of a custom component is done in its constructor, because it is the only routine that runs once.
But the question (unintentionally) touches a wide range of topics on component building, because the complexity of an initial setup of a control depends entirely on the type of control and the properties that are to be set.
Example
Consider writing this (useless yet illustrative) component that consists of a panel with a combo box aligned on top of it. The panel should initially have: no caption, a custom height and a silver background. The combo box should have: a custom font size and a 'picklist' style.
type
TMyPanel = class(TPanel)
private
FComboBox: TComboBox;
public
constructor Create(AOwner: TComponent); override;
end;
constructor TMyPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Color := clSilver;
ShowCaption := False;
Height := 100;
FComboBox := TComboBox.Create(Self);
FComboBox.Parent := Self;
FComboBox.Align := alTop;
FComboBox.Style := csDropDownList;
FComboBox.Font.Size := 12;
end;
Framework conformity
A component writer could now consider it done, but it is not. He/she has the responsibility to write components properly as described by the comprehensive Delphi Component Writer's Guide.
Note that no less then four properties (indicated bold in the object inspector) are needlessly stored in the DFM because of an incorrect designtime component definition. Although invisible, the caption property still reads MyPanel1, which is against te requirements. This can be solved by removing the applicable control style. The ShowCaption, Color and ParentBackground properties lack a proper default property value.
Note too that all default properties of TPanel are present, but you might want some not te be, especially the ShowCaption property. This can be prevented by descending from the right class type. The standard controls in the Delphi framework mostly offer a custom variant, e.g. TCustomEdit instead of TEdit that are there for exactly this reason.
Our example compound control that is rid of these issues looks as follows:
type
TMyPanel = class(TCustomPanel)
private
FComboBox: TComboBox;
public
constructor Create(AOwner: TComponent); override;
published
property Color default clSilver;
property ParentBackground default False;
end;
constructor TMyPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Color := clSilver;
ControlStyle := ControlStyle - [csSetCaption];
Height := 100;
FComboBox := TComboBox.Create(Self);
FComboBox.Parent := Self;
FComboBox.Align := alTop;
FComboBox.Style := csDropDownList;
FComboBox.Font.Size := 12;
end;
Of course, other implications due to setting up a component are possible.
Exceptions
Unfortunately there áre properties that require a control's valid window handle, because the control stores its value in Windows' native control. Take the Items property of the combo box above for example. Consider a deisgn time requirement of it been filled with some predefined text items. You then should need to override CreateWnd and add the text items the first time that it is called.
Sometimes the initial setup of a control depends on other controls. At design time you don't (want to) have control over the order in which all controls are read. In such case, you need to override Loaded. Consider a design time requirement of adding all menu-items from the PopupMenu property, if any, to the Items property of the combo box.
The example above, extended with these new features, results finally in:
type
TMyPanel = class(TCustomPanel)
private
FInitialized: Boolean;
FComboBox: TComboBox;
procedure Initialize;
protected
procedure CreateWnd; override;
procedure Loaded; override;
public
constructor Create(AOwner: TComponent); override;
published
property Color default clSilver;
property ParentBackground default False;
property PopupMenu;
end;
constructor TMyPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Color := clSilver;
ControlStyle := ControlStyle - [csSetCaption];
Height := 100;
FComboBox := TComboBox.Create(Self);
FComboBox.Parent := Self;
FComboBox.Align := alTop;
FComboBox.Style := csDropDownList;
FComboBox.Font.Size := 12;
end;
procedure TMyPanel.CreateWnd;
begin
inherited CreateWnd;
if not FInitialized then
Initialize;
end;
procedure TMyPanel.Initialize;
var
I: Integer;
begin
if HandleAllocated then
begin
if Assigned(PopupMenu) then
for I := 0 to PopupMenu.Items.Count - 1 do
FComboBox.Items.Add(PopupMenu.Items[I].Caption)
else
FComboBox.Items.Add('Test');
FInitialized := True;
end;
end;
procedure TMyPanel.Loaded;
begin
inherited Loaded;
Initialize;
end;
It is also possible that the component depends in some way on its parent. Then override SetParent, but also remember that any dependency on (properties of) its parent likely indicates a design issue which might require re-evaluation.
And surely there are other kind of dependencies imaginable. They then would require special handling somewhere else in the component code. Or another question here on SO. 😉
So, I did this test that shows the creation order.
UNIT cvTester;
{--------------------------------------------------------------------------------------------------
This file tests the initialization order of a custom control.
--------------------------------------------------------------------------------------------------}
INTERFACE
{$WARN GARBAGE OFF} { Silent the: 'W1011 Text after final END' warning }
USES
System.SysUtils, System.Classes, vcl.Controls, vcl.Forms, Vcl.StdCtrls, Vcl.ExtCtrls;
TYPE
TCustomCtrlTest = class(TPanel)
private
protected
Initialized: boolean;
Sub: TButton;
public
constructor Create(AOwner: TComponent); override;
procedure Loaded; override;
procedure AfterConstruction; override;
procedure CreateWnd; override;
procedure CreateWindowHandle(const Params: TCreateParams); override;
procedure WriteToString(s: string);
procedure SetParent(AParent: TWinControl); override;
published
end;
procedure Register;
IMPLEMENTATION
USES System.IOUtils;
procedure Register;
begin
RegisterComponents('Mine', [TCustomCtrlTest]);
end;
constructor TCustomCtrlTest.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Sub:= TButton.Create(Self);
Sub.Parent:= Self; // Typically, creating a sub-control and setting its Parent property to your main control will work just fine inside of your main control's constructor, provided that the sub-control does not require a valid HWND right way. Remy Lebeau
WriteToString('Create'+ #13#10);
end;
procedure TCustomCtrlTest.Loaded;
begin
inherited;
WriteToString('Loaded'+ #13#10);
end;
procedure TCustomCtrlTest.AfterConstruction;
begin
inherited;
WriteToString('AfterConstruction'+ #13#10);
end;
procedure TCustomCtrlTest.CreateWnd;
begin
WriteToString(' CreateWnd'+ #13#10);
inherited;
WriteToString(' CreateWnd post'+ #13#10);
Sub.Visible:= TRUE;
Sub.Align:= alLeft;
Sub.Caption:= 'SOMETHING';
Sub.Font.Size:= 20;
end;
procedure TCustomCtrlTest.CreateWindowHandle(const Params: TCreateParams);
begin
inherited CreateWindowHandle(Params);
WriteToString(' CreateWindowHandle'+ #13#10);
end;
procedure TCustomCtrlTest.SetParent(AParent: TWinControl);
begin
WriteToString('SetParent'+ #13#10);
inherited SetParent(AParent);
WriteToString('SetParent post'+ #13#10);
if NOT Initialized then { Make sure we don't call this code twice }
begin
Initialized:= TRUE;
SetMoreStuffHere;
end;
end;
procedure TCustomCtrlTest.WriteToString(s: string);
begin
System.IOUtils.TFile.AppendAllText('test.txt', s);
// The output will be in Delphi\bin folder when the control is used inside the IDE (dropped on a form) c:\Delphi\Delphi XE7\bin\
// and in app's folder when running inside the EXE file.
end;
end.
The order is:
Dropping control on a form:
Create
AfterConstruction
SetParent
CreateWnd
CreateWindowHandle
CreateWnd post
SetParent post
Deleting control from form:
SetParent
SetParent post
Cutting ctrol from form and pasting it back:
SetParent
SetParent post
Create
AfterConstruction
SetParent
CreateWnd
CreateWindowHandle
CreateWnd post
SetParent post
SetParent
SetParent post
Loaded
Executing the program
Create
AfterConstruction
SetParent
SetParent post
SetParent
SetParent post
Loaded
CreateWnd
CreateWindowHandle
CreateWnd post
Dynamic creation
Create
AfterConstruction
SetParent
CreateWnd
CreateWindowHandle
CreateWnd post
SetParent post
Reconstructing the form
Not tested yet
The solution I chose in the end is to initialize code that requires a handle in SetParent (or CreateWnd) and use a boolean var to protect from executing that code twice (see SetParent above).
I have created a simple test control inheriting from Tcustom control, which contains 2 panels. The first is a header aligned to the top and client panel aligned to alclient.
I would like the client panel to accept controls from the designer and although I can place controls on the panel, they are not visible at run time and they do not save properly when the project is closed.
The sample code for the control is as follows
unit Testcontrol;
interface
uses Windows,System.SysUtils, System.Classes,System.Types, Vcl.Controls,
Vcl.Forms,Vcl.ExtCtrls,graphics,Messages;
type
TtestControl = class(TCustomControl)
private
FHeader:Tpanel;
FClient:Tpanel;
protected
public
constructor Create(Aowner:Tcomponent);override;
destructor Destroy;override;
published
property Align;
end;
implementation
{ TtestControl }
constructor TtestControl.Create(Aowner: Tcomponent);
begin
inherited;
Fheader:=Tpanel.create(self);
Fheader.Caption:='Header';
Fheader.Height:=20;
Fheader.Parent:=self;
Fheader.Align:=altop;
Fclient:=Tpanel.Create(Self);
with Fclient do
begin
setsubcomponent(true);
ControlStyle := ControlStyle + [csAcceptsControls];
Align:=alclient;
Parent:=self;
color:=clwhite;
BorderStyle:=bssingle;
Ctl3D:=false;
ParentCtl3D:=false;
Bevelouter:=bvnone;
end;
end;
destructor TtestControl.Destroy;
begin
FHeader.Free;
FClient.Free;
inherited;
end;
end.
If I put a button on the test component, the structure shows it as part of the form and not a subcomponent of the test component....and then it doesnt work anyway.
Is there a way to do this?
After plenty of googling around, I found some information which allowed me to cobble together a solution that seems to work.
It seems there two procedures in the base class needs to be overridden to update the control.
The first is the a method called "Loaded" which is called when the streaming has ended.
It seems the streaming places all the sub-panel components placed by the designer on the base component, not on the panel they were originally parent to. So this routine manually reassigns the Parent properties after the loading process has finished.
The second method is called GetChildren, I couldn't find much information as to what this method actually does other than the rather cryptic text in the chm help. However I adapted the overridden code from another example I found on the web which had a similar requirement and it worked. So if anyone can provide some insight as to why this is necessary then that would be useful information.
I have pasted the complete source code for the sample custom component below so that anyone who has a similar requirement in the future, can use it as a starting template for their own components.
unit Testcontrol;
interface
uses Windows, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.ExtCtrls,graphics;
type
TtestControl = class(TCustomControl)
private
FHeader:Tpanel;
FClient:Tpanel;
protected
procedure Loaded;override;
procedure GetChildren(Proc:TGetChildProc; Root:TComponent);override;
public
constructor Create(Aowner:Tcomponent);override;
destructor Destroy;override;
published
property Align;
end;
implementation
{ TtestControl }
constructor TtestControl.Create(Aowner:Tcomponent);
begin
inherited;
Fheader:=Tpanel.create(self);
Fheader.setsubcomponent(true);
Fheader.Caption:='Header';
Fheader.Height:=20;
Fheader.Parent:=self;
Fheader.Align:=altop;
Fclient:=Tpanel.Create(Self);
with Fclient do
begin
setsubcomponent(true);
ControlStyle := ControlStyle + [csAcceptsControls];
Align:=alclient;
Parent:=self;
color:=clwhite;
BorderStyle:=bssingle;
Ctl3D:=false;
ParentCtl3D:=false;
Bevelouter:=bvnone;
end;
end;
destructor TtestControl.Destroy;
begin
FHeader.Free;
FClient.Free;
inherited;
end;
procedure TtestControl.Loaded;
var i:integer;
begin
inherited;
for i := ControlCount - 1 downto 0 do
if (Controls[i] <> Fheader) and (Controls[i] <> Fclient) then
Controls[i].Parent := Fclient;
end;
procedure TtestControl.GetChildren(Proc:TGetChildProc; Root:TComponent);
var i:integer;
begin
inherited;
for i := 0 to Fclient.ControlCount-1 do
Proc(Fclient.Controls[i]);
end;
end.
I wonder why, but I can not simply debug my simple program. Loaded method is ignored, it is never executed. No idea why. Look:
TGridObj = class (TComponent)
private
FPen1:TPen;
FBrush1:TBrush;
FChange:TNotifyEvent;
protected
procedure Loaded; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property OnChange:TNotifyEvent read FChange write FChange;
property Pen1:TPen read FPen1 write FPen1;
property Brush1:TBrush read FBrush1 write FBrush1;
end;
.
.
.
constructor TGridObj.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FPen1:=TPen.Create;
FPen1.OnChange:=FChange;
FBrush1:=TBrush.Create;
FBrush1.OnChange:=FChange;
end;
destructor TGridObj.destroy;
begin
FPen1.Free;
FBrush1.Free;
inherited;
end;
procedure TGridObj.Loaded();
begin
inherited Loaded;
ShowMessage(''); // this is never executed;
FPen1.OnChange:=FChange;
FBrush1.OnChange:=FChange;
end;
.
.
procedure TForm1.FormCreate(Sender: TObject);
begin
Grid:=TGridObj.Create(nil);
Grid.OnChange:=ev1.OnChange;
Form1.InsertComponent(Grid);
end;
Thanx
Loaded is only called when the component's properties are streamed from the form file. Since you are creating it at runtime, Loaded does not get called. This is by design.
Your code needs some work anyway to allow for the OnChange event to be modified at runtime and have that change filter down to the pen and brush. I'd do it like this:
TGridObj = class (TComponent)
private
FPen1: TPen;
FBrush1: TBrush;
FChange: TNotifyEvent;
procedure DoChange(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property OnChange: TNotifyEvent read FChange write FChange;
property Pen1: TPen read FPen1;
property Brush1: TBrush read FBrush1;
end;
constructor TGridObj.Create(AOwner: TComponent);
begin
inherited;
FPen1 := TPen.Create;
FPen1.OnChange := DoChange;
FBrush1 := TBrush.Create;
FBrush1.OnChange := DoChange;
end;
destructor TGridObj.Destroy;
begin
FBrush1.Free;
FPen1.Free;
inherited;
end;
procedure TGridObj.DoChange(Sender: TObject);
begin
if Assigned(FChange) then
FChange(Sender);
end;
Now there's no need for Loaded or anything like that. Because you wait until the OnChange events of the pen and brush actually fire before accessing FChange.
By the way, in your code it's a mistake to add property setters for Pen1 and Brush1 that modify the underlying fields. That leads to leaks and all sorts of mess. Also, be warned that exposing the pen and brush as public properties allows clients of TGridObj to change the OnChange event. And that subverts TGridObj.OnChange.
OnLoaded will only be executed if the component is loaded from a form resource (dfm file).
If the component is created at run time in code only, it will not be executed.
Update:
I recommend to design components so that they can be created and configured at run time too - which means I avoid overriding OnLoaded. The advantage is that no package installation / component registration is needed.
I started learning Delphi two days ago but I got stuck. I broke down because nothing goes my way so I decided to write here. I wanted to create class that would have a field with its own TTimer object and which will perform some action at some time interval. Is it even possible? Suppose we have such code:
Sth = class
private
public
clock:TTimer;
procedure clockTimer(Sender: TObject);
constructor Create();
end;
constructor Sth.Create()
begin
clock.interval:=1000;
clock.OnTimer := clockTimer;
end;
procedure Sth.clockTimer(Sender: TObject);
begin
//some action on this Sth object at clock.interval time...
end;
My similar code copiles but it doesn't work properly. When I call the constructor the program crashes down (access violation at line: clock.interval:=1000;). I don't know what
Sender:TObject
does but I think that's not the problem. Is it possible to create such class I want to?
You have not created the timer. Declaring a variable is not enough. You do need to create the timer.
constructor Sth.Create()
begin
clock := TTimer.Create(nil);
clock.interval:=1000;
clock.OnTimer := clockTimer;
end;
And you should destroy it too. Add a destructor to the class
destructor Destroy; override;
and implement it like this
destructor Sth.Destroy;
begin
clock.Free;
inherited;
end;
I would also recommend that you make your clock field have private visibility. It's not good to expose the internals of a class like that.
TMyClass = class
private
FClock: TTimer;
procedure ClockTimer(Sender: TObject);
public
constructor Create;
destructor Destroy; override;
end;
....
constructor TMyClass.Create
begin
inherited;
FTimer := TTimer.Create(nil);
FTimer.Interval := 1000;
FTimer.OnTimer := ClockTimer;
end;
destructor TMyClass.Destroy;
begin
FTimer.Free;
inherited;
end;
Note that I have included calls to the inherited constructor and destructor. These are not necessary in this class since it derives directly from TObject and the constructor and destructor for TObject is empty. But if you change the inheritance at some point, and make your class derive from a different class, then you will need to do this. So, in my view, it is good practise to include these calls always.
How can i simulate an OnDestroy event for a TFrame in Delphi?
i nievely added a constructor and destructor to my frame, thinking that is what TForm does:
TframeEditCustomer = class(TFrame)
...
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
...
end;
constructor TframeEditCustomer.Create(AOwner: TComponent)
begin
inherited Create(AOwner);
//allocate stuff
end;
destructor TframeEditCustomer.Destroy;
begin
//cleanup stuff
inherited Destroy;
end;
The problem with this is that by the time my destructor runs, controls on the frame have been destroyed and are no longer valid.
The reason for this is in the containing form's destructor, which it uses to fire an OnDestroy event:
destructor TCustomForm.Destroy;
begin
...
if OldCreateOrder then DoDestroy; //-->fires Form's OnDestroy event; while controls are still valid
...
if HandleAllocated then DestroyWindowHandle; //-->destroys all controls on the form, and child frames
...
inherited Destroy; //--> calls destructor of my frame
...
end;
The destructor of my frame object is being called when the form's destructor runs. Problem with this is that it's too late. The form calls DestroyWindowHandle, which asks Windows to destroy the form's window handle. This recursively destroys all child windows - including those on my frame.
So when my frame's destructor runs, i attempt to access controls that are no longer in a valid state.
How can i simulate an OnDestroy event for a TFrame in Delphi?
See also
Simulating OnCreate and OnDestroy for a Frame?
How to Implement the OnCreate event for a Delphi TFrame object
Embargadero QC#1767: TFrame misses OnCreate, OnDestroy, OnShow
You need to add a WM_DESTROY handler and check for csDestroying in the ComponentState so it's only caught when actually destroying, and not when recreating the handle.
type
TCpFrame = class(TFrame)
private
FOnDestroy: TNotifyEvent;
procedure WMDestroy(var Msg: TWMDestroy); message WM_DESTROY;
published
property OnDestroy: TNotifyEvent read FOnDestroy write FOnDestroy;
end;
procedure TCpFrame.WMDestroy(var Msg: TWMDestroy);
begin
if (csDestroying in ComponentState) and Assigned(FOnDestroy) then
FOnDestroy(Self);
inherited;
end;
That will only work if the frame's window handle has actually been created. There isn't another good hook point, so if you want to ensure it's always called you'll need to set a flag in WMDestroy and fall back to calling it in the destructor if that isn't hit.
The window handles themselves are all cleared in WM_NCDESTROY, which is called after all of the descendant WM_DESTROY messages return, so the form and all of its childens' handles should still be valid at this point (ignoring any that were freed in the form's OnDestroy).
Sounds more like OnClose than OnDestroy.
Anyway, I just inherited all my frames and forms from a base ancestor, and the form's onclose calls then all frames in the component hierarchy.
(It's just an idea but I haven't got the time right now to construct a proof of concept, but I'll share it none the less:)
If it's a problem with the Windows handle(s), you should check wether you're able to attach a Windows' event callback pointer that gets called when the frame's Windows handle ceases to exists. Perhaps with a function like RegisterWaitForSingleObject
Another option is to override AfterConstruction and BeforeDestruction
Something like this:
TMyFrame = class(TFrame)
private
FOnCreate: TNotifyEvent;
FOnDestroy: TNotifyEvent;
protected
procedure DoCreate; virtual;
procedure DoDestroy; virtual;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
property OnCreate: TNotifyEvent read FOnCreate write FOnCreate;
property OnDestroy: TNotifyEvent read FOnDestroy write FOnDestroy;
end;
implementation
procedure TMyFrame.AfterConstruction;
begin
inherited;
DoCreate;
end;
procedure TMyFrame.BeforeDestruction;
begin
inherited;
DoDestroy;
end;
procedure TMyFrame.DoCreate;
begin
if Assigned(FOnCreate) then
FOnCreate(Self);
end;
procedure TMyFrame.DoDestroy;
begin
if Assigned(FOnDestroy) then
FOnDestroy(Self);
end;