Dynamic Fontsize for TextBlock with wrapping - c#

I have a TextBlock with a fixed size thats wrapping text. sometimes short sometimes long.
If the text is getting to long it isnt displayed entirely like this
How can i make the Fontsize flexible to make the text fit the TextBox with static size?

My solution is the following:
Set the fontsize to a value, than which you don't want any bigger.
The ActualHeight of the TextBlock changes, when you change the font size or when the content is changed. I built the solution based upon this.
You should create an event handler for the SizeChanged event and write the following code to it.
private void MyTextBlock_SizeChanged(object sender, SizeChangedEventArgs e)
{
double desiredHeight = 80; // Here you'll write the height you want the text to use
if (this.MyTextBlock.ActualHeight > desiredHeight)
{
// You want to know, how many times bigger the actual height is, than what you want to have.
// The reason for Math.Sqrt() is explained below in the text.
double fontsizeMultiplier = Math.Sqrt(desiredHeight / this.MyTextBlock.ActualHeight);
// Math.Floor() can be omitted in the next line if you don't want a very tall and narrow TextBox.
this.MyTextBlock.FontSize = Math.Floor(this.MyTextBlock.FontSize * fontsizeMultiplier);
}
this.MyTextBlock.Height = desiredHeight; // ActualHeight will be changed if the text is too big, after the text was resized, but in the end you want the box to be as big as the desiredHeight.
}
The reason why I used the Math.Sqrt() is that if you set the font size to half as big as before, then the area that the font will use, will be one quarter the size, then before (because it became half as wide and half as tall as before). And you obviously want to keep the width of the TextBox and only change the height of it.
If you were lucky, the font size will be appropriate after this method gets executed once. However, depending on the text that gets re-wrapped after the font size change, you might be so "unlucky", that the text will be one line longer than you would want it to be.
Luckily the event handler will be called again (because you changed the font size) and the resizing will be done again if it is still too big.
I tried it, it was fast and the results looked good.
However, I can imagine, that in a really unlucky choice of text and height, the correct font size would be reached after several iterations. This is why I used Math.Floor(). All in all, it doesn't matter much if the font size is in the end 12.34 or 12 and this way I wouldn't be concerned about an "unlucky" text, which will take too long to render.
But I think Math.Floor() can be omitted if you don't want to have a really tall text box (like 2000 pixels) with a lot of text.

Here's a full solution including an option to set maxheight / maxwidth and and it is calculated straight on render:
public class TextBlockAutoShrink : TextBlock
{
private double _defaultMargin = 6;
private Typeface _typeface;
static TextBlockAutoShrink()
{
TextBlock.TextProperty.OverrideMetadata(typeof(TextBlockAutoShrink), new FrameworkPropertyMetadata(new PropertyChangedCallback(TextPropertyChanged)));
}
public TextBlockAutoShrink() : base()
{
_typeface = new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch, this.FontFamily);
base.DataContextChanged += new DependencyPropertyChangedEventHandler(TextBlockAutoShrink_DataContextChanged);
}
private static void TextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var t = sender as TextBlockAutoShrink;
if (t != null)
{
t.FitSize();
}
}
void TextBlockAutoShrink_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
FitSize();
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
FitSize();
base.OnRenderSizeChanged(sizeInfo);
}
private void FitSize()
{
FrameworkElement parent = this.Parent as FrameworkElement;
if (parent != null)
{
var targetWidthSize = this.FontSize;
var targetHeightSize = this.FontSize;
var maxWidth = double.IsInfinity(this.MaxWidth) ? parent.ActualWidth : this.MaxWidth;
var maxHeight = double.IsInfinity(this.MaxHeight) ? parent.ActualHeight : this.MaxHeight;
if (this.ActualWidth > maxWidth)
{
targetWidthSize = (double)(this.FontSize * (maxWidth / (this.ActualWidth + _defaultMargin)));
}
if (this.ActualHeight > maxHeight)
{
var ratio = maxHeight / (this.ActualHeight);
// Normalize due to Height miscalculation. We do it step by step repeatedly until the requested height is reached. Once the fontsize is changed, this event is re-raised
// And the ActualHeight is lowered a bit more until it doesnt enter the enclosing If block.
ratio = (1 - ratio > 0.04) ? Math.Sqrt(ratio) : ratio;
targetHeightSize = (double)(this.FontSize * ratio);
}
this.FontSize = Math.Min(targetWidthSize, targetHeightSize);
}
}
}

