When we create a component as a custom control and the control is dropped on a panel the control always appears on the form rather than the containing control. How do you set the parent of the custom control in Create so that when the button is dropped on panel the buttons parent is the panel?
TGlassButton = class(TCustomControl)
...
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
...
constructor TGlassButton.Create(AOwner: TComponent);
begin
inherited; ???????????
inherited Create(AOwner); ????????????
Parent := TWinControl( AComponent ); ??????????????
...
end;
The problem is designtime creation not runtime. This works perfectly:
procedure TForm10.FormCreate(Sender: TObject);
begin
GlassButton0 := TGlassButton.Create( Panel1 );
GlassButton0.Parent := Panel1;
GlassButton0.Left := 20;
GlassButton0.Top := 6;
GlassButton0.Width := 150;
GlassButton0.Height := 25;
GlassButton0.Caption := 'Created At RunTime';
end;
DO NOT set the Parent property in the constructor! As others have said, the IDE and DFM streaming systems will assign the Parent automatically AFTER the constructor has exited. If you need to perform operations in your constructor that are dependant on a Parent being assigned, then you need to re-design your component. Override the virtual SetParent() and/or Loaded() methods and do your operations from there instead. And make use of if (csDesigning in ComponentState) then ... checks in places where you can avoid operations that are not actually needed at design-time.
Parents should be set by whomever is creating the control. For controls created at design time, this would be done by the streaming system when the form is created. For controls created at run-time, it should be done when the control is created:
var
Control: TWinControl;
begin
Control := TGlassButton.Create(<Form or Application>);
Control.Parent := <Some other control on the form>;
end;
Please note that in general the form is the owner of all controls on it, regardless of parent-ing. The Parent of a control is / should be the control responsible for painting it: in other words the control in which it is visually located. Ie a Panel, TabSheet, GroupBox or some other container.
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 custom control inherited from TControl, using TPathData to create lines - it draws a custom wayfinding path for a graphical application that I'm building. I currently have created the control and have a path which is displayed and property repaints.
The inherited properties that would provide mouse-based interaction do not work (e.g., OnClick, OnMouseDown) even though I can assign an OnClick event handler without runtime errors. The handler is simply never called.
My custom control is declared as:
TPathSegment = class (TControl)
...
with:
public
constructor Create(ACanvas: TCanvas; AObject: TControl; listsize:int32);
destructor Destroy; override;
...
procedure Paint; override;
From the calling program, I create an object like this:
mypath := TPathSegment.Create(canvas,layout1,50);
mypath.HitTest := true;
mypath.Parent := layout1;
mypath.Onclick := MyPathOnClick;
mypath.pathThickness := 10;
mypath.AddSegment(PointF(100,150),TlineMove.mMT,'',TAlphaColorRec.Black
,TlineEnd.leRectangle,TAlphaColorRec.Blue,TStrokeDash.Solid);
I expect to be able to click on a drawn line (defined by TPathData) and handle an event - but I can't.
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.
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.
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;