Preferred way to keep control centered on resize - delphi

What is your preferred way of keeping controls centered on its parent when the parent change width or height?

If by 'centered' you mean "it was already in the middle and you want to keep it there without resizing it", then remove all anchors. If it should be resized, gabr's solution is the one to with :)

Set control's Anchors property to [akLeft, akTop, akRight, akBottom].

If you mean a sort of "updating, please wait..." type thing, I manually move it in the Form's OnResize event. This allows me to keep a panel out of the way during design, and hidden normally, but I can make it visible when needed.
procedure TMyForm.FormResize(Sender: TObject);
var
nNewTop : Integer;
begin
inherited;
pnlRegenerating.Left := (ClientWidth - pnlRegenerating.Width) div 2;
nNewTop := (ClientHeight div 5) {* 4};
if (nNewTop + pnlRegenerating.Height) > ClientHeight then
nNewTop := ClientHeight - pnlRegenerating.Height - 4;
pnlRegenerating.Top := nNewTop;
end;

Related

Delphi element alignment - center

There seems to be align property that works really well, but is is possible to align element so all elements on panel would be aligned to center all on bottom of each other if they all have less than size of container? Something like top-center-center.
Something like this:
Or at least horizontally, and vertically they can have 100%.
Put the elements into their own container, such as a TPanel or TFrame, that is a child of your main container. Set the child container's Align property to alCustom and use the parent container's OnAlignPosition event to keep the child container centered to itself:
// Panel1 is the Parent container for the child panel...
procedure TMyForm.Panel1AlignPosition(Sender: TWinControl; Control: TControl;
var NewLeft, NewTop, NewWidth, NewHeight: Integer; var AlignRect: TRect;
AlignInfo: TAlignInfo);
begin
if Control = ChildPanel then
begin
NewLeft := AlignRect.Left + ((AlignRect.Width - Control.Width) div 2);
NewTop := AlignRect.Top + ((AlignRect.Height - Control.Height) div 2);
end;
end;
There is no need for coding anything. Just place panels and other visual objects in the right way an set the properties of the visual objects as shown here:
Align: alNone or alCustom
and
Anchors: none (akLeft=False, akTop=False, akRight=False, akBottom=False)
Than and an object will stay at its relative horizontal and vertical position. If you place it in the middle in a container it will stay centered.
To center it only vertical set
Align: alNone or alCustom
and
Anchors: akTop=True OR akBottom=True
To center it only horizontal set
Align: alNone or alCustom
and
Anchors: akLeft=True OR akRight=True
You can center the control with this little procedure
procedure CenterControl( AControl : TControl );
begin
if Assigned( AControl.Parent )
then
begin
// remove alignment
AControl.Align := alNone;
// remove the anchors
AControl.Anchors := [];
// center on parent
AControl.Left := ( AControl.Parent.ClientWidth - AControl.Width ) div 2;
AControl.Top := ( AControl.Parent.ClientHeight - AControl.Height ) div 2;
end
else
raise Exception.Create( 'Control needs a Parent!' );
end;
If the parent gets resized the control will always be centered, as long as you did not change its size.
In RAD 10+ there is control TRelativePanel which has AlignVerticalCenterWithPanel and AlignHorisontalCenterWithPanel life-save options (and other useful capabilities).
You can also position invisilble line or dot in the center and build other controls around it with TRelativePanel provided properties Above/Below/etc.
Worth to mention, control is made on top level Embarcadero quality standards (crashes only in design mode).

Conflicting AutoSize and AutoWrap in TFlowPanel

