Search code examples
c#winformsonpaint

How to correctly get a WinForms Button control to draw custom text


I'm trying to create a custom winforms button control that will allow rotation of the button text through a rotation property. I've mostly got it working, but it's very kludgy and I would like to know the proper way to do this.

Particularly right now the text redrawing is behaving strangely. If the control is moved off screen, and then moved slowly back on the text either becomes very messed up (such as only half drawn), or disappears entirely, until moused over. Obviously I'm doing something wrong, but can't figure out what.

I'm inheriting from the button control and overriding its OnPaint method.

Here is the code:

public class RotateButton : Button
{
    private string text;
    private bool painting = false;

    public enum RotationType { None, Right, Flip, Left}

    [DefaultValue(RotationType.None), Category("Appearance"), Description("Rotates Button Text")]
    public RotationType Rotation { get; set; }

    public override string Text
    {
        get
        {
            if (!painting)
                return text;
            else
                return "";
        }
        set
        {
            text = value;
        }
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        painting = true;

        base.OnPaint(e);

        StringFormat format = new StringFormat();
        Int32 lNum = (Int32)Math.Log((Double)this.TextAlign, 2);
        format.LineAlignment = (StringAlignment)(lNum / 4);
        format.Alignment = (StringAlignment)(lNum % 4);

        int padding = 2;

        SizeF txt = e.Graphics.MeasureString(Text, this.Font);
        SizeF sz = e.Graphics.VisibleClipBounds.Size;

        if (Rotation == RotationType.Right)
        {
            //90 degrees
            e.Graphics.TranslateTransform(sz.Width, 0);
            e.Graphics.RotateTransform(90);
            e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Height - padding, sz.Width - padding), format);
            e.Graphics.ResetTransform();
        }
        else if (Rotation == RotationType.Flip)
        {
            //180 degrees
            e.Graphics.TranslateTransform(sz.Width, sz.Height);
            e.Graphics.RotateTransform(180);
            e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Width - padding, sz.Height - padding), format);
            e.Graphics.ResetTransform();
        }
        else if (Rotation == RotationType.Left)
        {
            //270 degrees
            e.Graphics.TranslateTransform(0, sz.Height);
            e.Graphics.RotateTransform(270);
            e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Height - padding, sz.Width - padding), format);
            e.Graphics.ResetTransform();
        }
        else
        {
            //0 = 360 degrees
            e.Graphics.TranslateTransform(0, 0);
            e.Graphics.RotateTransform(0);
            e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Width - padding, sz.Height - padding), format);
            e.Graphics.ResetTransform();
        }

        painting = false;
    }
}

So my main question is how might I fix the text redrawing problem?

In addition I have a few other questions/comments on the above code:

  1. At first the text was showing up twice, once in it's default location, and once in the rotated location. I assume this is because the text is being drawn first when the base.OnPaint method is called. If this is the case, how do I keep the text from drawing initially?

    My solution is to to override the Text string and clear it before calling base.OnPaint using a boolean, which is not a solution I'm particularly happy with.

  2. Should I be disposing of the PaintEventArgs at the end with e.dispose? I guess I'm not sure how the PaintEventArgs object is being dealt with.

Thanks in advance!

Ps. This is my first post/question, so I apologize in advance if I inadvertently ignored some etiquette or rules.


Solution

    1. VisibleClipBounds returns the area that needs to be repainted, for example if half the button needs to be repainted (a top form covering half the button is closed) VisibleClipBounds returns only that area. So you can't use that for painting centered text. SizeF sz = new SizeF(Width, Height); Should take care of the repainting problem.

    2. Button doesn't support owner drawing, your way seems fine.

    3. As a rule you shouldn't dispose objects you haven't created, and disposable event arguments are disposed by the logic that created them (and called the On... in the first place) so don't worry about disposing PaintEventArgs.

    Welcome to Stack Overflow :)