How to make the c# winforms custom textbox control class to work perfectly with fade in/out watermark text?

Sharp Liverman 20 Reputation points
2025-03-28T00:20:43.77+00:00

I created a new c# winforms class of a custom TextBox and the fade in/out watermark text is working perfect. when i move the mouse cursor over the textbox control it will show with smooth fade in the watermark text and when moving the mouse cursor away from the textbox control the watermark text will fade out smooth.

the problem is when i try to type something in the textbox control.

in the attached image here on the left when i type the letters the characters i type start from near the end right side but not from the end as it supposed to be. the text i type is appending a bit before the end right.

second problem on the right side of the attached image, when i try to mark and highlight with the mouse the text i typed if i will leave the mouse left button it will not mark it anymore and i can't delete the text.

I tried many ways to fix it, changed the onpaint event code and other codes. and i got success to fix the typing problem but then i had a problem with the fade in/out that it was flicker while fading.

so, i move in circles each time solve one problem and then get back to the second problem. i can't find the balance to fix perfectly the two problems. either i have flickering with the fading and the text typing is working perfect or the fading is working perfect but then i can't type.

here is the completed class code. if someone can test it please and maybe find a solution.

Thank you very much.

texttype1

Here is the full code:

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

namespace FolderSearchApp
{
    class TextBoxWatermark : TextBox
    {
        #region Protected Fields

        protected string _placeholderText = "Enter text here...";
        protected Color _inactivePlaceholderColor = Color.LightGray;
        protected Color _focusedPlaceholderColor = Color.Gray;

        #endregion

        #region Private Fields

        private float placeholderOpacity = 0f;
        private bool isMouseOver = false;
        private Timer fadeTimer;
        private Font placeholderFont;
        private bool useFocusBehavior = false;

        #endregion

        #region Constructor

        public TextBoxWatermark()
        {
            placeholderFont = this.Font;
            this.Multiline = false;

            // Enable double-buffered smooth painting
            this.SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);

            this.TextChanged += (s, e) => this.Invalidate();
            this.GotFocus += (s, e) => this.Invalidate();
            this.LostFocus += (s, e) => this.Invalidate();

            this.MouseMove += (s, e) =>
            {
                if (!useFocusBehavior && !isMouseOver)
                {
                    isMouseOver = true;
                    fadeTimer.Start();
                }
            };

            this.MouseLeave += (s, e) =>
            {
                if (!useFocusBehavior)
                {
                    isMouseOver = false;
                    fadeTimer.Start();
                }
            };

            fadeTimer = new Timer();
            fadeTimer.Interval = 30;
            fadeTimer.Tick += FadeTimer_Tick;
        }

        #endregion

        #region Fade Logic

        private void FadeTimer_Tick(object sender, EventArgs e)
        {
            float target = (useFocusBehavior ? this.Focused : isMouseOver) ? 1f : 0f;
            float step = 0.1f;

            if (Math.Abs(placeholderOpacity - target) < step)
            {
                placeholderOpacity = target;
                fadeTimer.Stop();
            }
            else
            {
                placeholderOpacity += (placeholderOpacity < target) ? step : -step;
            }

            this.Invalidate();
        }

        #endregion

        #region Overridden Painting

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

            // Draw the placeholder
            if (this.TextLength == 0 && placeholderOpacity > 0f)
            {
                Color color = this.Focused ? _focusedPlaceholderColor : _inactivePlaceholderColor;
                Color faded = Color.FromArgb((int)(placeholderOpacity * 255), color);
                using (Brush b = new SolidBrush(faded))
                {
                    e.Graphics.DrawString(_placeholderText, placeholderFont, b, new PointF(2f, 2f));
                }
            }

            // Draw the typed text (required if UserPaint is enabled)
            if (this.TextLength > 0)
            {
                TextRenderer.DrawText(e.Graphics, this.Text, this.Font, this.ClientRectangle, this.ForeColor, TextFormatFlags.Left | TextFormatFlags.VerticalCenter);
            }
        }

        #endregion

        #region Properties

        [Category("Placeholder Settings")]
        [Description("The placeholder text that appears when the textbox is empty.")]
        public string PlaceholderText
        {
            get => _placeholderText;
            set { _placeholderText = value; this.Invalidate(); }
        }

        [Category("Placeholder Settings")]
        [Description("Color of placeholder when textbox is focused.")]
        public Color PlaceholderFocusColor
        {
            get => _focusedPlaceholderColor;
            set { _focusedPlaceholderColor = value; this.Invalidate(); }
        }

        [Category("Placeholder Settings")]
        [Description("Color of placeholder when textbox is not focused.")]
        public Color PlaceholderIdleColor
        {
            get => _inactivePlaceholderColor;
            set { _inactivePlaceholderColor = value; this.Invalidate(); }
        }

        [Category("Placeholder Settings")]
        [Description("Font used for placeholder text.")]
        public Font PlaceholderFont
        {
            get => placeholderFont;
            set { placeholderFont = value; this.Invalidate(); }
        }

        [Category("Placeholder Settings")]
        [Description("Use focus-based placeholder behavior instead of mouse-over.")]
        public bool UseFocusBehavior
        {
            get => useFocusBehavior;
            set
            {
                useFocusBehavior = value;
                fadeTimer.Start();
                this.Invalidate();
            }
        }

        #endregion
    }
}

