在WinForms中添加水印文本框

64

有没有人能为我指出一个基本的Windows Forms TextBox的好实现,它最初将显示水印文本,当光标进入时会消失?我认为可以通过创意使用Enter和Leave事件来创建自己的实现,但我相信某个完全可用的实现正在某个地方等着被使用。我看到了WPF的实现,如果必要,我可以嵌套它,但更好的是本地的WinForms TextBox派生。

我目前有这个代码;还没有尝试过,但有人看到任何明显的问题吗?

public class WatermarkTextBox:TextBox
{
    public string WatermarkText { get; set; }

    public Color WatermarkColor { get; set; }

    private Color TextColor { get; set; }

    private bool isInTransition;

    public WatermarkTextBox()
    {
        WatermarkColor = SystemColors.GrayText;
    }

    private bool HasText { get { return Text.IsNotNullOrBlankOr(WatermarkText); }}

    protected override void OnEnter(EventArgs e)
    {
        base.OnEnter(e);

        if (HasText) return;

        isInTransition = true;
        ForeColor = TextColor;
        Text = String.Empty;
        isInTransition = false;
    }

    protected override void OnForeColorChanged(EventArgs e)
    {
        base.OnForeColorChanged(e);
        if (!isInTransition) //the change came from outside
            TextColor = ForeColor;
    }

    protected override void OnLeave(EventArgs e)
    {
        base.OnLeave(e);

        if (HasText) return;

        isInTransition = true;
        ForeColor = WatermarkColor;
        Text = WatermarkText.EmptyIfNull();
        isInTransition = false;
    }
}

编辑:上述方法在稍加调整后可能会起作用,但CueProvider工作得更好。以下是我的最终实现:

public class WatermarkTextBox:TextBox
{
    private string watermarkText;
    public string WatermarkText
    {
        get { return watermarkText; }
        set
        {
            watermarkText = value;
            if (watermarkText.IsNullOrBlank())
                CueProvider.ClearCue(this);
            else
                CueProvider.SetCue(this, watermarkText);
        }
    }
}

我原本可以完全整合CueProvider功能,但这样做也很美好。


7
水印是纸张上的图像或图案,用于唯一标识该纸张(在纸张之外称为数字水印)。您所描述的术语是提示横幅。 - Tergiver
从Jay Riggs的删除答案中:尝试CueProvider - Hans Passant
1
水印,又称提示文本或占位符文本。相关帖子 - System.Windows.Forms.TextBox中的水印 & 水印/提示文本/占位符TextBox & 向文本框添加占位符文本 - RBT
11个回答

109
官方术语是“提示横幅”。还有另一种方法,只需继承TextBox也能完成任务。在项目中添加一个新类,并粘贴下面显示的代码。编译。从工具箱的顶部拖放新控件并设置Cue属性。
在设计器中,您可以实时预览根据表单的Language属性本地化的Cue值。以很少的代价获得很大的收益,这是Winforms的优秀部分的出色演示。
using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Runtime.InteropServices;

class CueTextBox : TextBox {
    [Localizable(true)]
    public string Cue {
        get { return mCue; }
        set { mCue = value; updateCue(); }
    }

    private void updateCue() {
        if (this.IsHandleCreated && mCue != null) {
            SendMessage(this.Handle, 0x1501, (IntPtr)1, mCue);
        }
    }
    protected override void OnHandleCreated(EventArgs e) {
        base.OnHandleCreated(e);
        updateCue();
    }
    private string mCue;

    // PInvoke
    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, string lp);
}

更新:现在也可用于.NET Core 3.0(以及.NET5及更高版本),他们添加了PlaceholderText属性

