-1

I work on a control inheriting from TextBox. I want it to feature a watermark property (the text you see when there is no text).

All steps taken:

In a new Visual Studio instance, click the link Create New Project, select the project type Windows Forms Control Library, name the project TBW1 (TextBox with Watermark), and click OK. Rename the default control to UTextBoxWatermark.

I want to inherit from the TextBox control, but new user controls inherit from the UserControl class by default. This is defined in the designer.

To access and modify the designer, at the top of Solution Explorer, click Show All Files. Expand UTextBoxWatermark.vb. Doubleclick CTextBoxWatermark.Designer.vb to open it in the code editor.

Replace the base class Control from UserControl

Partial Class UTextBoxWatermark
    Inherits System.Windows.Forms.UserControl
    ...
End Class

to TextBox

Partial Class UTextBoxWatermark
    Inherits System.Windows.Forms.TextBox
    ...
End Class

In the InitializeComponent procedure, remove the AutoScaleMode assignment. It does not exist in the TextBox control.

Private Sub InitializeComponent()
    components = New System.ComponentModel.Container()
End Sub

Close CTextBoxWatermark.Designer.vb. Use Save All to save the new project in your projects main folder.

The user control designer is not available anymore, because it will be painted from the inherited class, i.e., TextBox. Open CTextBoxWatermark.vb in the code editor. It is here that the extended functionality is implemented.

I want to add 2 properties: one for the text to be shown when the Text property contains a 0-length string, and one for the color in which this text will be drawn.

Public Class UTextBoxWatermark
    '============================================================================
    'VARIABLES.
    '============================================================================
    Private gsWatermarkText As String
    Private glWatermarkColor As Color

    '============================================================================
    'PROPERTIES.
    '============================================================================
    Public Property WatermarkText As String
        Get
            Return gsWatermarkText
        End Get
        Set(sValue As String)
            gsWatermarkText = sValue
        End Set
    End Property

    Public Property WatermarkColor As Color
        Get
            Return glWatermarkColor
        End Get
        Set(lValue As Color)
            glWatermarkColor = lValue
        End Set
    End Property
End Class

To draw the text the OnPaint event is overridden. For text boxes, this event is not called, unless the ControlStyles.UserPaint property is set to True in the constructor. If true, the control paints itself rather than the OS doing so.

Public Class UTextBoxWatermark
    ...

    '============================================================================
    'CONSTRUCTORS AND DESTRUCTORS.
    '============================================================================
    Public Sub New()
        'This call is required by the designer.
        InitializeComponent()

        SetStyle(ControlStyles.UserPaint, True)
    End Sub

    '============================================================================
    'EVENT HANDLERS.
    '============================================================================
    Protected Overrides Sub OnPaint(
        ByVal e As System.Windows.Forms.PaintEventArgs)

        Dim oBrush As SolidBrush

        'If the text is empty now, the watermark text should be written instead.
        If Me.Text.Length = 0 Then
            oBrush = New SolidBrush(glWatermarkColor)
            e.Graphics.DrawString(gsWatermarkText, Me.Font, oBrush, 0, 0)
        End If
    End Sub
End Class

In order to test the component, it must be built now.

Add a new project to this solution via File > Add > New Project. Select Windows Forms App and name the project TBW1_Client. Right-click it in the solutions explorer and select Set as Startup Project.

Add a reference to the text box with watermark project via Project > TBW1_Client Properties > References > Add > Browse > [Path to TBW1] > bin > Debug >TBW1.dll > OK.

Build the project. The control is available in the toolbox now. Doubleclick it to obtain a control in the test window. It should look exactly like an ordinary TextBox control.

Click on the control and check its properties in the properties window. The two newly defined properties WatermarkColor and WatermarkText will be shown near the end of the properties list. To test the functionality, provide a distinct color, for example red, and a text reading, for instance, "Type here".

It works! However, there are two problems:

(1) It works just once when the control is shown for the first time. Typing something and then removing the text leaves the text box empty. I would want to see the watermark text again.

(2) When displaying the watermark text for the first time, the proper font (as inherited from the form) is shown. When starting to type, an ugly system font is used.

How can I solve these 2 problems?

Edit

As per comment from VisualVincent, I improved OnPaint:

Protected Overrides Sub OnPaint(
    ByVal e As System.Windows.Forms.PaintEventArgs)

    Dim oBrush As SolidBrush

    MyBase.OnPaint(e)

    'If the text is empty now, the watermark text should be written instead.
    If Me.Text.Length = 0 Then
        oBrush = New SolidBrush(glWatermarkColor)
        e.Graphics.DrawString(gsWatermarkText, Me.Font, oBrush, 0, 0)
    Else
        oBrush = New SolidBrush(Me.ForeColor)
        e.Graphics.DrawString(Me.Text, Me.Font, oBrush, 0, 0)
    End If
End Sub

and added

Private Sub UTextBoxWatermark_TextChanged(sender As Object, e As EventArgs) _
    Handles Me.TextChanged

    Me.Invalidate()
End Sub

Watermark appears. When starting to write, the text appears, still in ugly System font, though. When I hover with the mouse over it, the text disappers.

