Search code examples
c#winformsgraphicstextboxdrawing

TextBox's Text is moving slightly when toggling Enabled: Font and Font size changes


In my application, I have Custom TextBox Control. Its Text seems to be moving when I change the Enabled property.

  1. I don't understand what cause this.

  2. I will be changing TextAlign, AutoSize, Enabled, and Font properties

I've tried using TextRenderer.DrawText(): it seems to fix some issues but when I change to certain Font and Font size, the Text is moving again (for example, setting MS ゴシック, 10pt).

I have searched the site and read the following but didn't seem to understand properly:

Using Graphics.DrawString to Simulate TextBox rendering

How do I use DrawString without trimming?

Graphics.DrawString vs TextRenderer.DrawText?Which can Deliver Better Quality


Here are some screenshots:

MS ゴシック, 10pt And TextAlign Center and e.Graphics.DrawString

↓↓ Enabled ↓↓

↓↓ Disabled ↓↓

comparison

MS ゴシック, 10pt And TextAlign Center and TextRenderer.DrawText

↓↓ Enabled ↓↓

↓↓ Disabled ↓↓

comparison (text seems to be slightly elongated when disabled)

Original post of my code https://devlights.hatenablog.com/entry/20070523/p1

This is my Custom Control code do far:

using System.Drawing;
using System.Windows.Forms;

public partial class CustomEnabledStateTextBox : TextBox
{
    public CustomEnabledStateTextBox()
    {
        InitializeComponent();
        AutoSize = false;
    }

    public override bool AutoSize
    {
        get { return base.AutoSize; }
        set { base.AutoSize = value; }
    }

    protected override void OnEnabledChanged(EventArgs e)
    {
        this.SetStyle(ControlStyles.UserPaint, !this.Enabled);
        this.Invalidate();
        base.OnEnabledChanged(e);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        using (StringFormat sf = new StringFormat())
        {
            if (this.TextAlign == HorizontalAlignment.Center)
            {
                sf.Alignment = StringAlignment.Center;
            }
            else if (this.TextAlign == HorizontalAlignment.Left)
            {
                sf.Alignment = StringAlignment.Near;
            }
            else
            {
                sf.Alignment = StringAlignment.Far;
            }

            using (SolidBrush brush = new SolidBrush(ForeColor))
            {
                //MS UI Gothic, 8pt --> not OK
                //MS UI Gothic, 9pt --> OK
                //MS UI Gothic, 10pt --> OK
                //MS UI Gothic, 11pt --> OK
                //MS UI Gothic, 12pt --> not OK
                e.Graphics.DrawString(
                    base.Text, base.Font, brush, new Rectangle(-1, 1, base.Width, base.Height), sf);

                //MS ゴシック, 8pt --> not OK
                //MS ゴシック, 9pt --> not OK
                //MS ゴシック, 10pt --> not OK
                //MS ゴシック, 12pt --> not OK
                //e.Graphics.DrawString(
                //    base.Text, base.Font, brush, new Rectangle(-1, 1, base.Width, base.Height), sf);

                //MS ゴシック, 10pt --> not OK
                //TextRenderer.DrawText(e.Graphics, Text, Font, new Rectangle(0, 0, Width, Height), ForeColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPadding | TextFormatFlags.Internal);
            }

            if (this.BorderStyle == BorderStyle.FixedSingle)
            {
                e.Graphics.DrawRectangle(new Pen(Color.Black), 0, 0, this.Width - 1, this.Height - 1);
            }
        }
    }
}

Test Form:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        customEnabledStateTextBox1.Text = "2021/04/04";
        customEnabledStateTextBox1.Size = new Size(88, 20);
        customEnabledStateTextBox1.BorderStyle = BorderStyle.None;
        customEnabledStateTextBox1.TextAlign = HorizontalAlignment.Center;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        customEnabledStateTextBox1.Enabled = true;
    }

    private void button2_Click(object sender, EventArgs e)
    {
        customEnabledStateTextBox1.Enabled = false;
    }
}

Update 1

I have Changed my code according to Answer provided by Jimi And tested it and it fix some issues but the problem still exits when I used
MS ゴシック, 10pt And fount out that in both MS UI Gothic, 10pt AND MS ゴシック, 10pt cases , Text is moving slightly downward.My application use MS ゴシック 7pt~12pt.So I have to show 10pt correctly. I haven't tested with other Fonts though. Please Help.