Related

Embed Any Control with UltraControlContainerEditor does not work for Image column

I want to display a picture in another scaling mode in an UltraGridCell. The mode should scale the image to the cell height and clip the rest of the image from the right to fit into the cell. This is easy if I could just draw it myself as the EmbeddableImageRenderer does not allow me to set its scaling behaviour (I am not talking about MaintainAspectRatio as I still want to maintain the aspect ratio).
I tried it with the tutorial to embed any control in a cell. And it is working fine with the given example of a TrackBar (and in my tiny testing project also with a ProgressBar as RendererControl). But it appears not to work with columns that are displaying images.
As a DataSource I have a list of my own class with an Image property which is displayed in the grid. As Editor-/RendererControl I set two regular PictureBoxes.
Any suggestions to solve the main problem of scaling the image or to set any control to the picture column (that would then deal with the scaling)?
I can't see any reason why the UltraControlContainerEditor shouldn't work with an image column, provided that the control you are using has a property that takes an Image and that you specify the correct PropertyName on the editor. But that's probably not the most efficient approach, anyway.
A better approach would be to use a DrawFilter to draw the image yourself.
public class ImageScalingDrawFilter : IUIElementDrawFilter
{
bool IUIElementDrawFilter.DrawElement(DrawPhase drawPhase, ref UIElementDrawParams drawParams)
{
switch (drawPhase)
{
case DrawPhase.BeforeDrawImage:
ImageUIElement imageElement = (ImageUIElement)drawParams.Element;
Image image = imageElement.Image;
int availableHeight = drawParams.Element.RectInsideBorders.Height;
float ratio = (float)availableHeight / (float)image.Height;
float newHeight = image.Height * ratio;
float newWidth = image.Width * ratio;
Rectangle rect = new Rectangle(
imageElement.Rect.X,
imageElement.Rect.Y,
(int)(newWidth),
(int)(newHeight)
);
// Draw the scaled image.
drawParams.Graphics.DrawImage(image, rect);
// This tells the grid not to draw the image (since we've already drawn it).
return true;
}
return false;
}
DrawPhase IUIElementDrawFilter.GetPhasesToFilter(ref UIElementDrawParams drawParams)
{
UIElement element = drawParams.Element;
// Look for an ImageUIElement
if (element is ImageUIElement)
{
// Presumably, we only want to this images in cells
// and not every image in the entire grid, so make sure it's in a cell.
CellUIElement cellElement = element.GetAncestor(typeof(CellUIElement)) as CellUIElement;
if (null != cellElement)
{
// We could also limit this to a particular column or columns.
switch (cellElement.Cell.Column.Key)
{
case "Image":
return DrawPhase.BeforeDrawImage;
}
}
}
return DrawPhase.None;
}
}
You assign the DrawFilter to the grid like so:
private void Form1_Load(object sender, EventArgs e)
{
this.ultraGrid1.DrawFilter = new ImageScalingDrawFilter();
}

ScrollViewer doesn't scroll for custom panel

