Compress text to fit within a control's displayed width - c#

In my C# WinForms application, I have a control in which I display some text to the user on screen. For time being, assume it is a TextBox.
My requirement is if the text does not fully fit within the displayed width of the control, I want to keep reducing the font size or compress the text in some other way to fit the displayed width of the control.
I understand in extreme situations, the text may not be readable at all. But that's fine.
Can I get a code example how to achieve this?

To measure the width of the font you'll have to determine it using TextRenderer. The following code illustrates how to achieve this, and to resize the font in the textbox.
var text = "Some unnecessarily long, long, long string.";
var size = default(SizeF);
// SizeF size; // Use this if you're on an older version of C# without default
do
{
using (var font = new Font(textBox1.Font.Name, textBox1.Font.SizeInPoints))
{
size = TextRenderer.MeasureText(text, font);
if (size.Width <= textBox1.Width)
textBox1.Text = text;
else
{
textBox1.Text = "Won't fit";
textBox1.Font = new Font(font.Name, font.SizeInPoints - 1f);
}
}
} while (size.Width > textBox1.Width);
You may want to adjust the by how much the font size decreases if it ends up too small for your liking.

Related

Graphis.DrawString always uses the default font

I have a project in which I create an image with rotated text around an invisible circle.
The drawing in itself is working just fine. However, it seems that no matter the font I use, I always get the same result, which is I assume some low quality default font.
Here is the code :
Bitmap objBmpImage = new Bitmap(1000, 1000);
System.Drawing.Text.InstalledFontCollection installedFontCollection = new System.Drawing.Text.InstalledFontCollection();
FontFamily[] fontFamilies = installedFontCollection.Families;
System.Drawing.Font objFont = new System.Drawing.Font(fontFamilies.Where(x => x.Name == "Arial").FirstOrDefault(),10);
Graphics objGraphics = Graphics.FromImage(objBmpImage);
objGraphics.Clear(Color.Transparent);
float angle = (float)360.0 / (float)competences.Count();
objGraphics.TranslateTransform(500, 450);
objGraphics.RotateTransform(-90 - (angle / 3));
int nbComptetence = competences.Count();
int indexCompetence = 0;
foreach (T_Ref_Competence competence in competences)
{
byte r, g, b;
HexToInt(competence.T_Ref_CompetenceNiveau2.T_Ref_CompetenceNiveau1.Couleur, out r, out g, out b);
Brush brush = new System.Drawing.SolidBrush(Color.FromArgb(255,r,g,b));
if (indexCompetence * 2 < nbComptetence)
{
objGraphics.DrawString(competence.Nom, objFont, brush, 255, 0);
objGraphics.RotateTransform(angle);
}
else
{
objGraphics.RotateTransform(180);
objGraphics.RotateTransform(angle/2);
float textSize = objGraphics.MeasureString(competence.Nom, objFont).Width;
objGraphics.DrawString(competence.Nom, objFont, brush, -253 - textSize, 0);
objGraphics.RotateTransform(angle);
objGraphics.RotateTransform(-180);
objGraphics.RotateTransform(-angle / 2);
}
indexCompetence++;
}
I get the font using the installed families like this
System.Drawing.Text.InstalledFontCollection installedFontCollection = new System.Drawing.Text.InstalledFontCollection();
FontFamily[] fontFamilies = installedFontCollection.Families;
System.Drawing.Font objFont = new System.Drawing.Font(fontFamilies.Where(x => x.Name == "Arial").FirstOrDefault(),10);
I tried using other font but the result is always the same. Is there anything I am missing ? If not, what could be the reason ?
Thanks,
EDIT : To answer the question, what is it that I want exactly, consider this :
This image is a screenshot of a web site I am making. The chart in the middle was generated using charts.js, but its limitation force me to draw the text as a background image. It actually takes most of my screen so it can't really get much bigger than this. As you can see, the text font is pretty blurry and I would simply want it to be easier to read. I though the font was the problem, but I don't really know.
I am not really familiar with the whole image drawing part of C#, so if there are is better way to draw my text (which can change depending of many variables), I will gladly try other things.
Option 1: change text rendering
objGraphics.TextRenderingHint = TextRenderingHint.SingleBitPerPixel
Option 2: change the mode of anti aliasing
objGraphics.InterpolationMode=InterpolationMode.NearestNeighbor;
Option 3: change the DPI of the image
You'll get the best result if you scale the input image and then draw the text in higher DPI.
The default DPI for a Bitmap are 96. Probably the JS library exported with that setting.
If you want a smoother rendering of the font, you need to increase the DPI, e.g.
objBmpImage.SetResolution(1200,1200);
If you do so, you probably need to increase the number of pixels your Bitmap has.
If the "ugly" text just fitted the 1000x1000 picture, you now need 1000*1200/96=12500 pixels.
Before the change (using Arial 10 pt):
After the change (still using Arial 10 pt):
Note that the size in centimeters doesn't change. So it will still print well.

How does the size get calculated when using GetPreferredSize()?