Sorry that I didn`t provide .Net framework before I am be using

 .NET Framework 3.5
 BorderStyle None

Test Case

MS UI Gothic, 8pt-- > OK
MS UI Gothic, 9pt-- > OK
MS UI Gothic, 10pt-- > not OK
MS UI Gothic, 11pt-- > OK
MS UI Gothic, 12pt-- > OK

MS ゴシック, 8pt-- > OK
MS ゴシック, 9pt-- > OK
MS ゴシック, 10pt-- > not OK
MS ゴシック, 12pt-- > OK

Here are Screenshots for

MS ゴシック, 10pt And TextAlign Center and BorderStyle None andTextRenderer.DrawText Enabled Disabled Comparison

MS UI Gothic, 10pt And TextAlign Center and BorderStyle None and TextRenderer.DrawText Enabled Disabled Comparison

Updated Code below : I have tested it in both .Net Framework 3.5 And 4.8

CustomEnabledStateTextBox :

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
//using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinFormTest1
{
    public partial class CustomEnabledStateTextBox : TextBox
    {
        public override bool AutoSize
        {
            get { return base.AutoSize; }
            set { base.AutoSize = value; }
        }

        public CustomEnabledStateTextBox()
        {
            InitializeComponent();

            AutoSize = false;
        }

        
        protected override void OnEnabledChanged(EventArgs e)
        {
            //>>>Change 1
            SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, !Enabled);
            UpdateStyles();
            //>>>Change 1
            base.OnEnabledChanged(e);
        }

        //>>>Change 1
        protected override void OnBorderStyleChanged(EventArgs e)
        {
            base.OnBorderStyleChanged(e);
            if (BorderStyle == BorderStyle.FixedSingle)
            {
                BorderStyle = BorderStyle.Fixed3D;
            }
        }
        //>>>Change 1



        protected override void OnPaint(PaintEventArgs e)
        {

            using (StringFormat sf = new StringFormat())
            {

                if (this.TextAlign == HorizontalAlignment.Center)
                {
                    sf.Alignment = StringAlignment.Center;
                }
                else if (this.TextAlign == HorizontalAlignment.Left)
                {
                    sf.Alignment = StringAlignment.Near;
                }
                else
                {
                    sf.Alignment = StringAlignment.Far;
                }

                //>>>Change 1
                var rect = ClientRectangle;
                if (BorderStyle != BorderStyle.None) rect.Inflate(-1, -1);

                var alignment = TextAlign == HorizontalAlignment.Left
                              ? TextFormatFlags.Left : (TextFormatFlags)((int)TextAlign ^ 3);

                var flags = TextFormatFlags.TextBoxControl | TextFormatFlags.NoPadding | alignment;
                //>>>Change 1

                using (SolidBrush brush = new SolidBrush(ForeColor))
                {
                    //MS UI Gothic, 8pt --> not OK
                    //MS UI Gothic, 9pt --> OK
                    //MS UI Gothic, 10pt --> OK
                    //MS UI Gothic, 11pt --> OK
                    //MS UI Gothic, 12pt --> not OK
                    //e.Graphics.DrawString(
                    //    base.Text, base.Font, brush, new Rectangle(-1, 1, base.Width, base.Height), sf);

                    //MS ゴシック, 8pt --> not OK
                    //MS ゴシック, 9pt --> not OK
                    //MS ゴシック, 10pt --> not OK
                    //MS ゴシック, 12pt --> not OK
                    //e.Graphics.DrawString(
                    //    base.Text, base.Font, brush, new Rectangle(-1, 1, base.Width, base.Height), sf);

                    //MS ゴシック, 10pt --> not OK
                    //TextRenderer.DrawText(e.Graphics, Text, Font, new Rectangle(0, 0, Width, Height), ForeColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPadding | TextFormatFlags.Internal);

                    //TextRenderer.DrawText(e.Graphics, Text, Font, new Rectangle(0, 0, Width, Height), ForeColor, flags);

                    //>>>Change 1
                    //MS UI Gothic, 8pt --> OK
                    //MS UI Gothic, 9pt --> OK
                    //MS UI Gothic, 10pt --> not OK
                    //MS UI Gothic, 11pt --> OK
                    //MS UI Gothic, 12pt --> OK

                    //MS ゴシック, 8pt --> OK
                    //MS ゴシック, 9pt --> OK
                    //MS ゴシック, 10pt --> not OK
                    //MS ゴシック, 12pt --> OK

                    TextRenderer.DrawText(e.Graphics, Text, Font, rect, ForeColor, flags);
                    //>>>Change 1

                }

                if (this.BorderStyle == BorderStyle.FixedSingle)
                {
                    e.Graphics.DrawRectangle(new Pen(Color.Black), 0, 0, this.Width - 1, this.Height - 1);
                }

                //>>>Change 1
                base.OnPaint(e);
                //>>>Change 1
            }
        }


 
    }
}

CustomEnabledStateTextBoxSO :

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
//using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinFormTest1
{
    public partial class CustomEnabledStateTextBoxSO : TextBox
    {
        public override bool AutoSize
        {
            get { return base.AutoSize; }
            set { base.AutoSize = value; }
        }

        public CustomEnabledStateTextBoxSO()
        {
            InitializeComponent();

            AutoSize = false;
        }

        protected override void OnEnabledChanged(EventArgs e)
        {
            SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, !Enabled);
            UpdateStyles();
            base.OnEnabledChanged(e);
        }

        protected override void OnBorderStyleChanged(EventArgs e)
        {
            base.OnBorderStyleChanged(e);
            if (BorderStyle == BorderStyle.FixedSingle)
            {
                BorderStyle = BorderStyle.Fixed3D;
            }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            var rect = ClientRectangle;
            if (BorderStyle != BorderStyle.None) rect.Inflate(-1, -1);

            var alignment = TextAlign == HorizontalAlignment.Left
                          ? TextFormatFlags.Left : (TextFormatFlags)((int)TextAlign ^ 3);

            var flags = TextFormatFlags.TextBoxControl | TextFormatFlags.NoPadding | alignment;

            //MS UI Gothic, 8pt-- > OK
            //MS UI Gothic, 9pt-- > OK
            //MS UI Gothic, 10pt-- > not OK
            //MS UI Gothic, 11pt-- > OK
            //MS UI Gothic, 12pt-- > OK

            //MS ゴシック, 8pt-- > OK
            //MS ゴシック, 9pt-- > OK
            //MS ゴシック, 10pt-- > not OK
            //MS ゴシック, 12pt-- > OK
            TextRenderer.DrawText(e.Graphics, Text, Font, rect, ForeColor, flags);
            base.OnPaint(e);
        }
    }
}


Test Form:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
//using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinFormTest1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            //Left Side
            customEnabledStateTextBox1.Text = "2021/04/04";
            customEnabledStateTextBox1.Size = new Size(88, 20); //size will change if i set Autosize true
            customEnabledStateTextBox1.BorderStyle = BorderStyle.None;
            customEnabledStateTextBox1.TextAlign = HorizontalAlignment.Center;

            //Right Side
            customEnabledStateTextBoxSO1.Text = "2021/04/04";
            customEnabledStateTextBoxSO1.Size = new Size(88, 20); //size will change if i set Autosize true
            customEnabledStateTextBoxSO1.BorderStyle = BorderStyle.None;
            customEnabledStateTextBoxSO1.TextAlign = HorizontalAlignment.Center;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            customEnabledStateTextBox1.Enabled = true;
            customEnabledStateTextBoxSO1.Enabled = true;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            customEnabledStateTextBox1.Enabled = false;
            customEnabledStateTextBoxSO1.Enabled = false;
        }
    }
}


Solution

  • A few changes to the text rendering should get you close to what you're expecting.

    • Use TextRenderer to draw the text
    • Add TextFormatFlags.TextBoxControl and TextFormatFlags.NoPadding to the TextFormatFlags used in TextRenderer.DrawText() (see the Docs about these two)
    • Override OnBorderStyleChanged() to replace the FixedSingle style with Fixed3D when the Control is custom-drawn. This is what the base class (TextBoxBase) does, otherwise you get the default internal border that shrinks the Win32 Control's Client area.
    • Inflate the Control's ClientRectangle by -1 pixel when BorderStyle = BorderStyle.Fixed3D.

    I added UpdateStyles() after the SetStyle() call: this forces the new Styles and also causes the Control to repaint itself.


    Note that setting a Font that doesn't support all CodePoint (simplified: the Unicode chars in your Text) used when setting the Text of a control, may cause a Font Fallback.
    In practice, the System will select a mapped surrogate Font that replaces the Control's Font. This happens transparently and without notice.
    A different Graphic renderer can behave in a different manner in similar situations.

    See the notes here:
    How can a Label control display Japanese characters properly when the Font used doesn't support this language?

    Somewhat different in a RichTextBox Control (the .Net base class is the same, but the Win32 Control is quite different):
    Some Alt keys changes my RichTextBox font

    Also, quite recently, all Asian Font rendering and UI presentation has been modified to better handle its characteristics. So the .Net version in use can also change the behavior.
    The code here is tested with .Net Framework 4.8.


    public partial class CustomEnabledStateTextBox : TextBox
    {
        public CustomEnabledStateTextBox() => this.AutoSize = false;
    
        protected override void OnEnabledChanged(EventArgs e)
        {
            SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, !Enabled);
            UpdateStyles();
            base.OnEnabledChanged(e);
        }
    
        protected override void OnBorderStyleChanged(EventArgs e)
        {
            base.OnBorderStyleChanged(e);
            if (BorderStyle == BorderStyle.FixedSingle) BorderStyle = BorderStyle.Fixed3D;
        }
    
        private TextFormatFlags flags = TextFormatFlags.TextBoxControl | 
                                        TextFormatFlags.NoPadding;
        protected override void OnPaint(PaintEventArgs e)
        {
            var rect = ClientRectangle;
            if (BorderStyle != BorderStyle.None) rect.Inflate(-1, -1);
    
            var alignment = TextAlign == HorizontalAlignment.Left
                          ? TextFormatFlags.Left : (TextFormatFlags)((int)TextAlign^ 3);
    
            flags |= alignment;
            TextRenderer.DrawText(e.Graphics, Text, Font, rect, ForeColor, flags);
            base.OnPaint(e);
        }
    }