I am making my own custom panel, which is supposed to scroll vertically when the content does not fit the available space, so i put it in a ScrollViewer.
Right now i can't get the ScrollViewer to activate the scrollbar when the panel inside is bigger then the ScrollViewer itself.
The permille functions get attached properties telling how big the childs have to be compared to the available size (without scrolling), aka the ViewPort.
As the size passed in MeasureOverride passes infinite, i don't think i can use the permille functions there.
That is why i measure my children in ArrangeOverride (not best practice, i guess) but that way the scrollviewer doesn't scroll.
How do i get this to work?
My XAML code:
<ScrollViewer>
<controls:TilePanel x:Name="TilePanel" PreviewMouseLeftButtonDown="TilePanel_PreviewMouseLeftButtonDown" PreviewMouseLeftButtonUp="TilePanel_PreviewMouseLeftButtonUp"
PreviewMouseMove="TilePanel_PreviewMouseMove" DragEnter="TilePanel_DragEnter" Drop="TilePanel_Drop" AllowDrop="True" />
</ScrollViewer>
My Custom Panel Class:
/// <summary>
/// A Panel Showing Tiles
/// </summary>
public class TilePanel : PermillePanel
{
public TilePanel()
{
}
protected override Size MeasureOverride(Size constraint)
{
//here constraint width or height can be infinite.
//as tiles are a permille of that height, they too can be infinite after measuring
//this is unwanted behavior, so we measure in the ArrangeOverride method
if (constraint.Width == double.PositiveInfinity)
{
return new Size(0, constraint.Height);
}
else if (constraint.Height == double.PositiveInfinity)
{
return new Size(constraint.Width, 0);
}
else
{
return constraint;
}
}
protected override Size ArrangeOverride(Size arrangeSize)
{
//return base.ArrangeOverride(arrangeSize);
foreach (FrameworkElement child in InternalChildren)
{
Size availableSize = new Size();
//set the width and height for the child
availableSize.Width = arrangeSize.Width * TilePanel.GetHorizontalPermille(child) / 1000;
availableSize.Height = arrangeSize.Height * TilePanel.GetVerticalPermille(child) / 1000;
child.Measure(availableSize);
}
// arrange the children on the panel
// fill lines horizontally, when we reach the end of the current line, continue to the next line
Size newSize = new Size(arrangeSize.Width, arrangeSize.Height);
double xlocation = 0;
double ylocation = 0;
double ystep = 0;
double maxYvalue = 0;
foreach (FrameworkElement child in InternalChildren)
{
double endxlocation = xlocation + child.DesiredSize.Width;
double constrainedWidth = arrangeSize.Width * TilePanel.GetHorizontalPermille(child) / 1000;
double constrainedHeight = arrangeSize.Height * TilePanel.GetVerticalPermille(child) / 1000;
if (TilePanel.GetVerticalPermille(child) != 0 && TilePanel.GetHorizontalPermille(child) != 0)
{
//horizontal overflow -> next line
if (endxlocation >= this.DesiredSize.Width *1.01)
{
ylocation += ystep;
xlocation = 0;
}
}
Rect rect = new Rect(xlocation, ylocation, constrainedWidth, constrainedHeight);
child.Arrange(rect);
xlocation += constrainedWidth;
ystep = Math.Max(ystep, constrainedHeight);
maxYvalue = Math.Max(maxYvalue, ystep + constrainedHeight);
}
if (maxYvalue > newSize.Height)
{
newSize.Height = maxYvalue;
}
return newSize;
}
}
Calling Measure() from within ArrangeOverride() will cause problems. The framework detects this and forces a remeasure. Set a tracepoint in MeasureOverride(), and I'll bet you'll see that it keeps getting called over and over again, even though the layout hasn't changed1.
If you absolutely have to call Measure() from ArrangeOverride(), you will need to do so conditionally such that it only forces a remeasure when the available size actually changes since the last call to Measure(). Then, you'll effectively end up with two measure + arrange passes any time the layout is invalidated, as opposed to just one. However, such an approach is hacky, and I would advise sticking to the best practice of only measuring within MeasureOverride().
1Interestingly, your UI may still respond to input, despite this apparent "infinite loop" in the layout.
If you want to use a custom Panel inside a ScrollViewer then you must add the code that does the actual scrolling. You can do that by implementing the IScrollInfo Interface in your custom Panel.
You can find a tutorial that explains this interface and provides an example code imeplementation in the WPF Tutorial - Implementing IScrollInfo page on the Tech Pro website. It's a fairly simple procedure and looks a tiny bit like this:
public void LineDown() { SetVerticalOffset(VerticalOffset + LineSize); }
public void LineUp() { SetVerticalOffset(VerticalOffset - LineSize); }
public void MouseWheelDown() { SetVerticalOffset(VerticalOffset + WheelSize); }
public void MouseWheelUp() { SetVerticalOffset(VerticalOffset - WheelSize); }
public void PageDown() { SetVerticalOffset(VerticalOffset + ViewportHeight); }
public void PageUp() { SetVerticalOffset(VerticalOffset - ViewportHeight); }
...

AutoSize text in a fixed-sized label to avoid text jumping to second line

