tags:

views:

1014

answers:

3

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.

Thanks in advance.

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.

+2  A: 

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);
}
Steven Richards
Thanks for your suggestion.
blak3r
+2  A: 

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.

blak3r
A: 

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();
    }
GinoA
The iterative approach is slow (especially if your application is in the TextChanged handler). I wanted the code to be easy to understand. There are certainly a few optimizations which could be added. The iterative approach becomes necessary if you want to handle multiline text box areas which I don't think yours does.
blak3r