I'm using label to display the message. I have calculated the size of the label by using GetPreferredSize() method. This method works fine when I didn't do any manipulation in sizing the label. But when, I reduce the width of the label, the text gets clipped. However, if I include a newline(\n) at the end of the text to be displayed, the text which gets clipped in previous case is displayed in the next line.
Here is what I tried.
label1.Text = "Are you sure you wish to cancel? \n You will permanently discard any information you have entered!";
label1.Font = new Font(new FontFamily("Calibri"), 15);
Size textSize = label1.GetPreferredSize(Size.Empty); //Works fine.
label1.Size = textSize;
textSize.Width -= 25;
label1.Size = textSize;// Text is clipped.
label1.Text = "Are you sure you wish to cancel? \n You will permanently discard any information you have entered! \n "; //Works fine again!
Refer to the image,
Label Text
If the method calculates the size of the label based on the contents, why does the content clipped in case2 and works good in case3? How does the width and height is related to? Can anyone explain what I'm missing?
Thanks,
Sindhu
Size sz = new Size(this.Width, Int32.MaxValue);
sz = TextRenderer.MeasureText(this.Text, this.Font, sz, TextFormatFlags.WordBreak);
this.Height = sz.Height;
where "this " is your control.
How big was text size before decreasing it by 25? Maybe it's just not enough space and cut off - try with a different number. And ... you certainly wanted to subtract and not assign?
Modify the "Anchor" setting to only anchor to the left or the right

Fitting text into a listView

I want to create a listView that shows users nicknames for a chat program. For that I created a new class that inherits from listViewItem.
What i want to do is, depending on the length of the nickname scale my font size.
I have read lots of articles about scaling but ALL of them depend on a graphics object and i have no clue how i get one of those ??? i tried it with a label and there it would be from the paint event but listView doesnt have such an event? so how do i scale this font ?
Q:
How do I get the right fontsize that the Nickname will fit into a specified rectangle ?
EDIT: Forgot to say I'm completly new to anything with grafic stuff i only used the Designer and set some properties.
You should set OwnerDraw property of the ListView to true, add draw item event handler like this:
listView1.DrawItem += listView1_DrawItem;
And here is a simple implementation of what you want so you can play with and tune it up:
void listView1_DrawItem(object sender, DrawListViewItemEventArgs e)
{
float emSize = e.Item.Font.Size;
Font font = new Font(e.Item.Font.FontFamily, emSize);
while(e.Graphics.MeasureString(e.Item.Text, e.Item.Font).Width>e.Item.Bounds.Width)
{
emSize--;
font = new Font(e.Item.Font.FontFamily, emSize);
e.Item.Font = font;
}
e.DrawText();
}
You see that you need to change the font size and measure the string you want to display so it fits in the cell completely. Presuming that if your current font size doesn't fit, you want to make it smaller.
I marked #Nikola answer right because it explained a lot but in my case i needed something way simpler and thanks to #TaW i also got the problem with the Graphics solved here my code snippet
public static Font getNewFont(Font origFont, string text, float maxWidth, Graphics g)
{
float emSize = origFont.Size;
Font font = origFont;
while (g.MeasureString(text, font).Width > maxWidth)
{
emSize--;
font = new Font(origFont.FontFamily, emSize);
}
return font;
}

How to get the HEIGHT of the Run or Paragraph

I found the Run or Paragraph in FlowDocument and now I need to know the HEIGHT of it.
i.e.
while (navigator.CompareTo(flowDocViewer.Document.ContentEnd) < 0)
{
TextPointerContext context = navigator.GetPointerContext(LogicalDirection.Backward);
Run run = navigator.Parent as Run;
// I need to get HEIGHT of Run in pixels somehow
Is it possible to do in fact?
Thank you!
A little function i am using. The input is a string containing a Section. You can easily render other blockelements like Paragraph.
You also can omit the second parameter of the Parse method.
The trick is not to measure the Paragraph, but the ViewBox which contains a RichTextBox. This is needed to actually render the Flowdocument. The ViewBox dynamically gets the size of the rtb. Maybe you even can do this without the ViewBox. I spent some time to figure this out and it works for me.
Note that Width of the RichTextBox is set to double.MaxValue. This means when you want to measure a single paragraph it has to be very long or everything is in one line. So this only makes sense when you know the Width of your output device. As this is a FlowDocument there is no Width, it flows ;)
I use this to paginate a FlowDocument where i know the paper size.
The returned Height is device independent units.
private double GetHeaderFooterHeight(string headerFooter)
{
var section = (Section)XamlReader.Parse(headerFooter, _pd.ParserContext);
var flowDoc = new FlowDocument();
flowDoc.Blocks.Add(section);
var richtextbox = new RichTextBox { Width = double.MaxValue, Document = flowDoc };
var viewbox = new Viewbox { Child = richtextbox };
viewbox.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
viewbox.Arrange(new Rect(viewbox.DesiredSize));
var size = new Size() { Height = viewbox.ActualHeight, Width = viewbox.ActualWidth };
return size.Height;
}

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