I have a label with a fixed size, approximately 100 pixel width and 20 pixel height.
When I place a long string into the label, the text wraps to the second line but I cannot see the second line because the size of the label is fixed.
Instead of wrapping to the second line, I want the fontsize to shrink so that the string is displayed on a single line in the label. Does anyone know of a simple way to do this?
EDIT:
The below code is working for me (most of the time). I didn't want to do anything recursive. There are times when the text still spills over to another line. I assume because I can't truly use the entire width of the label. How do I compensate for that?
private void Label_TextChanged(object sender, EventArgs e)
{
Label label = sender as Label;
if (label != null && label.Text.Length != 0)
{
SizeF size = new SizeF();
using (Graphics g = label.CreateGraphics())
{
size = g.MeasureString(label.Text, label.Font);
}
Single x = (label.Width) / size.Width;
Single y = (label.Height) / size.Height;
Single scaler = x > y ? y : x;
using (Font font = label.Font)
{
label.Font = new Font(font.Name, font.SizeInPoints * scaler);
}
}
}
This is easy to do. Use Graphics.MeasureString(...) to determine the width required for your string, then progressively make the font smaller and smaller until the width required for the string is equal to or less than the width of your label.
You can use the System.Windows.Forms.Label.TextChanged-event and check the length of the string.
private void Label_TextChanged(object sender, EventArgs e){
System.Windows.Forms.Label label = sender as label;
if(label != null){
//check text-length and if necessary resize it
}
}
See here for TextChanged-event.
There is the FontHeight-property, which might do your trick. See here for a reference.
And if everything fails, derive from System.Windows.Forms.Label and create your own custom label.

Why is my TextBox.Width value not accepting the value I'm trying to assign it?

I am trying to evenly space dynamically created controls by giving them a width and height that is a fraction of the width and height of the container control.
However, although my functions calculating the dimensions are correct, for some reason the assignment to the Width property is being stubbornly refused. The WidthToUse() function is returning 19, yet when an attempt is made to assign that vaue to the dynamically created TextBox, it remains at...630! Why 630 to begin with, and why does it refuse to be assigned to?
Even the height, although "working" doesn't work quite right - there are too many controls or not enough space on the panel to accommodate all of the controls using this math.
Here's my code:
int WidthToUse = getTextBoxWidthToUse(tableLayoutPanelGreatGooglyMoogly.Width);
int HeightToUse = getControlHeightToUse(tableLayoutPanelGreatGooglyMoogly.Height);
TextBox txtbx = new TextBox();
txtbx.Parent = tableLayoutPanelGreatGooglyMoogly;
txtbx.Margin = new Padding();
txtbx.Dock = DockStyle.Fill;
txtbx.AutoSize = false;
txtbx.Width = WidthToUse; // WidthToUse is 19, but txtbx.Width is 630 both before AND after this assignment!
txtbx.Height = HeightToUse; // HeightToUse is 27
private static int getControlHeightToUse(int theDynPanelHeight) {
return (theDynPanelHeight / NUMBER_OF_ROWS);
}
private static int getTextBoxWidthToUse(int theDynPanelWidth) {
return (theDynPanelWidth / 32);
}
private static int getLabelWidthToUse(int theDynPanelWidth) {
return ((theDynPanelWidth / 64) * 3);
}
DockStyle.Fill is a property describing the size of a control relative to the size of it's container. A control will resize to fit all of the empty space in it's parent container with this property set.
Because you have the Dock property set to Fill would be my guess.

Autoscale Font in a TextBox Control so that its as big as possible and still fits in text area bounds