11
这种方法的问题在于,一旦文本框获得焦点,提示文本就会消失,即使框中没有任何文本。如果您有一个默认获取焦点的文本框表单,则用户将看不到提示,从而使提示失去其意义。 - Igor Brejc
3
我发现的问题是它不支持多行文本框控件。我不确定如何修改这个类以支持多行文本框。 - Derek W
1
@IgorBrejc 当适当时,您可以使用Enter和Leave事件来清除和设置提示。 - nopara73
4
这个设计不支持多行文本框/RTB控件。您应该自己编写控件来实现。 - Jack
4
只有当SendMessage方法的第三个参数为(IntPtr)0时,才会发生这种情况。在搜索EM_SETCUEBANNER消息时,这一点在msdn上有记录。 - Xam
显示剩余3条评论

32

我已经更新了@Hans Passant上面给出的答案,引入了常量,使其与pinvoke.net的定义保持一致,并让代码通过FxCop验证。

class CueTextBox : TextBox
{
    private static class NativeMethods
    {
        private const uint ECM_FIRST = 0x1500;
        internal const uint EM_SETCUEBANNER = ECM_FIRST + 1;

        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        public static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, string lParam);
    }

    private string _cue;

    public string Cue
    {
        get
        {
            return _cue;
        }
        set
        {
            _cue = value;
            UpdateCue();
        }
    }

    private void UpdateCue()
    {
        if (IsHandleCreated && _cue != null)
        {
            NativeMethods.SendMessage(Handle, NativeMethods.EM_SETCUEBANNER, (IntPtr)1, _cue);
        }
    }

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        UpdateCue();
    }
}

编辑:更新PInvoke调用以设置CharSet属性,以保守为主。更多信息请参见SendMessage页面,在pinvoke.net查看。


我很感激你的支持,但我个人不太喜欢 FxCop(会限制我的风格)。但是我刚刚注意到你声明了 SendMessage 的 Unicode W 版本,却没有使用 CharSet。你确定这样行得通吗? - Hans Passant
@HansPassant 我想我从pinvoke.net得到了定义。它指出必须使用MarshalAsCharSet属性之一。我测试了两者,在XP64上至少可以正常工作(!)。然而,我认为使用CharSet更清晰 - 将进行编辑。 我为那些必须在工作中使用FxCop/StyleCop/R#并希望避免一些红色下划线的人制作了“FxCop版本”。 - g t

31

.NET 5.0+,.NET Core 3.0+

您可以使用PlaceholderText属性。它支持多行和单行文本框。

textBox1.PlaceholderText = "Something";

如果你查看.NET CORE实现的TextBox,你会发现它是通过处理绘制消息来实现的。

.NET Framework

这里有一个支持显示提示(水印或cue)的TextBox实现:

  • MultiLine为true时,它也会显示提示。
  • 它基于处理WM_PAINT消息并绘制提示。因此,您可以简单地自定义提示并添加一些属性,例如提示颜色,或者您可以从右到左绘制它或控制何时显示提示。
using System.Drawing;
using System.Windows.Forms;
public class ExTextBox : TextBox
{
    string hint;
    public string Hint
    {
        get { return hint; }
        set { hint = value; this.Invalidate(); }
    }
    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        if (m.Msg == 0xf)
        {
            if (!this.Focused && string.IsNullOrEmpty(this.Text)
                && !string.IsNullOrEmpty(this.Hint))
            {
                using (var g = this.CreateGraphics())
                {
                    TextRenderer.DrawText(g, this.Hint, this.Font,
                        this.ClientRectangle, SystemColors.GrayText , this.BackColor, 
                        TextFormatFlags.Top | TextFormatFlags.Left);
                }
            }
        }
    }
}

如果您使用 EM_SETCUEBANNER,那么将会有两个问题。文本始终显示为系统默认颜色。而且当 TextBoxMultiLine 时,文本不会被显示。

使用绘图解决方案,您可以显示任何颜色的文本。您还可以在控件是多行时显示水印:

enter image description here

下载

您可以克隆或下载这个可工作的示例:


这个能作为 TextBox 控件的扩展使用吗? - Aesthetic
1
@Yawz 它是从 TextBox 继承而来的。将此类添加到您的项目后,如果构建该项目,则 ExTextBox 控件将被添加到工具箱中,您可以将其拖放到表单上。 - Reza Aghaei
这对我很有效 - 谢谢。我的应用程序使用BorderStyle = FixedSingle的文本框,因此我不得不更改上面的代码,以便说“TextFormatFlags.VerticalCenter”而不是“TextFormatFlags.Top”,否则提示文本会落在边框上。 - Erik Schroder
它给了我一个错误:变量'tbPassword'未声明或从未赋值。 错误出现在 this.Controls.Add(this.tbPassword); - Moeez

15
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, string lParam);

还有这些消息常量:

private const uint EM_SETCUEBANNER = 0x1501;
private const uint CB_SETCUEBANNER = 0x1703;    // minimum supported client Windows Vista, minimum supported server Windows Server 2008

在我看来,实现最佳方式是作为扩展方法。
因此,对于TextBox控件,语法将是:

MyTextBox.CueBanner(false, "Password");

从代码中:

public static void CueBanner(this TextBox textbox, bool showcuewhenfocus, string cuetext)
{
    uint BOOL = 0;
    if (showcuewhenfocus == true) { BOOL = 1; }

    SendMessage(textbox.Handle, EM_SETCUEBANNER, (IntPtr)BOOL, cuetext); ;
}

4
你不能这样随意更改pinvoke声明。它既不适用于32位代码(返回int32),也不适用于64位代码(wparam是int64)。尽管它可能会偶然地成功,但当你运气不好时,你将无法弄清楚为什么它停止工作。请勿随意更改。 - Hans Passant

5

使用WinForms在.NET Core上:

.NET Core大大简化了此过程。您可以通过修改TextBox的新PlaceholderText属性直接添加占位符文本。

public virtual string PlaceholderText { get; set; }

请注意,如果您想获得有颜色的占位符文本,则可能仍需要编辑ForeColor。当Text字段为null或empty时,PlaceholderText字段可见。

属性

WinForms .NET Core应用程序中的占位符文本


2
请注意,此功能仅适用于.NET Core 3.0或更高版本。 - chuang

2

你可以通过在不同控件的Paint事件中绘制来为Textbox(多行或单行)添加水印。例如:

    Private Sub Panel1_Paint(sender As Object, e As PaintEventArgs) Handles Panel1.Paint
        If TextBox1.Text = "" Then
            TextBox1.CreateGraphics.DrawString("Enter Text Here", Me.Font, New SolidBrush(Color.LightGray), 0, 0)
        End If
    End Sub

1
我为我的项目编写了一个可重用的自定义控件类。
也许它可以帮助需要在项目中实现多个占位文本框的人。 这是C#和vb.net版本:

C#:


我为我的项目编写了一个可重用的自定义控件类。
也许它可以帮助需要在项目中实现多个占位文本框的人。 这是C#和vb.net版本:
namespace reusebleplaceholdertextbox
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // implementation
            CustomPlaceHolderTextbox myCustomTxt = new CustomPlaceHolderTextbox(
                "Please Write Text Here...", Color.Gray, new Font("ARIAL", 11, FontStyle.Italic)
                , Color.Black, new Font("ARIAL", 11, FontStyle.Regular)
                );

            myCustomTxt.Multiline = true;
            myCustomTxt.Size = new Size(200, 50);
            myCustomTxt.Location = new Point(10, 10);
            this.Controls.Add(myCustomTxt);
        }
    }

    class CustomPlaceHolderTextbox : System.Windows.Forms.TextBox
    {
        public string PlaceholderText { get; private set; }
        public Color PlaceholderForeColor { get; private set; }
        public Font PlaceholderFont { get; private set; }

        public Color TextForeColor { get; private set; }
        public Font TextFont { get; private set; }

        public CustomPlaceHolderTextbox(string placeholdertext, Color placeholderforecolor,
            Font placeholderfont, Color textforecolor, Font textfont)
        {
            this.PlaceholderText = placeholdertext;
            this.PlaceholderFont = placeholderfont;
            this.PlaceholderForeColor = placeholderforecolor;
            this.PlaceholderFont = placeholderfont;
            this.TextForeColor = textforecolor;
            this.TextFont = textfont;
            if (!string.IsNullOrEmpty(this.PlaceholderText))
            {
                SetPlaceHolder(true);
                this.Update();
            }
        }

        private void SetPlaceHolder(bool addEvents)
        {
            if (addEvents)
            {  
                this.LostFocus += txt_lostfocus;
                this.Click += txt_click;
            }

            this.Text = PlaceholderText;
            this.ForeColor = PlaceholderForeColor;
            this.Font = PlaceholderFont;
        }

        private void txt_click(object sender, EventArgs e)
        {
            // IsNotFirstClickOnThis:
            // if there is no other control in the form
            // we will have a problem after the first load
            // because we dont other focusable control to move the focus to
            // and we dont want to remove the place holder
            // only on first time the place holder will be removed by click event
            RemovePlaceHolder();
            this.GotFocus += txt_focus;
            // no need for this event listener now
            this.Click -= txt_click;
        }

        private void RemovePlaceHolder()
        {
            this.Text = "";
            this.ForeColor = TextForeColor;
            this.Font = TextFont;
        }
        private void txt_lostfocus(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(this.Text))
            {
                // set placeholder again
                SetPlaceHolder(false);
            }
        }

        private void txt_focus(object sender, EventArgs e)
        {
            if (this.Text == PlaceholderText)
            {
                // IsNotFirstClickOnThis:
                // if there is no other control in the form
                // we will have a problem after the first load
                // because we dont other focusable control to move the focus to
                // and we dont want to remove the place holder
                RemovePlaceHolder();
            }
        }
    }
}


VB.NET:


Namespace CustomControls

    Public Class PlaceHolderTextBox
        Inherits System.Windows.Forms.TextBox

        Public Property PlaceholderText As String
        Public Property PlaceholderForeColor As Color
        Public Property PlaceholderFont As Font
        Public Property TextForeColor As Color
        Public Property TextFont As Font

        Public Sub New(ByVal placeholdertext As String, ByVal placeholderforecolor As Color, ByVal placeholderfont As Font, ByVal txtboxbackcolor As Color, ByVal textforecolor As Color, ByVal textfont As Font)
            Me.PlaceholderText = placeholdertext
            Me.PlaceholderFont = placeholderfont
            Me.PlaceholderForeColor = placeholderforecolor
            Me.PlaceholderFont = placeholderfont
            Me.TextForeColor = textforecolor
            Me.TextFont = textfont
            Me.BackColor = txtboxbackcolor
            If Not String.IsNullOrEmpty(Me.PlaceholderText) Then
                SetPlaceHolder(True)
                Me.Update()
            End If
        End Sub

        Private Sub SetPlaceHolder(ByVal addEvents As Boolean)
            If addEvents Then
                AddHandler Me.LostFocus, AddressOf txt_lostfocus
                AddHandler Me.Click, AddressOf txt_click
            End If

            Me.Text = PlaceholderText
            Me.ForeColor = PlaceholderForeColor
            Me.Font = PlaceholderFont
        End Sub

        Private Sub txt_click(ByVal sender As Object, ByVal e As EventArgs)
            RemovePlaceHolder()
            AddHandler Me.GotFocus, AddressOf txt_focus
            RemoveHandler Me.Click, AddressOf txt_click
        End Sub

        Private Sub RemovePlaceHolder()
            Me.Text = ""
            Me.ForeColor = TextForeColor
            Me.Font = TextFont
        End Sub

        Private Sub txt_lostfocus(ByVal sender As Object, ByVal e As EventArgs)
            If String.IsNullOrEmpty(Me.Text) Then
                SetPlaceHolder(False)
            End If
        End Sub

        Private Sub txt_focus(ByVal sender As Object, ByVal e As EventArgs)
            If Me.Text = PlaceholderText Then
                RemovePlaceHolder()
            End If
        End Sub
    End Class

End Namespace

