User control with auto-height property

2019-02-19 18:34发布

问题:

I want to create user control that will display text.
I need a way to resize control at run-time so that it will adjust height to show all the text. I've created control that looks like this:

As You can see I have icon and text drawn using TextRenderer.DrawText. Unfortunately when I resize my control (only to left or right) my text is sometimes cut, ass shown below:

I'm measuring text using TextRenderer.MeasureText based on that I'm counting number of lines and then I'm drawing that text. Here is code I'm using:

[Designer(typeof(MyTextBoxDesigner))]
public partial class MyTextBox : UserControl
{
    public MyTextBox()
    {
        InitializeComponent();
    }

    [DefaultValue("Demo"), Description("Text of control"), Category("Appearance"),Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public override string Text
    {
        get
        {
            return base.Text;
        }
        set
        {
            if (base.Text == value) return;
            base.Text = value;
            Invalidate();
        }
    }

    Image _image;
    [Description("Image shown on the left side of the control"),
    Category("Appearance")]
    public Image Image
    {
        get
        {
            return _image;
        }
        set
        {
            if (_image == value) return;
            _image = value;
            Invalidate();
        }
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        int imagePadding = 0;
        if (_image != null)
        {
            imagePadding = 25;
            e.Graphics.DrawImage(_image,new Rectangle(5,5,16,16));
        }
        Size textSize1 = TextRenderer.MeasureText(e.Graphics, Text, Font);
        SizeF textSize2 = e.Graphics.MeasureString(Text, Font);

        Debug.WriteLine(textSize2);

        int maxTextWidth = Width - Padding.Left-Padding.Right-imagePadding;
        int lineHeight = textSize1.Height + 2;
        int numLines = 1;

        if (textSize1.Width > maxTextWidth)
        {
            numLines = textSize1.Width / maxTextWidth + 1;
        }

        Rectangle textRect = new Rectangle
        {
            Width = Width - Padding.Left-Padding.Right-imagePadding,
            Height = (numLines * lineHeight),
            X = Padding.Left+imagePadding,
            Y = 5
        };

        TextRenderer.DrawText(e.Graphics, Text, Font, textRect, ForeColor, TextFormatFlags.WordBreak | TextFormatFlags.Left | TextFormatFlags.Top);

        e.Graphics.DrawRectangle(Pens.CadetBlue, textRect);

        base.OnPaint(e);
    }
}

internal class MyTextBoxDesigner : ControlDesigner
{
    public override SelectionRules SelectionRules
    {
        get
        {
            //all minus bottom and top
            return (base.SelectionRules & ~(SelectionRules.BottomSizeable | SelectionRules.TopSizeable));
        }
    }
}

Ideally I'd like my control to auto-adjust height based on text length, so if text is short (or control long enough) in will be for example 20px height, but if text is long my control should be (for example) 40px height (2 lines of text).

How should I change my text measurement to calculate textRect correctly?
Should I update height of my control using Size property or is there a better way?

EDIT: Basically I want to draw text that can wrap inside rectangle, but has fixed width, so when text is longer that rectangle will adjust its height. Then based on that height I want to adjust control height.

回答1:

Here is an auto-height control. If you change the width of control, the height of control will changes in way which the whole text can be shown.

You can create such control using different approaches including:

  • Approach 1: Auto-size Composite Control hosting a Label
    This approach is based on hosting an auto-size Label with dynamic maximum width in an auto-size Control. In this approach we set maximum width of label based on width of control and since the label is auto-size, its height will be automatically set to show all texts and then we set height of control based on height of label.

  • Approach 2: Auto-Size Simple Control from scratch without Label
    This approach is based on overriding SetBoundsCore and setting size of control based on its Text size. In this approach we calculate the size of text based on width of control using TextRenderer.MeasureText and then set calculated height as height of control. In this approach you should handle text format flags and rendering yourself.

In both approaches a ControlDesigner is used to disable all size grab handles except left and right.

Please note

These are not the only approaches available but are good examples. As another option you can inherit from a Label and change it's behavior.

Approach 1: Auto-size Composite Control hosting a Label

It works based on setting AutoSize property of label to true and then setting MaximumSize of label based on contol Width. Also the height of control is set based on height of label. You can simply draw the image in OnPaint method. Also you can add a PictureBox for image.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
[Designer(typeof(MyLabelDesigner))]
public partial class MyLabel : Control
{
    public MyLabel() { InitializeComponent(); }
    private System.Windows.Forms.Label textLabel;
    private void InitializeComponent()
    {
        this.textLabel = new System.Windows.Forms.Label();
        this.textLabel.AutoSize = true;
        this.textLabel.Location = new System.Drawing.Point(0, 0);
        this.textLabel.Name = "label1";
        textLabel.SizeChanged += new EventHandler(textLabel_SizeChanged);
        this.AutoSize = true;
        this.Controls.Add(this.textLabel);
    }
    void textLabel_SizeChanged(object sender, EventArgs e)
    {
        this.Height = this.textLabel.Bottom + 0;
    }
    protected override void OnSizeChanged(EventArgs e)
    {
        base.OnSizeChanged(e);
        this.textLabel.MaximumSize = new Size(this.Width, 0);
    }
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public override string Text
    {
        get { return this.textLabel.Text; }
        set { this.textLabel.Text = value; }
    }
}

Approach 2: Auto-Size Simple Control from scratch without Label

This approach works based on setting size of control in SetBoundsCore based on current width and calculated height of its Text. To calculate height of control. You can simply draw the Image.

using System.Windows.Forms;
using System.Drawing;
using System.ComponentModel;
using System.Windows.Forms.Design;
[Designer(typeof(MyLabelDesigner))]
public class ExLabel : Control
{
    public ExLabel()
    {
        AutoSize = true;
        DoubleBuffered = true;
        SetStyle(ControlStyles.ResizeRedraw, true);
    }
    protected override void OnTextChanged(System.EventArgs e)
    {
        base.OnTextChanged(e);
        SetBoundsCore(Left, Top, Width, Height, BoundsSpecified.Size);
        Invalidate();
    }
    protected override void SetBoundsCore(int x, int y, int width, int height,
        BoundsSpecified specified)
    {
        var flags = TextFormatFlags.Left | TextFormatFlags.WordBreak;
        var proposedSize = new Size(width, int.MaxValue);
        var size = TextRenderer.MeasureText(Text, Font, proposedSize, flags);
        height = size.Height;
        base.SetBoundsCore(x, y, width, height, specified);
    }
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        var flags = TextFormatFlags.Left | TextFormatFlags.WordBreak;
        TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle,
            ForeColor, BackColor, flags);
    }
}

Designer

Here is the ControlDesigner which used to limit size grab handles in designer to left and right for both implementations:

using System.Windows.Forms.Design;
public class MyLabelDesigner : ControlDesigner
{
    public override SelectionRules SelectionRules
    {
        get
        {
            return (base.SelectionRules & ~(SelectionRules.BottomSizeable | 
                                            SelectionRules.TopSizeable));
        }
    }
}