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).
Related
In firemonkey I am trying to make a progressbar using rectangles with round corners. The simplest case is a rectangle (the progressbar) and the second rectangle inside it (progress till now). Attached a simple example is provided.
Progressbar with corners (paint):
I've tried the following things:
Let the second rectangle also have rounded corners. This doesn't work because these roundings will change if the second rectangle is very short or almost at the end.
Use clipchildren. This is almost the same as hiding overflow in html / css, but Delphi does not include rounded corners in this function.
Create a TPath in which the image should be drawn. I really like to avoid this solution, because it doesn't use the stylebook. I prefer using one stylebook for all styles, instead of using multiple places in the code for style solutions.
What does work:
There is one really ugly method to make this work. I use that method now, but I really hope you can help me find another solution. The ugly method is:
Just use one rectangle. Fill it with a gradient brush, set the two gradient point at the same place and make the gradient itself 0 degrees. The result of this method is a lot of ugly code when I've to change the status of the progressbar etc.
Is this something we can avoid, or is this the only solution that is possible?
Progressbar goal (paint):
Thank you in advance!
Jan
I'm not sure what you mean by
Use clipchildren. This is almost the same as hiding overflow in html / css, but Delphi does not include rounded corners in this function.
I got this to work by using one Rectangle for the border; on top of that a Layout for the progress, which contains another Rectangle. The second Rectangle always has the dimensions of the first (which means the corners look the same), the Layout's ClipChildren is set to true, and the progress is controlled by setting its Width.
Here's how I implemented it:
type
TRoundProgressBar = class (TLayout)
strict private
FProgress: Single;
FFill: TBrush;
FStroke: TStrokeBrush;
StrokeRect, FillRect: TRectangle;
FillLayout: TLayout;
procedure SetFill(const Value: TBrush);
procedure SetStroke(const Value: TStrokeBrush);
procedure FillChanged(Sender: TObject);
procedure StrokeChanged(Sender: TObject);
procedure SetProgress(Progress: Single);
procedure UpdateWidths;
protected
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property Fill: TBrush read FFill write SetFill;
property Stroke: TStrokeBrush read FStroke write SetStroke;
property Progress: Single read FProgress write SetProgress;
end;
implementation
constructor TRoundProgressBar.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FFill := TBrush.Create(TBrushKind.Solid, $FFE0E0E0);
FFill.OnChanged := FillChanged;
FStroke := TStrokeBrush.Create(TBrushKind.Solid, $FF000000);
FStroke.OnChanged := StrokeChanged;
FillLayout := TLayout.Create(self);
FillLayout.Parent := self;
FillLayout.Align := TAlignLayout.Left;
FillLayout.ClipChildren := true;
FillRect := TRectangle.Create(FillLayout);
FillRect.Parent := FillLayout;
FillRect.Align := TAlignLayout.Left;
FillRect.XRadius := 15;
FillRect.YRadius := 15;
StrokeRect := TRectangle.Create(self);
StrokeRect.Parent := self;
StrokeRect.Align := TAlignLayout.Contents;
StrokeRect.XRadius := 15;
StrokeRect.YRadius := 15;
StrokeRect.Fill.Kind := TBrushKind.None;
end;
destructor TRoundProgressBar.Destroy;
begin
FFill.Free;
FStroke.Free;
inherited;
end;
procedure TRoundProgressBar.SetFill(const Value: TBrush);
begin
FFill.Assign(Value);
end;
procedure TRoundProgressBar.SetProgress(Progress: Single);
begin
FProgress := Min(Max(Progress, 0), 100);
UpdateWidths;
end;
procedure TRoundProgressBar.FillChanged(Sender: TObject);
begin
FillRect.Fill.Assign(FFill);
end;
procedure TRoundProgressBar.Resize;
begin
inherited;
UpdateWidths;
end;
procedure TRoundProgressBar.SetStroke(const Value: TStrokeBrush);
begin
FStroke.Assign(Value);
end;
procedure TRoundProgressBar.StrokeChanged(Sender: TObject);
begin
StrokeRect.Stroke.Assign(FStroke);
end;
procedure TRoundProgressBar.UpdateWidths;
begin
FillRect.Width := Width;
FillLayout.Width := Width * (FProgress / 100);
Repaint;
end;
Exactly clipchildren can not work, because it's use the bounding box of the control (so a Rectf). however what you can do :
1) override the onpaint of the trectangle (it's quite simple)
2) Use 2 Trectangles (call them orange and white), on the first tRectangle (orange) you set to not draw the left sides (via the Sides property of Trectangle) and of the second Trectangle (white) you set to not draw the right sides (also via the sides property). put these 2 Trectangles inside a Tlayout (or any other container you would like), set the align of the second Trectangle (white) to all, and the align of the first Trectangle (orange) to ALleft. after you just need to say MyOrangeRect.width := XX where xx the amount of your progress relative to the with of the container off course
I like to chip in with another solution with just one TRectangle:
Just add a TRectangle, set your borders, corners and set the fill property to TBitmap.
Now you can create a TBitmap with a color (with the width as progress) to the fill.bitmap.bitmap (notice the double bitmap) property at runtime.
Your corners are still respected.
extra: You can also use a one vertical line bitmap created in photoshop with a nice glow/color effect like the IOS battery progress bar and stretch that in your TRectangle.
Just use two shapes (roundrect) like this:
procedure TForm4.SpinBox1Change(Sender: TObject);
begin
roundrect2.Width:=strtoint(SpinBox1.Text);
end;
And change the width property of the upper shape when ever you want to progress more;
When a TRectangle is painted internally it actually creates a path.
The best solution for you would be to make a custom component, which contains two TPathData (call them e.g. PathBackground and PathFill), that are recalculated when the percentage changes and when it is resized.
In the Paint routine I would paint this way
Canvas.FillPath(PathBackground, ...);
Canvas.FillPath(PathFill, ...);
Canvas.DrawPath(PathBackground, ...);
By drawing the edge as the last thing, you avoid rendering errors.
It is easy to detect whether the vertical scrollbar of a TScrollBox is at the very top or not:
IsScrollBarAtTop := ScrollBox1.VertScrollBar.Position = 0;
But how can I detect whether the vertical scrollbar of a TScrollBox is at the very BOTTOM or not?
You can retrieve scroll bar information through the API and determine if its at the bottom.
function IsScrollBarAtBottom(Box: TScrollBox): Boolean;
var
Info: TScrollInfo;
begin
Info.cbSize := SizeOf(Info);
Info.fMask := SIF_POS or SIF_RANGE or SIF_PAGE;
Win32Check(GetScrollInfo(Box.Handle, SB_VERT, Info));
Result := Info.nPos >= Info.nMax - Info.nMin - Info.nPage;
end;
From Vcl.Forms.TControlScrollBar.Range:
Range represents the virtual size (in pixels) of the associated control's client area. For example, if the Range of a form's horizontal scroll bar is set to 500, and the width of the form is 200, the scroll bar's Position can vary from 0 to 300.
IsScrollBarAtBottom := ScrollBox1.VertScrollBar.Position =
(ScrollBox1.VertScrollBar.Range - ScrollBox1.ClientHeight);
If the range is less than the height of the scrollbox, the scrollbar is not visible.
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.
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;
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;