Text属性用作占位符并不是一个好主意。例如,考虑数据绑定;占位符将位于数据源中。 - Reza Aghaei

0

更新于2022年12月30日 使用.NET6(或者更低版本也可能有此选项,我不确定)

只需右键单击文本框 > 属性 > PlaceHolderText > 并将其设置为任何您想要的内容即可

点击此处查看该属性的图片


0

在 .Net Core 3 中,引入了一个属性到 TextBox 中: PlaceHolderText

如果你需要在 FrameWork 应用程序中使用它,可以从官方开源代码中获取所需的代码部分,并放置在 TextBox 子类中(参见许可证)。

这支持多行 TextBox 和 RTL 文本。

public class PlaceHolderTextBox : TextBox
{
    private const int WM_KILLFOCUS = 0x0008;
    private const int WM_PAINT = 0x000F;

    private string _placeholderText;

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);

        if (this.ShouldRenderPlaceHolderText(m))
        {
            using Graphics g = this.CreateGraphics();
            this.DrawPlaceholderText(g);
        }
    }

    #region PlaceHolder

    /// <summary>
    ///  Gets or sets the text that is displayed when the control has no text and does not have the focus.
    /// </summary>
    /// <value>The text that is displayed when the control has no text and does not have the focus.</value>
    [Localizable(true), DefaultValue("")]
    public virtual string PlaceholderText
    {
        get => _placeholderText;
        set
        {
            if (value == null)
            {
                value = string.Empty;
            }

            if (_placeholderText != value)
            {
                _placeholderText = value;
                if (this.IsHandleCreated)
                {
                    this.Invalidate();
                }
            }
        }
    }

    //-------------------------------------------------------------------------------------------------

    /// <summary>
    ///  Draws the <see cref="PlaceholderText"/> in the client area of the <see cref="TextBox"/> using the default font and color.
    /// </summary>
    private void DrawPlaceholderText(Graphics graphics)
    {
        TextFormatFlags flags = TextFormatFlags.NoPadding | TextFormatFlags.Top |
                                TextFormatFlags.EndEllipsis;
        Rectangle rectangle = this.ClientRectangle;

        if (this.RightToLeft == RightToLeft.Yes)
        {
            flags |= TextFormatFlags.RightToLeft;
            switch (this.TextAlign)
            {
                case HorizontalAlignment.Center:
                    flags |= TextFormatFlags.HorizontalCenter;
                    rectangle.Offset(0, 1);
                    break;
                case HorizontalAlignment.Left:
                    flags |= TextFormatFlags.Right;
                    rectangle.Offset(1, 1);
                    break;
                case HorizontalAlignment.Right:
                    flags |= TextFormatFlags.Left;
                    rectangle.Offset(0, 1);
                    break;
            }
        }
        else
        {
            flags &= ~TextFormatFlags.RightToLeft;
            switch (this.TextAlign)
            {
                case HorizontalAlignment.Center:
                    flags |= TextFormatFlags.HorizontalCenter;
                    rectangle.Offset(0, 1);
                    break;
                case HorizontalAlignment.Left:
                    flags |= TextFormatFlags.Left;
                    rectangle.Offset(1, 1);
                    break;
                case HorizontalAlignment.Right:
                    flags |= TextFormatFlags.Right;
                    rectangle.Offset(0, 1);
                    break;
            }
        }

        TextRenderer.DrawText(graphics, this.PlaceholderText, this.Font, rectangle, SystemColors.GrayText, this.BackColor, flags);
    }
    
    private bool ShouldRenderPlaceHolderText(in Message m) =>
        !string.IsNullOrEmpty(this.PlaceholderText) &&
        (m.Msg == WM_PAINT || m.Msg == WM_KILLFOCUS) &&
        !this.GetStyle(ControlStyles.UserPaint) &&
        !this.Focused && this.TextLength == 0;

    #endregion
}

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

