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.
Related
I have simple form TForm1 with 2 panels. First with Align := alLeft and second with Align := alClient and empty frame TFrame1. When i add following procedures to the form, everything works without problems.
procedure TForm1.FormCreate(Sender: TObject);
var
lFrame1, lFrame2 : TFrame1;
begin
lFrame1 := TFrame1.Create(nil);
lFrame1.Parent := pnl1;
lFrame1.Align := alClient;
lFrame2 := TFrame1.Create(nil);
lFrame2.Parent := pnl2;
lFrame2.Align := alClient;
end;
procedure TForm1.FormResize(Sender: TObject);
begin
pnl1.Width := ClientWidth div 2;
end;
But when I set Constrains for TFrame1, for example TFrame1.Contraints.MinWidth := 100 and maximize and restore the form, then the form won't return to its previous state. Regardless of frame size, form size or constrains values, it always ends the same way. In my case default form has 300 width and after maximize and restore it ends with 1062. However without Constraints or FormResize it works. Can someone explain this strange behavior?
I am using a TFlowLayout to display a number of boxes.
When the screen is resized the FlowLayout adjusts the number of boxes per line automatically.
However I want to adjust the height of the surrounding element (TTreeViewItem) automatically.
I achieved this by adding an event:
procedure TDeviceTreeView.DeviceTreeViewResize(Sender: TObject);
begin
height := ChildrenRect.Height;
end;
This works halfways: the size is adjusted to grow when the elements in the flow layout need more lines.
However it never shrinks.
I know why. If you look at TControl.GetChildrenRect you will see:
function TControl.GetChildrenRect: TRectF;
var
I: Integer;
Control: TControl;
begin
Result := AbsoluteRect;
{ children }
if not (ClipChildren or SmallSizeControl) and (FControls <> nil) then
for I := GetFirstVisibleObjectIndex to GetLastVisibleObjectIndex - 1 do
begin
Control := FControls[I];
if Control.Visible then
Result := UnionRect(Result, Control.GetChildrenRect);
end
end;
Note that the base rectangle is:
Result := AbsoluteRect;
And from that it will loop through the child controls, always adding (union) to the first rect.
This causes the behavior you are experiencing: if the ChildControl's rect surpasses the FlowLayout's rect it increases, but will never decrease because the FlowLayout.AbsoluteRect is the starting rect in the function.
What you can do to solve that in a simple way is calculating the "ChildRect" yourself.
procedure TDeviceTreeView.DeviceTreeViewResize(Sender: TObject);
var childrenRect: TRectF;
begin
if ((csLoading in FlowLayout1.ComponentState) = False) then // You might want to check for csLoading to avoid unecessary calls to resize
begin
childrenRect := TRectF.Empty;
for i := 0 to FlowLayout1.ControlsCount - 1 do
childrenRect := TRectF.Union(childrenRect, FlowLayout1.Controls[i].ChildrenRect);
FlowLayout1.Height := childrenRect.Height;
end;
end;
You need to set TTreeViewItem property Align:alTop. In FMX it looks like: TTreeViewItem.Align:=talignlayout(1);
How do you get the label height to automatically adjust when resizing the form? All of the properties are set. Align is top. Autosize is true. Word wrap is true.
When I change the form size the label adjust the caption fine. However, the actual label will not resize its height.
This leaves a gap when the form width is increasing or it leaves the bottom part of the caption unreadable. Makes it ugly when you have controls below the label that should move up or down depending on the label's height.
I would hate to do this using the form's resize event. Too bad there is no form "resize end" event.
Any help? Thanks.
If I recall correctly, with Autosize set to true, the height of the label is automatically set to the actual height of the text in Caption.
You might try setting Autosize to false and see how that works for you.
I've solved by inheriting from tlabel.
there is a bug with the autosize in this case (autosize, wordwrap and alTop)
to make it recalculate it size you need to:
AutoSize := false;
AutoSize := true;
so you can override the resize procedure like that:
procedure TResizableLabel.Resize;
begin
AutoSize := false;
AutoSize := true;
end;
however if you will do it on every resize it will shrink the width also, so you will lose the width of the parent from alTop, in case it is just aligned left it will probably be ok, but if you want center or right alignment you will need a better solution.
this is the full solution, it will call the autosize only when needed:
TResizableLaber = class(TLabel)
protected
FTextHeight, FTextWidth : integer;
function GetCaption : TCaption;
procedure SetCaption(ACaption : TCaption);
function GetFont : TFont;
procedure SetFont(AFont : TFont);
public
procedure Resize; override;
property Caption : TCaption read GetCaption write SetCaption;
property Font : TFont read GetFont write SetFont;
end;
implementation
procedure TResizableLaber.Resize;
var
num : double;
begin
inherited;
if AutoSize then
begin
if (FTextHeight = 0) or (FTextWidth = 0) then
begin
//lazy evaluation, we re evaluate every time the caption or font changes
FTextWidth := Utills.GetTextWidth(Caption, Font);
FTextHeight := Utills.GetTextHeight(Caption,Font);
end;
//TODO: there is still one bug here, set alCenter and make the last word long enough so it cant always wrapped to the line before, even though there is globally enough space
num := ( Height / FTextHeight) - (FTextWidth /Width );
//if num is greater then 1 it means we need an extra line, if it is lower then zero it means there is an extra line
if (num > 1) or (num < 0) then
begin
//just doing this all the time will cause it to really resize and will break alTop matching the whole space
AutoSize := false;
AutoSize := true;
end;
end;
end;
function TResizableLaber.GetCaption : TCaption;
begin
Result := inherited Caption;
end;
procedure TResizableLaber.SetCaption(ACaption : TCaption);
begin
FTextWidth := Utills.GetTextWidth(ACaption, Self.Font);
FTextHeight := Utills.GetTextHeight(ACaption,Self.Font);
inherited Caption := ACaption;
end;
function TResizableLaber.GetFont : TFont;
begin
Result := inherited Font;
end;
procedure TResizableLaber.SetFont(AFont : TFont);
begin
FTextWidth := Utills.GetTextWidth(Caption, AFont);
FTextHeight := Utills.GetTextHeight(Caption,AFont);
inherited Font := AFont;
end;
class function Utills.GetTextHeight(const Text:String; Font:TFont) : Integer;
var
bitmap: TBitmap;
begin
bitmap := TBitmap.Create;
try
bitmap.Canvas.Font := Font;
Result := bitmap.Canvas.TextHeight(Text);
finally
bitmap.Free;
end;
end;
class function Utills.GetTextWidth(const Text:String; Font:TFont) : Integer;
var
bitmap: TBitmap;
begin
bitmap := TBitmap.Create;
try
bitmap.Canvas.Font := Font;
Result := bitmap.Canvas.TextWidth(Text);
finally
bitmap.Free;
end;
end;
I've spent quite some time to get both the wordwrap and the height of a series of labels right. The previous answer (thanks ndori), using the pointless-looking solution of first making Autosize false, followed by setting it to true is the solution!
Below my code for publishing a (long) series of labels, where the caption text is generated somewhere else and can be as short as one character up to several lines of text. So, I need a fixed label width, active wordwrap and a constant white space between all different labels.
When resizing the form the label.width (arbitrary set to 560 below) may be adjusted to fit the new form when resizing. I think the real problem is getting the label heights correctly displayed.
{ AL[] = global variable: array of TLabel
{ AL[].caption (the text) is delivered elsewhere, and can be short or long (= multiline text)
{ N_ActiveLabels = global integer variable: # of active labels to publish }
procedure PublishListOfLabels;
var
i : integer;
begin
AL[0].Top := 15; // or whatever
AL[0].Visible := true;
AL[0].Width := 560; // (re)set this here as otherwise the wordwrap makes
// the label text a long narrow column!
AL[0].AutoSize := false; // THIS IS REQUIRED!
AL[0].AutoSize := true; // THIS IS REQUIRED!
if N_ActiveLabels > 1 then begin
for i := 1 to N_ActiveLabels -1 do begin
AL[i].Visible := true;
AL[i].Width := 560;
AL[i].AutoSize := false;
AL[i].AutoSize := true;
AL[i].Top := AL[i-1].Top + AL[i-1].Height + 18;
// 18 was chosen as vertical white space between any two labels
end;
end;
end;
I found repainting (or refreshing) of the labels not needed.
I also encountered solutions like:
H := AL[i].Canvas.TextHeight(AL[i].caption);
where H is supposed to contain the real height of AL[i] (after filling its caption with text and calling PublishListOfLabels. it is NOT working.
I mention this as this solution has been proposed at several other places dealing with the same issue (getting a correct TLabel height).
[I use Berlin 10.1 - perhaps later versions have solved the Autosize.false /.true aberation]
I am creating a number of dynamically created THTMLabels but after these are created,when I try to get it's height,it always return the default height value.
Here is my code:
for i := 0 to ASentencePtr^.MUS.Count - 1 do
begin
j := Random(slTemp.Count);
sSen := ASentencePtr^.MUS.Strings[StrToInt(slTemp.Strings[j])] + ' / ';
THTMLabel.Create(Self).Name := 'lblSen_' + slTemp.Strings[j];
with THTMLabel(FindComponent('lblSen_' + slTemp.Strings[j])) do
begin
Font.Size := 18;
Font.Style := [fsBold];
Parent := FlowPanel1;
Width := Parent.Width;
Cursor := crHandPoint;
DragMode := dmAutomatic;
ControlStyle := ControlStyle + [csDisplayDragImage];
HTMLText.Add(sSen);
Autosizing := True;
end;
slTemp.Delete(j);
end;
Now when I try to access THTMLabel(FindComponent('lblSen_0')).Height, it returns only the default value which is 17. Where have I gone wrong? Any thoughts anyone? Any help is greatly appreciated, thanks.
We had the same problems but managed to solve them with the THTMLStaticText component and this function that calculates the height when dynamically (height) adjusted:
function CalculateDynamicHeight( aLabel: TLabel; htmlStaticText: THTMLStaticText): Integer;
var
lRect : TRect;
lText : string;
begin
lRect := Rect( 0, 0, htmlStaticText.Width, 0);
lText := htmlStaticText.Text;
aLabel.Caption := htmlStaticText.Text;
aLabel.Font := htmlStaticText.Font;
aLabel.Canvas.Font := htmlStaticText.Font;
aLabel.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
aLabel.Height := lRect.Bottom;
Result := lRect.Bottom;
end;
The function requires a TLabel component, used exclusively for calculation purposes (you can set the visibility to false). The htmlStaticText component should have AutoSize set to true (in our case AutoSizeType is set to asVertical) and the htmlStaticText.Text should be present when calling the function.
I think THTMLLabel is part of the Jedi Library, itsn't? This problem is weird. I don't know the implementation of this control, but if you are having this problem, then there's may be a very bad implementation of the AutoSize Property.
If you can't inspect and fix the source of this control, try to use the BoundsRect property to get the height:
LabelHeight := THTMLabel(FindComponent('lblSen_0')).BoundsRect.Bottom;
I have created one TScrollBox. I have added the Label and Edit Box on it dynamically on Button click. For setting the location of component i have used the height,width,left,top property of components.
But when Scroll Bar gets appeared on screen after 5 components added, the next components location gets disturbed. and the next component is not placed in synchronous manner on ScrollBox.
The Top coordinate for controls placed on a ScrollBox need to take into account the amount of "scroll" that already took place. If you add the controls all at once this is not a problem, because the ScrollBox doesn't get the chance to "scroll".
If you add controls to the ScrollBox after it got a chance to "scroll", you need to take into account the amount of vertical "scroll" that took place. Here's a sample piece of code that will add labels to ScrollBox1, taking vertical scroll into account so controls don't overlap each other. Here I'm using the form's "Tag" property to hold the Top for the next control added, and I'm also using Tag to generate unique names for the labels (so you can see they're going into the ScrollBox at the correct coordinates).
procedure TForm31.Button1Click(Sender: TObject);
var L: TLabel;
begin
L := TLabel.Create(Self);
L.Caption := 'Test: ' + IntToStr(Tag);
L.Parent := ScrollBox1;
L.Top := Tag + ScrollBox1.VertScrollBar.Size - ScrollBox1.VertScrollBar.Position;
Tag := Tag + L.Height;
end;
An other approach I sometimes used is to keep track of the last control added and base the coordinates for the new control on the coordinates of that last added control:
var LastControl: TControl;
procedure TForm31.Button1Click(Sender: TObject);
var L: TLabel;
begin
L := TLabel.Create(Self);
L.Caption := 'Test: ' + IntToStr(Tag);
L.Parent := ScrollBox1;
if Assigned(LastControl) then
L.Top := LastControl.Top + LastControl.Height
else
L.Top := 0;
Tag := Tag + L.Height;
LastControl := L;
end;
And yet an other approach would be to find the lowest control and add your control based on it's coordinates:
procedure TForm31.Button1Click(Sender: TObject);
var L: TLabel;
Bottom, TestBottom: Integer;
i: Integer;
begin
// Find "Bottom"
Bottom := 0;
for i:=0 to ScrollBox1.ControlCount-1 do
with ScrollBox1.Controls[i] do
begin
TestBottom := Top + Height;
if TestBottom > Bottom then
Bottom := TestBottom;
end;
L := TLabel.Create(Self);
L.Caption := 'Test: ' + IntToStr(Tag);
L.Parent := ScrollBox1;
L.Top := Bottom;
Tag := Tag + L.Height;
end;