I am trying to use TFlowPanel component in the following manner:
Place on the main form Form1 component FlowPanel1: TFlowPanel.
Set Form1.Width = 400, FlowPanel1.Align = alTop, FlowPanel1.AutoSize = True, FlowPanel1.AutoWrap = True.
Place on the FlowPanel1 5 SpeedButtons and set their Width to 64.
Compile and run.
Reduce width of the form (something about Form1.Width = 200).
For some reason, the speedbuttons do not automatically line up in two rows when user resizes the form. Although, they do line up in two rows when AutoSize = False, AutoWrap = True.
What is the reason for this behavior and how to solve it?
Edit: I've found "quick and dirty" solution. The following code is the event handler to the TFlowPanel.OnResize event:
procedure TForm1.FlowPanel1Resize(Sender: TObject);
begin
with FlowPanel1 do
begin
AutoSize := False;
Realign; // line up controls
AutoSize := True; // adjust TFlowPanel.Height
end;
end;
However, I still wonder if there is a standard way to solve the problem.
I wasn't able to find the exact reason of such behavior in code, but basically you've challenged two sizing properties to fight, the AutoSize and Align. The problem is, I think, that when you resize a form, the control with AutoSize configured to True and Align set to alTop will first try to autosize the control and then align to top of its parent. What I can tell for sure, these two properties shouldn't be combined at least from their logical meaning.
What I would suggest to your workaround is turn off the autosize by default and in OnResize event turn it temporary on and back to off to automatically adjust the height. So in code it would change simply to:
procedure TForm1.FlowPanel1Resize(Sender: TObject);
begin
// there's no Realign here, since the AlignControls request is called
// at control resize, so here you have children already aligned, what
// you then need is to request the control to autosize the height and
// turn off the autosizing to the default, disabled state
FlowPanel1.AutoSize := True;
FlowPanel1.AutoSize := False;
end;
tl,dr: It's a bug in TFlowPanel.
Normally, the AutoSize and Align properties go together very well by default since this is taken care of already at TControl level, so I wondered why this happened. I noticed an overriden AlignControls method in TFlowPanel and thought to bypass it for testing purposes:
type
TWinControlAccess = class(TWinControl);
TAlignControls = procedure(Instance: TObject; AControl: TControl;
var Rect: TRect);
TFlowPanel = class(Vcl.ExtCtrls.TFlowPanel)
protected
procedure AlignControls(AControl: TControl; var Rect: TRect); override;
end;
TForm1 = class(TForm)
...
procedure TFlowPanel.AlignControls(AControl: TControl; var Rect: TRect);
begin
// Skip TCustomFlowPanel.AlignControls
TAlignControls(#TWinControlAccess.AlignControls)(Self, AControl, Rect);
end;
procedure TForm1.FlowPanel1Resize(Sender: TObject);
begin
// Do my own aligning of the last button
if FlowPanel1.ClientWidth < Button5.BoundsRect.Right then
begin
Button5.Left := 1;
Button5.Top := Button1.Height + 1;
end
else if FlowPanel1.ClientWidth > Button4.BoundsRect.Right + Button5.Width then
begin
Button5.Left := Button4.BoundsRect.Right;
Button5.Top := 1;
end;
end;
Now, this works as expected. So what's wrong with TFlowPanel's implementation of AlignControls? It looks like the following snippet is the reason:
if AutoSize then
Rect := TRect.Create(
Rect.Left,
Rect.Top,
Rect.Left + (ExplicitWidth - (Width - (Rect.Right - Rect.Left))),
Rect.Top + (ExplicitHeight - (Height - (Rect.Bottom - Rect.Top))));
When this part is commented out, the behaviour is as expected with Align set as well as not. Now, I would like to submit this to QC, but maybe I am overlooking some of its aspects. Please edit or comment when (and then why) this code indeed is needed.

TStatusBar with bottom aligned panel

I have a form with a TStatusBar, and bottom aligned TPanel, a bottom aligned TSplitter and client aligned TPanel, as shown in the following screenshot:
(The splitter is coloured red to make it a little more obvious)
Button1 simply increases the height of Panel1 by 20:
Panel1.Height := Panel1.Height + 20;
But when clicked the order of the controls changes, with Panel1 being blow the status bar and the splitter is now resizing the status bar.
This only happens when the height of Panel1 is increased by more than the height of StatusBar1 (19).
I assume this is caused by having two bottom aligned controls, but I'm at a loss as to the exact cause of the problem and how to work around it.
I'm currently using XE2, but I have the same issue with D2010.
In this situation is there a way to set the height of Panel1 to an arbitrary value, while ensuring that the controls maintain their expected positions?
Work around the problem by setting the Top property for the controls after changing the panel height.
StatusBar1.Top := Panel1.Top + Panel1.Height;
Try this (worked fine for me):
procedure TForm1.Button1Click(Sender: TObject);
begin
Panel1.SetBounds(Panel1.Left, Panel1.Top - 20,
Panel1.Width, Panel1.Height + 20);
end;
Alternatively, if you don't want to keep track of where you're changing position/size of controls,
type
TForm1 = class(TForm)
..
private
protected
procedure AlignControls(AControl: TControl; var Rect: TRect); override;
..
procedure TForm1.AlignControls(AControl: TControl; var Rect: TRect);
begin
inherited;
if AControl = Panel1 then
StatusBar1.Top := Panel1.Top + Panel1.Height;
end;

Delphi - Open window at location of a TLabel

I have two forms, Form1 and Form2
I have a TLabel on Form1 which an onclick event which calls Form2.show;
What I am trying to do, if figure out how I can make form2 show 5px below the label centered between the label :) Form2 is small and just shows some options.
I can use the mouse position but it's not quite good enough.
I was thinking something like
// Set top - add 20 for the title bar of software
Form2.Top := Form1.Top + Label1.Top + Label1.Height + 20;
// Set the Left
Form2.Left := Form1.Left + Label1.Left + round(Label1.Width / 2) - round(form2.Width/2);
but I think there can be a better way
You need to set the coordinates for Form2 using the coordinate system of it's Parent. Assuming the Parent is the Desktop (since you're attempting to compensate for the height of the title bar), this can do it:
procedure ShowForm;
var P: TPoint;
begin
// Get the top-left corner of the Label in *screen coordinates*. This automatically compensates
// for whatever height the non-client area of the window has.
P := Label1.ScreenToClient(Label1.BoundsRect.TopLeft);
// Assign the coordinates of Form2 based on the translated coordinates (P)
Form2.Top := P.Y + 5; // You said you want it 5 pixels lower
Form2.Left := P.X + 5 + (Label1.Width div 2); // Use div since you don't care about the fractional part of the division
end;
You'll need to adapt the code for the positioning of Form2 based on your centering requirement, I didn't quite understand what you want. And of course, if a frame or panel is enough, it's better! Take a good look at Guillem's solution.
procedure TForm2.AdjustPosition(ARefControl: TControl);
var
LRefTopLeft: TPoint;
begin
LRefTopLeft := ARefControl.ScreenToClient(ARefControl.BoundsRect.TopLeft);
Self.Top := LRefTopLeft.Y + ARefControl.Height + 5;
Self.Left := LRefTopLeft.X + ((ARefControl.Width - Self.Width) div 2);
end;
Then you can have the form adjust itself relative to any desired control as follows:
Form2.AdjustPosition(Form1.Label1);
Do you really need Form2 to be a form? You could choose to create a frame containing the Form2 logic and use a hidden TPanel as its parent. When the user clicks on the Label1 you show then the panel.
Something like following. When you create Form1 or when you click on Label1 (depending on your needs):
Frame := TFrame1.Create(Self);
Frame.Parent := Panel1;
In the OnClick event for Label1:
Panel1.Top := Label1.Top + 5;
Panel1.Left := Label1.Left + round(Label1.Width / 2) - round(form2.Width/2);
Panel1.Visible := true;
When the user is done just hide the panel again (and destroy the Frame if necessary). If you keep the Frame alive while the user is using Form1 remember to free it when leaving the form.
HTH
The ClientOrigin property will return the lebel's upper-left corner in screen coordinates, so you do not need to determine it manually:
var
Pt: TPoint;
begin
Pt := Label1.ClientOrigin;
Form2.Left := Pt.X + Round(Label1.Width / 2) - Round(Form2.Width/2);
Form2.Top := Pt.Y + Label1.Height + 5;
end;