I need a TextBox or some type of Multi-Line Label control which will automatically adjust the font-size to make it as large as possible and yet have the entire message fit inside the bounds of the text area.
I wanted to see if anyone had implemented a user control like this before developing my own.
Example application: have a TextBox which will be half of the area on a windows form. When a message comes in which is will be approximately 100-500 characters it will put all the text in the control and set the font as large as possible. An implementation which uses Mono Supported .NET libraries would be a plus.
If know one has implemented a control already... If someone knows how to test if a given text completely fits inside the text area that would be useful for if I roll my own control.
Edit: I ended up writing an extension to RichTextBox. I will post my code shortly once i've verified that all the kinks are worked out.
I had to solve the same basic problem. The iterative solutions above were very slow. So, I modified it with the following. Same idea. Just uses calculated ratios instead of iterative. Probably, not quite as precise. But, much faster.
For my one-off need, I just threw an event handler on the label holding my text.
private void PromptLabel_TextChanged(object sender, System.EventArgs e)
{
if (PromptLabel.Text.Length == 0)
{
return;
}
float height = PromptLabel.Height * 0.99f;
float width = PromptLabel.Width * 0.99f;
PromptLabel.SuspendLayout();
Font tryFont = PromptLabel.Font;
Size tempSize = TextRenderer.MeasureText(PromptLabel.Text, tryFont);
float heightRatio = height / tempSize.Height;
float widthRatio = width / tempSize.Width;
tryFont = new Font(tryFont.FontFamily, tryFont.Size * Math.Min(widthRatio, heightRatio), tryFont.Style);
PromptLabel.Font = tryFont;
PromptLabel.ResumeLayout();
}
I haven't seen an existing control to do this, but you can do it the hard way by using a RichTextBox and the TextRenderer's MeasureText method and repeatedly resizing the font. It's inefficient, but it works.
This function is an event handler for the 'TextChanged' event on a RichTextBox.
An issue I've noticed:
When typing, the text box will scroll to the current caret even if scrollbars are disabled. This can result in the top line or left side getting chopped off until you move back up or left with the arrow keys. The size calculation is correct assuming you can get the top line to display at the top of the text box. I included some scrolling code that helps sometimes (but not always).
This code assumes word wrap is disabled. It may need modification if word wrap is enabled.
The code:
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, uint wMsg, int wParam, uint lParam);
private static uint EM_LINEINDEX = 0xbb;
private void richTextBox1_TextChanged(object sender, EventArgs e)
{
// If there's no text, return
if (richTextBox1.TextLength == 0) return;
// Get height and width, we'll be using these repeatedly
int height = richTextBox1.Height;
int width = richTextBox1.Width;
// Suspend layout while we mess with stuff
richTextBox1.SuspendLayout();
Font tryFont = richTextBox1.Font;
Size tempSize = TextRenderer.MeasureText( richTextBox1.Text, richTextBox1.Font);
// Make sure it isn't too small first
while (tempSize.Height < height || tempSize.Width < width)
{
tryFont = new Font(tryFont.FontFamily, tryFont.Size + 0.1f, tryFont.Style);
tempSize = TextRenderer.MeasureText(richTextBox1.Text, tryFont);
}
// Now make sure it isn't too big
while (tempSize.Height > height || tempSize.Width > width)
{
tryFont = new Font(tryFont.FontFamily, tryFont.Size - 0.1f, tryFont.Style);
tempSize = TextRenderer.MeasureText(richTextBox1.Text, tryFont);
}
// Swap the font
richTextBox1.Font = tryFont;
// Resume layout
richTextBox1.ResumeLayout();
// Scroll to top (hopefully)
richTextBox1.ScrollToCaret();
SendMessage(richTextBox1.Handle, EM_LINEINDEX, -1, 0);
}
The solution i came up with was to write a control which extends the standard RichTextBox control.
Use the extended control in the same way you would a regular RichTextBox control with the following enhancements:
Call the ScaleFontToFit() method after resizing or text changes.
The Horizontal Alignment field can be used to center align the text.
The Font attributes set in the designer will be used for the entire region. It is not possible to mix fonts as they will changed once the ScaleFontToFit method is called.
This control combines several techniques to determine if the text still fits within it's bounds. If the text area is multiline, it detects if scrollbars are visible. I found a clever way to detect whether or not the scrollbars are visible without requiring any winapi calls using a clever technique I found on one of Patrick Smacchia's posts.. When multiline isn't true, vertical scrollbars never appear so you need to use a different technique which relies on rendering the text using a the Graphics object. The Graphic rendering technique isn't suitable for Multiline boxes because you would have to account for word wrapping.
Here are a few snippets which shows how it works (link to source code is provided below). This code could easily be used to extend other controls.
/// <summary>
/// Sets the font size so the text is as large as possible while still fitting in the text
/// area with out any scrollbars.
/// </summary>
public void ScaleFontToFit()
{
int fontSize = 10;
const int incrementDelta = 5; // amount to increase font by each loop iter.
const int decrementDelta = 1; // amount to decrease to fine tune.
this.SuspendLayout();
// First we set the font size to the minimum. We assume at the minimum size no scrollbars will be visible.
SetFontSize(MinimumFontSize);
// Next, we increment font size until it doesn't fit (or max font size is reached).
for (fontSize = MinFontSize; fontSize < MaxFontSize; fontSize += incrementDelta)
{
SetFontSize(fontSize);
if (!DoesTextFit())
{
//Console.WriteLine("Text Doesn't fit at fontsize = " + fontSize);
break;
}
}
// Finally, we keep decreasing the font size until it fits again.
for (; fontSize > MinFontSize && !DoesTextFit(); fontSize -= decrementDelta)
{
SetFontSize(fontSize);
}
this.ResumeLayout();
}
#region Private Methods
private bool VScrollVisible
{
get
{
Rectangle clientRectangle = this.ClientRectangle;
Size size = this.Size;
return (size.Width - clientRectangle.Width) >= SystemInformation.VerticalScrollBarWidth;
}
}
/**
* returns true when the Text no longer fits in the bounds of this control without scrollbars.
*/
private bool DoesTextFit()
{
if (VScrollVisible)
{
//Console.WriteLine("#1 Vscroll is visible");
return false;
}
// Special logic to handle the single line case... When multiline is false, we cannot rely on scrollbars so alternate methods.
if (this.Multiline == false)
{
Graphics graphics = this.CreateGraphics();
Size stringSize = graphics.MeasureString(this.Text, this.SelectionFont).ToSize();
//Console.WriteLine("String Width/Height: " + stringSize.Width + " " + stringSize.Height + "form... " + this.Width + " " + this.Height);
if (stringSize.Width > this.Width)
{
//Console.WriteLine("#2 Text Width is too big");
return false;
}
if (stringSize.Height > this.Height)
{
//Console.WriteLine("#3 Text Height is too big");
return false;
}
if (this.Lines.Length > 1)
{
//Console.WriteLine("#4 " + this.Lines[0] + " (2): " + this.Lines[1]); // I believe this condition could be removed.
return false;
}
}
return true;
}
private void SetFontSize(int pFontSize)
{
SetFontSize((float)pFontSize);
}
private void SetFontSize(float pFontSize)
{
this.SelectAll();
this.SelectionFont = new Font(this.SelectionFont.FontFamily, pFontSize, this.SelectionFont.Style);
this.SelectionAlignment = HorizontalAlignment;
this.Select(0, 0);
}
#endregion
ScaleFontToFit could be optimized to improve performance but I kept it simple so it'd be easy to understand.
Download the latest source code here. I am still actively working on the project which I developed this control for so it's likely i'll be adding a few other features and enhancements in the near future. So, check the site for the latest code.
My goal is to make this control work on Mac using the Mono framework.
I had a similar requirement for a text box in a panel on a windows form hosted window. (I injected the panel onto the existing form). When the size of the panel changes (in my case) the text would resize to fit the box. Code
parentObject.SizeChanged += (sender, args) =>
{
if (textBox1.Text.Length > 0)
{
int maxSize = 100;
// Make a Graphics object to measure the text.
using (Graphics gr = textBox1.CreateGraphics())
{
for (int i = 1; i <= maxSize; i++)
{
using (var test_font = new Font(textBox1.Font.FontFamily, i))
{
// See how much space the text would
// need, specifying a maximum width.
SizeF text_size =
TextRenderer.MeasureText(
textBox1.Text,
test_font,
new Size(textBox1.Width, int.MaxValue),
TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl);
try
{
if (text_size.Height > textBox1.Height)
{
maxSize = i - 1;
break;
}
}
catch (System.ComponentModel.Win32Exception)
{
// this sometimes throws a "failure to create window handle" error.
// This might happen if the TextBox is invisible and/or
// too small to display a toolbar.
// do whatever here, add/delete, whatever, maybe set to default font size?
maxSize = (int) textBox1.Font.Size;
}
}
}
}
// Use that font size.
textBox1.Font = new Font(textBox1.Font.FontFamily, maxSize);
}
};

Categories