Developer technologies | C#
0 comments No comments
{count} votes

Accepted answer
  1. Anonymous
    2025-03-28T09:49:59.2533333+00:00

    Hi @Sharp Liverman , Welcome to Microsoft Q&A,

    Because UserPaint affects the default way text is drawn, you need to properly let the TextBox handle the text itself, rather than drawing it manually.

    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 xxx
    {
        public partial class TextBoxWatermark : TextBox
        {
              #region Protected Fields
            protected string _placeholderText = "Enter text here...";
            protected Color _inactivePlaceholderColor = Color.LightGray;
            protected Color _focusedPlaceholderColor = Color.Gray;
            #endregion
    
            #region Private Fields
            private float placeholderOpacity = 0f;
            private bool isMouseOver = false;
            private Timer fadeTimer;
            private Font placeholderFont;
            private bool useFocusBehavior = false;
            private bool isPlaceholderVisible = true;
            #endregion
    
            #region Constructor
            public TextBoxWatermark()
            {
                placeholderFont = this.Font;
                this.Multiline = false;
    
                this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
    
                this.TextChanged += (s, e) => { CheckPlaceholderVisibility(); };
                this.GotFocus += (s, e) => { StartFade(true); };
                this.LostFocus += (s, e) => { StartFade(false); };
    
                this.MouseMove += (s, e) =>
                {
                    if (!useFocusBehavior && !isMouseOver)
                    {
                        isMouseOver = true;
                        StartFade(true);
                    }
                };
    
                this.MouseLeave += (s, e) =>
                {
                    if (!useFocusBehavior)
                    {
                        isMouseOver = false;
                        StartFade(false);
                    }
                };
    
                fadeTimer = new Timer();
                fadeTimer.Interval = 30;
                fadeTimer.Tick += FadeTimer_Tick;
            }
            #endregion
    
            #region Fade Logic
            private void StartFade(bool fadeIn)
            {
                float target = fadeIn ? 1f : 0f;
                if (Math.Abs(placeholderOpacity - target) < 0.1f)
                {
                    placeholderOpacity = target;
                    fadeTimer.Stop();
                }
                else
                {
                    fadeTimer.Start();
                }
            }
    
            private void FadeTimer_Tick(object sender, EventArgs e)
            {
                float target = (useFocusBehavior ? this.Focused : isMouseOver) ? 1f : 0f;
                float step = 0.1f;
    
                if (Math.Abs(placeholderOpacity - target) < step)
                {
                    placeholderOpacity = target;
                    fadeTimer.Stop();
                }
                else
                {
                    placeholderOpacity += (placeholderOpacity < target) ? step : -step;
                }
    
                this.Invalidate();
            }
    
            private void CheckPlaceholderVisibility()
            {
                isPlaceholderVisible = string.IsNullOrEmpty(this.Text);
                this.Invalidate();
            }
            #endregion
    
            #region Custom Placeholder Drawing
            protected override void WndProc(ref Message m)
            {
                base.WndProc(ref m);
    
                if (m.Msg == 0xF /* WM_PAINT */ && isPlaceholderVisible)
                {
                    using (Graphics g = Graphics.FromHwnd(this.Handle))
                    {
                        Color color = this.Focused ? _focusedPlaceholderColor : _inactivePlaceholderColor;
                        Color faded = Color.FromArgb((int)(placeholderOpacity * 255), color);
                        using (Brush b = new SolidBrush(faded))
                        {
                            g.DrawString(_placeholderText, placeholderFont, b, new PointF(2f, 2f));
                        }
                    }
                }
            }
            #endregion
    
            #region Properties
            [Category("Placeholder Settings")]
            [Description("The placeholder text that appears when the textbox is empty.")]
            public string PlaceholderText
            {
                get => _placeholderText;
                set { _placeholderText = value; this.Invalidate(); }
            }
    
            [Category("Placeholder Settings")]
            [Description("Color of placeholder when textbox is focused.")]
            public Color PlaceholderFocusColor
            {
                get => _focusedPlaceholderColor;
                set { _focusedPlaceholderColor = value; this.Invalidate(); }
            }
    
            [Category("Placeholder Settings")]
            [Description("Color of placeholder when textbox is not focused.")]
            public Color PlaceholderIdleColor
            {
                get => _inactivePlaceholderColor;
                set { _inactivePlaceholderColor = value; this.Invalidate(); }
            }
    
            [Category("Placeholder Settings")]
            [Description("Font used for placeholder text.")]
            public Font PlaceholderFont
            {
                get => placeholderFont;
                set { placeholderFont = value; this.Invalidate(); }
            }
    
            [Category("Placeholder Settings")]
            [Description("Use focus-based placeholder behavior instead of mouse-over.")]
            public bool UseFocusBehavior
            {
                get => useFocusBehavior;
                set
                {
                    useFocusBehavior = value;
                    fadeTimer.Start();
                    this.Invalidate();
                }
            }
            #endregion
        }
    }
    
    

    Best Regards,

    Jiale


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment". 

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.


0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.