My program uses dynamically created frames and sometimes I get an issue that their controls are improperly aligned.
I use my own container control inherited from TPanel, but the same problem can be found when using the GridPanel as well.
Here is the source of a test program that reproduces the problem (with compiled exe).
The key code snippets:
in the main form:
//creating Frame from the main form
procedure TForm1.FormCreate(Sender: TObject);
begin
f := TFrame2.Create(Self);
f.Parent := Self;
end;
in the frame:
//constructor of the Frame
constructor TFrame2.Create(AOwner: TComponent);
begin
inherited;
Edit1.Clear;// the problem appears
end;
The frame and all its controls are aligned and must have the width of the main form, but Edit1 and ComboBox1 are visually not aligned until you resize the form manually (Sending WM_SIZE has no effect).
But if you comment the Edit1.Clear line everything will work fine from the program start. This code is not specific for the error and you can enter here e.g. ComboBox1.Items.Add('') etc.
If the frame is created statically or the GridPanel is changed to Panel the problem disappears.
I have made a new test2 version thanks to #quasoft, it works better - now the controls are horizontally aligned proper but vertically combobox is not in the right place that can be seen by changing the form size.
Quick fix:
The quick solution to your problem is to use Text property of TEdit, instead of the Clear method - as already said, replacing Edit1.Clear with Edit1.Text := ''.
Understanding the problem
But you need to understand this problem better if you plan to use frames in Delphi in the long term, or they will haunt you while you sleep (joking).
The real problem is that you are modifying the state of the frame before a Parent has been assigned to it.
procedure TForm1.FormCreate(Sender: TObject);
begin
f := TFrame2.Create(Self); // <--- Your text edit is cleared inside
f.Parent := Self; // <--- Your frame is attached to the form here
end;
Doing so does not allow the TGridPanel component to take in account the width, height and position of parent when calculating the size of its columns and rows.
Using Text property works, because the property setter does not change the text of the control directly, but sends a message for the purpose to the message queue:
Except from Controls.pas:
...
procedure TControl.SetTextBuf(Buffer: PChar);
begin
Perform(WM_SETTEXT, 0, Longint(Buffer));
Perform(CM_TEXTCHANGED, 0, 0);
end;
procedure TControl.SetText(const Value: TCaption);
begin
if GetText <> Value then SetTextBuf(PChar(Value));
end;
...
Which in effect, causes the text to actually change after you have assigned the Parent of the frame - as the message queue will be processed a little bit after the form create method completes.
Clear method on the other hand directly changes the text:
Excerpt from StdCtrls.pas:
...
procedure TCustomEdit.Clear;
begin
SetWindowText(Handle, '');
end;
...
A better solution
As you already learned, the quick fix works only in the specific example you provided.
A better solution is to create an Init method in your frame and call this method from the main form after Parent has been assigned:
Your frame:
procedure TFrame2.Init;
begin
Edit1.Clear;
ComboBox1.Items.Add('Foo Bar');
end;
Your main form:
procedure TForm1.FormCreate(Sender: TObject);
begin
f := TFrame2.Create(Self);
f.Parent := Self; // <--- Your frame is attached to the form here
f.Init; // <--- Calls initialization code of frame
end;
Related
In this particular case I'm using PowerPDF library to dynamically build a PDF document, but the same applies for the general concept of dynamically aligning controls sequentially inside of a parent control. In this library, TPRPage is the base control to contain all element controls, in this case, sequential instances of TPRLayoutPanel.
What I do when dynamically adding controls:
Create a control (TPRLayoutPanel)
Set the control's parent (TPRPage)
Align the control to top (PRLayoutPanel.Align:= alTop;)
The problem is it gets forced to the very beginning (top) instead of the very end (bottom) of the page.
I've tried setting its order PRLayoutPanel.SendToBack; or PRLayoutPanel.BringToFront but with no luck.
How can I dynamically create and align multiple controls within a parent control sequentially? My only current work-around is to add the controls in reverse order (from end to beginning) which is ridiculously unnecessary.
Here's my universal function which creates every new instance of an aligned control in this parent:
function TfrmReport.InsertPanel: TPRLayoutPanel;
begin
Result:= TPRLayoutPanel.Create(PRPage);
Result.Parent:= PRPage;
Result.Align:= alTop;
Result.Height:= 40; //Default, may change later
end;
Once again, DisableAlign and EnableAlign to the rescue:
procedure TForm1.FormCreate(Sender: TObject);
var
I: Integer;
P: TPanel;
begin
DisableAlign;
try
for I := 0 to 4 do
begin
P := TPanel.Create(Self);
P.Caption := IntToStr(I);
P.Align := alTop;
P.Parent := Self;
end;
finally
EnableAlign;
end;
end;
Explanation:
When alignment is enabled, every single addition of a control to a container (the form itself in this specific case) will re-evaluate all alignment (and anchor) settings of all other controls within that container. In case that control has no specific Top property set, then Top will be 0. When there is already another control aligned to the top, then there are two controls with Top = 0, and the one which is about to inserted wins. I (currently) have no in-depth explanation for that, but it just is, and the position order indeed gets reversed from the creation order.
Now, when alignment of the container is disabled, then consecutive added controls are simply just inserted with all their positioning properties unaltered. When alignment is enabled again, then all those controls are re-evaluated in the same manner, with the difference that this takes place in one single loop in the order of the index in the Controls array; i.e. the order in which they were created.
You need to set the Top property to be the bottom of the previous panel. For example, like this:
PanelTop := 0;
for i := 0 to 5 do
begin
Panel[i] := TPanel.Create(Self);
Panel[i].Parent := Self;
Panel[i].Height := PanelHeight;
Panel[i].Align := alTop;
Panel[i].Top := PanelTop;
inc(PanelTop, PanelHeight);
end;
To fit it into your code you'd have to keep track of the location of the most recently added panel. Perhaps you could add a var parameter to your InsertPanel function:
function TfrmReport.InsertPanel(var PanelTop: Integer): TPRLayoutPanel;
begin
Result:= TPRLayoutPanel.Create(PRPage);
Result.Parent:= PRPage;
Result.Top:= PanelTop;
Result.Height:= 40;
Result.Align:= alTop;
inc(PanelTop, Result.Height);
end;
I trust you get the idea!
You may use alCustom align type and control all of your panels positions via CustomAlignPosition method (you will need to override it in parent control). This will give you more flexibility and control.
I want to create a custom control derived from TPanel that contains an image and a bunch of other controls on it.
After writing the code I have some weird behavior in my program. I realized that some vars that were supposed to be initialized in TDisplay.Resize (override) was never initialized because the Resize was never executed.
To 'solve it' I put a button on a form and called the LoadSample function which calls ClientHeight which calls Resize FOR THE FIRST TIME!
constructor TDisplay.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Ready := FALSE;
Parent := Owner as TWinControl;
Width := 200;
Height := 86;
Color := clSilver;
Caption := '';
DoubleBuffered:= TRUE;
InternalDisplay:= TImage32.Create(Self);
with Display DO
begin
Parent := Self;
Bitmap.Width := 1;
Bitmap.Height := 1;
RepaintMode := rmOptimizer;
Align := alClient;
SetupBitmap(TRUE, clBlack32);
Visible := TRUE;
OnMouseDown := DMouseDown;
end;
...
end;
Update:
Also the InternalDisplay won't be aligned to its parent size until I manually resize the form (the control) at runtime. Only then it will act as it was supposed to act (to stay aligned to alClient).
Update 2:
Resize is declared like that: procedure Resize; override;
Update 3:
I removed the ClientHeight line from my construnctor and move it here:
procedure TDisplay.LoadSample(VAR Obj: TMySample; CONST bReleaseOnExit: boolean)
begin
ClientHeight; <--------- this will call Resize for the first time and my code will be finally initialized. But until this point my display will look odd because the code was never initialized. So the user will see weird stuff until it pushes the 'LoadSample' button.
more code here....
end;
Update 4:
I used HandleNeeded as David suggested and it solved the initialization problem. However, the Image still won't align to the entire client area unless I manually resize the form/control.
Update 5
Continued here, as David suggested: TImage won't align to parent
Your control is derived from TWinControl, and TWinControl calls Resize in response to the WM_SIZE message. So, Resize will not be called unless the control's window handle has been created.
The Resize method is not called when you assign Height, or indeed Width, because the window handle has not yet been allocated.
When you evaluate the ClientHeight property, that results in the window handle being created, and then Resize is called. That's because GetClientHeight calls GetClientRect which looks like this:
function TWinControl.GetClientRect: TRect;
begin
Winapi.Windows.GetClientRect(Handle, Result);
end;
And it's the evalutation of the Handle property that forces the window handle into existence.
Your form isn't showing yet, so it isn't yet able to receive Windows messages (such as the resize message that triggers the OnResize event).
Delphi XE.
There is a Buttoned Edit (with the left button), an image list with a picture for the button. All is on a frame (it is Ok if on a form).
There is no an indent of the button in a design time, but it is in a run time.
Is it a bug?
Thanks!
Yes it's a bug. For some reason the Ctl3D property of the TButtonEdit is not being streamed correctly from the .dfm file when the edit resides in a frame. The streaming is resulting in Ctl3D being False instead of True.
Then, in TEditButton.UpdateBounds the following code executes:
if (not FEditControl.Ctl3D) and (FEditControl.BorderStyle <> bsNone) then
begin
FGlyph.Top := 2;
Inc(NewLeft, 2);
end;
This is what is shifting the position of your button 2 pixels right and down.
You can work around the bug by manually setting Ctl3D in code and forcing UpdateBounds to be called again. I did this with an interposer:
type
TButtonedEdit = class(ExtCtrls.TButtonedEdit)
protected
procedure Loaded; override;
end;
procedure TButtonedEdit.Loaded;
begin
inherited;
Ctl3D := True;
LeftButton.Visible := not LeftButton.Visible;
LeftButton.Visible := not LeftButton.Visible;
RightButton.Visible := not RightButton.Visible;
RightButton.Visible := not RightButton.Visible;
end;
You can include this in your frame, but make sure that the declaration of the interposed TButtonedEdit is before your frame is declared. Or if the bug afflicts many frames, declare it in a common unit and use that unit in your frame after you use ExtCtrls.
Now, as for the obvious question as to why the streaming doesn't set Ctl3D correctly, I have no answer for that. Perhaps someone more knowledgeable than I am about form streaming could enlighten us!
I have an application that has 5 different sized frames. I'd like to dynamically re-size the main form to fit the frame when I move from one frame to another.
I can use the MinHeight/MinWidth properties of the frame to force the main form to fit the frame, but then when moving to a smaller frame, the main form does not adjust it's size.
Any ideas?
--Edit
...
TFormMain = Class(TForm)
...
public
FrameImportPackage: TFrameImportPackage;
...
procedure TFormMain.MenuPackagesImportClick(Sender: TObject);
begin
if not (Assigned(FrameImportPackage)) then
begin
FrameImportPackage := TFrameImportPackage.Create(Self);
FrameImportPackage.LabelFrameCaption.Caption := 'Import or Edit a Package';
end
else
begin
FrameImportPackage.BringToFront;
end;
FrameImportPackage.Parent := Self;
end;
--Edit
Regards, Pieter
If I understand your question correctly, you've got frames that don't change size, you want the form to update size to fit your frames. Let Delphi handle that for you, using the AutoSize property.
Set AutoSize = True for your form.
I've tested AutoSize with the following code, using Delphi 2010:
Create a new VCL application. On the blank form drop a single Panel, let it keep it's name (Panel1). Make sure the panel is not too small, because we'll write code to decrease it's size at runtime.
Set the form's AutoSize property to True.
Drop two buttons on the panel, Button1 and Button2.
Double click the buttons, and copy-paste the following event handlers:
Code:
procedure TForm31.Button1Click(Sender: TObject);
var NewR: TRect;
begin
NewR := Panel1.BoundsRect;
Dec(NewR.Right, 32);
Dec(NewR.Bottom, 32);
Button1.Parent := Self;
Button2.Parent := Self;
Panel1.Free;
Panel1 := TPanel.Create(Self);
Panel1.BoundsRect := NewR;
Panel1.Parent := Self;
Button1.Parent := Panel1;
Button2.Parent := Panel1;
end;
procedure TForm31.Button2Click(Sender: TObject);
begin
Panel1.Height := Panel1.Height - 32;
Panel1.Width := Panel1.Width - 32;
end;
This essentially gives you two ways of decreasing the size of the panel, to handle two possible scenarios: Button1 frees the old panel and creates a new, smaller panel. Button2 directly resize the existing panel. Both work as expected!
At least on Delphi 2006 there is a really anonying BUG with Form AutoSize.
You put a TStringGrid onto the form (Left and Top equal zero, align equal None, Top, CLient does not matter), when you change its ClientWidth and ClientHeightt the form not allways adjusts its size to the control.
Normally when it fails is when control size is reduced, form size does not get reduced.
There is no good fix, the only way to do it is manually set clientwidth and clientheight of the form when the object is resized.
It is said: Form AutoSize does not allways work well! It is a BUG on the VCL.
Quite a simple one i would think, but i need to be able to Maximize a form to a particular screen. Cant seem to find any Delphi specific info.
I can remember the forms position over subsequent application loads. However, when i restore the position, then call WindowState := wsMaximized, the form moves to the other screen! (i do have other forms also visible on that screen - it appears its maximizing to the 'active screen')
So i need a function like so:
procedure Maximize(const aScreenIndex : Integer);
begin
if aScreenIndex < Screen.MonitorCount then
//Maximize to that screen
end;
Intercept the WM_GETMINMAXINFO message and adjust the coordinates inside its MINMAXINFO structure as needed.
Set Form.Position to poDesigned at design time
In Form.FormShow or your Maximize procedure:
procedure Maximize(const aScreenIndex : Integer);
begin
if aScreenIndex < Screen.MonitorCount then
begin
//Maximize to that screen
Myform.Left := screen.Monitors[aScreenIndex ].Left;
Myform.WindowState := wsMaximized;
end;
end;