When I remove the text, the watermark does not re-appear, unless I hover with the mouse over it.

  • When overriding `OnPaint` you should call `MyBase.OnPaint(e)` before doing anything else (this applies to any overridden method, by the way). Also **(1)** Have you tried checking the text length in `OnTextChanged`, and if it's 0 call `Me.Invalidate()`? – Visual Vincent Apr 07 '18 at 06:00
  • @VisualVincent, thanks for the suggestions. I incorporated them, but the behaviour stll is not as it should be. See the Edit, please. –  Apr 07 '18 at 09:03
  • Very odd that mouse hover causes such strange behaviour, and unfortunately I am in not in a position to test it right now... By the way, do you need to be able to customize this watermark (color, font, etc.)? Because if not there is a built-in watermark in the native text box that can be accessed via P/Invoke. – Visual Vincent Apr 07 '18 at 09:07
  • @VisualVincent, the watermark font is the same as used for the Text, but the color needs to be user definable. –  Apr 07 '18 at 09:32
  • 3
    This cannot work reliably, TextBox dates back from the era where Windows had to run on 386sux hardware. One grave crime it commits to make that work reasonably well is painting without using WM_PAINT. TextBox already supports a watermark well: https://stackoverflow.com/a/4902969/17034 – Hans Passant Apr 08 '18 at 09:43

2 Answers2

1

Edit: Resurrected due to OP giving up on custom fore color requirement.

This feature is supported by the native edit control that the WinForm Textbox class wraps. It is supported in Windows Vista and greater if visual styles are enabled.

reference: EM_SETCUEBANNER message

Example:

Imports System.Runtime.InteropServices

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        SetWaterMark(TextBox1, "Enter Something Here", True)
    End Sub

    <DllImport("user32.dll", CharSet:=CharSet.Unicode)>
    Private Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As Boolean, ByVal lParam As String) As Boolean
    End Function

    Private Shared Sub SetWaterMark(tb As TextBox, waterMarkText As String, Optional showIfFocused As Boolean = True)
        Const ECM_FIRST As Int32 = &H1500
        Const EM_SETCUEBANNER As Int32 = ECM_FIRST + 1
        If VisualStyles.VisualStyleInformation.IsEnabledByUser Then
            SendMessage(tb.Handle, EM_SETCUEBANNER, showIfFocused, waterMarkText)
        End If
    End Sub
End Class
TnTinMn
  • 11,522
  • 3
  • 18
  • 39
1

Edit: I do not recommend using this answer, see comments. Go with TnTinMan's answer.


OnPaint is supposed to do all the drawing correctly. Only that it doesn't for reasons that escape me. (It turns out, that TextBox is not a .Net control. In fact, it is just wrapping a Win32 control.)

However, overriding WndProc's WM_PAINT message does work.

This is a working TextBox with two added properties: WatermarkText and WatermarkColor.

Public Class UTextBoxWatermark
    Inherits TextBox

    '============================================================================
    'CONSTANTS.
    '============================================================================
    Const WM_PAINT As Integer = &HF

    '============================================================================
    'VARIABLES.
    '============================================================================
    Private gsWatermarkText As String = "Watermark"
    Private glWatermarkColor As Color = Color.Gray

    '============================================================================
    'PROPERTIES.
    '============================================================================
    Public Property WatermarkText As String
        Get
            Return gsWatermarkText
        End Get
        Set(sValue As String)
            gsWatermarkText = sValue
            Me.Invalidate()
        End Set
    End Property

    Public Property WatermarkColor As Color
        Get
            Return glWatermarkColor
        End Get
        Set(lValue As Color)
            glWatermarkColor = lValue
            Me.Invalidate()
        End Set
    End Property

    '============================================================================
    'CONSTRUCTORS AND DESTRUCTORS.
    '============================================================================
    Public Sub New()
        'This call is required by the designer.
        InitializeComponent()
    End Sub

    '============================================================================
    'EVENT HANDLERS.
    '============================================================================
    Protected Overrides Sub WndProc(ByRef m As Message)
        MyBase.WndProc(m)

        Dim oBrush As SolidBrush

        If m.Msg = WM_PAINT Then
            'If the text is empty now, the watermark text should be written 
            'instead.
            If Me.Text.Length = 0 Then
                oBrush = New SolidBrush(glWatermarkColor)
                Using oGraphics As Graphics = Me.CreateGraphics
                    oGraphics.DrawString(gsWatermarkText, Me.Font, oBrush, 0, 0)
                End Using
            End If
        End If
    End Sub

    Private Sub UTextBoxWatermark_TextChanged(sender As Object, e As EventArgs) _
        Handles Me.TextChanged

        If Me.Text.Length = 0 Then
            Me.Invalidate()
        End If
    End Sub
End Class
  • 1
    Odd... `OnPaint()` should be raised when a `WM_PAINT` is received. I think what rather solved your problem was `CreateGraphics()` as it creates a new graphics object drawing on top of the text box. By the way you should wrap it in a `Using` statement like so: `Using g As Graphics = Me.CreateGraphics()` then use `g` to draw, otherwise it causes memory leaks. – Visual Vincent Apr 08 '18 at 09:15
  • @VisualVincent, thanks for your patience. However, going with this answer causes all sorts of strange side effects: text sometimes is drawn with a different font, sometimes it overlaps itself, etc. I can not recommend using it. In fact, it is probably best to remove the question altogether. Hans Passant has provided a link to a PInvoke solution in the comments of the original question, probably the same you were mentioning there. –  Apr 09 '18 at 11:58
  • It's the same as what I mentioned, yes. Although you cannot customize the color using that solution. TnTinMn posted a similar answer with example code, but removed it again as I told him you needed to customize the color. A workaround would be to place a label inside the text box (using `Me.Controls.Add()`) and show/hide that instead. – Visual Vincent Apr 09 '18 at 12:19
  • 1
    I got TnTinMn to re-add his answer. :) – Visual Vincent Apr 09 '18 at 13:39
  • The textbox control is a strange beast. What i have done in the past to do custom textbox controls is to do all the drawing myself in the OnPaint and when they click in the control i create a temp Textbox control and place it over the custom control and then copy the text back to the control on the lostfocus and then redraw it with the new text. It doesnt inherit from textbox but uses one internally. – Kevbo Apr 09 '18 at 19:02