namespace PlaceHolderTextBoxCSharp
{
    public class CTextBox : TextBox
    {
        private Panel contenedor;
        protected string texto = "PlaceHolderText";
        protected Color colorTextoDefault = Color.Gray;
        public Color colorTexto = Color.Gray;
        protected Color colorTextoObligatorio = Color.Red;
        private Font fuente;
        private SolidBrush establecerColorTexto;
        private bool obligatoriedad = false;
        private bool colorConFoco = false;
        private int vuelta = 0;

        public CTextBox()
        {
            Inicializar();
        }

        private void Inicializar()
        {
            fuente = Font;
            CharacterCasing = CharacterCasing.Upper;
            contenedor = null;

            MuestraPlaceHolder();

            Leave += new EventHandler(PierdeFoco);
            TextChanged += new EventHandler(CambiaTexto);
        }

        private void EliminaPlaceHolder()
        {
            if (contenedor != null)
            {
                Controls.Remove(contenedor);
                contenedor = null;
            }
        }

        private void MuestraPlaceHolder()
        {
            if (contenedor == null && TextLength <= 0)
            {
                contenedor = new Panel();
                contenedor.Paint += new PaintEventHandler(contenedorPaint);
                contenedor.Invalidate();
                contenedor.Click += new EventHandler(contenedorClick);
                Controls.Add(contenedor);
            }
        }

        private void contenedorClick(object sender, EventArgs e)
        {
            Focus();
        }

        private void contenedorPaint(object sender, PaintEventArgs e)
        {
            contenedor.Location = new Point(2, 0);
            contenedor.Height = Height;
            contenedor.Width = Width;
            contenedor.Anchor = AnchorStyles.Left | AnchorStyles.Right;
            establecerColorTexto = new SolidBrush(colorTexto);
            Graphics g = e.Graphics;
            g.DrawString(texto, fuente, establecerColorTexto, new PointF(-1f, 1f));
        }

        private void PierdeFoco(object sender, EventArgs e)
        {
            if (TextLength > 0)
            {
                EliminaPlaceHolder();
            }
            else
            {
                if (obligatoriedad == true)
                {
                    colorTexto = colorTextoObligatorio;
                }
                else
                {
                    colorTexto = colorTextoDefault;
                }

                Invalidate();
            }
        }

        private void CambiaTexto(object sender, EventArgs e)
        {
            if (TextLength > 0)
            {
                EliminaPlaceHolder();
            }
            else
            {
                MuestraPlaceHolder();

                vuelta += 1;

                if (vuelta >= 1 && obligatoriedad == true)
                {
                    colorTexto = colorTextoObligatorio;
                }
            }
        }

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

        protected override void OnInvalidated(InvalidateEventArgs e)
        {
            base.OnInvalidated(e);

            if (contenedor != null)
            {
                contenedor.Invalidate();
            }
        }

        [Category("Atributos PlaceHolder")]
        [Description("Establece el texto a mostrar.")]

        public string PlaceHolderText
        {
            get
            {
                return texto;
            }
            set
            {
                texto = value;
                Invalidate();
            }
        }

        [Category("Atributos PlaceHolder")]
        [Description("Establece el estilo de fuente del PlaceHolder.")]

        public Font PlaceHolderFont
        {
            get
            {
                return fuente;
            }
            set
            {
                fuente = value;
                Invalidate();
            }
        }

        [Category("Atributos PlaceHolder")]
        [Description("Indica si el campo es obligatorio.")]

        public bool PlaceHolderFieldRequired
        {
            get
            {
                return obligatoriedad;
            }
            set
            {
                obligatoriedad = value;
                Invalidate();
            }
        }
    }
}

2
请在您的源代码示例中添加一些注释。没有任何描述的发布不是一个好答案。 - Alexander I.
1
请将您的代码翻译成英文,或者至少在代码中添加一些英文注释和解释。现在的回答并不好,很难确定它是否真正尝试解决问题。请查看此链接以获取有关如何遵循 Stack Overflow 规则编写良好答案的更多信息:https://stackoverflow.com/help/how-to-answer。 - sɐunıɔןɐqɐp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接