Calculate needed size for a TLabel

Ok, here's the problem. I have a label component in a panel. The label is aligned as alClient and has wordwrap enabled. The text can vary from one line to several lines. I would like to re-size the height of the the panel (and the label) to fit all the text.
How do I get the necessary height of a label when I know the text and the width of the panel?
You can use the TCanvas.TextRect method, along with the tfCalcRect and tfWordBreak flags :
var
lRect : TRect;
lText : string;
begin
lRect.Left := 0;
lRect.Right := myWidth;
lRect.Top := 0;
lRect.Bottom := 0;
lText := myLabel.Caption;
myLabel.Canvas.Font := myLabel.Font;
myLabel.Canvas.TextRect(
{var} lRect, //will be modified to fit the text dimensions
{var} lText, //not modified, unless you use the "tfModifyingString" flag
[tfCalcRect, tfWordBreak] //flags to say "compute text dimensions with line breaks"
);
ASSERT( lRect.Top = 0 ); //this shouldn't have moved
myLabel.Height := lRect.Bottom;
end;
TCanvas.TextRect wraps a call to the DrawTextEx function from the Windows API.
The tfCalcRect and tfWordBreak flags are delphi wrappers for the values DT_CALCRECT and DT_WORDBREAK of the windows API. You can find detailed information about their effects in the DrawTextEx documentation on msdn
Use TextWidth and TextHeight.
See an example here:
http://www.greatis.com/delphicb/tips/lib/fonts-widthheight.html
TextWidth will tell you how wide the text would be, and then you can divide that by the control width to see how many rows you need. The remainder of the division should be an additional row.
You can use one line of code for this:
label.width := label.canvas.textwidth(label.caption);
or you can set the label's autosize property to true in the object inspector.
If you can align it alTop and keep AutoSize on then TLabel will auto adjust the height after settign the caption.
in FMX there is a trick to do that simply :
when creating a Label set Autosize := true and use the OnResize Event to update the size of the parent...
Rectangle1 := TRectangle.create(Form1);
Rectangle1.parent := Form1;
Label1 := TLabel.create(Rectangle1);
Label1.parent := Rectangle1;
Label1.Align := TAlignLayout.Top; // keep the same width and auto size parent height
Label1.OnResize := DoReSize;
Label1.WordWrap := true;
Lable1.Autosize := true;
The parent size will be updated here (assuming that the Sender object is the most bottom control in the parent, if not you need to arrange this function to summarize all the components size and verticaly)
procedure DoParentResize(Sender : TObject);
begin
TControl(TControl(Sender).parent).Height := TControl(Sender).Height + 4;
end;
if we use Label1.Align := TALignLayout.None;
then we should add the position inside the parent :
procedure DoParentResize(Sender : TObject);
begin
TControl(TControl(Sender).parent).Height := TControl(Sender).Position.Y + TControl(Sender).Height + 4;
end;
Wich result in a single function for (almost) all cases :
procedure TForm1.DoParentResize(Sender : TObject);
begin
if TControl(Sender).Align in [TAlignLayout.None, TAlignLayout.Client, TAlignLayout.Center, TAlignLayout.VertCenter ] then
begin
TControl(TControl(Sender).parent).Height := TControl(Sender).Position.Y + TControl(Sender).Height + 4;
end
else
begin
TControl(TControl(Sender).parent).Height := TControl(Sender).Height + 4;
end;
end;
You need to reduce the LRect.right by the label left and right margins, and then add the label top and bottom margins to the label height at the end or the text might not fit the label.
procedure TFrm.PatternEditTyping(Sender: TObject);
begin
(Sender as Tedit).Canvas.Font.Size := (Sender as Tedit).Font.Size;
(Sender as Tedit).Width := (Sender as Tedit).Canvas.TextWidth((Sender as Tedit).Text);
end;
This code adjusts Tedit.Width while you type inside it. Just keep the font family in Canvas and in Tedit the same.

Resources