(Print this page)

Watermark in Windows Forms
Published date: Sunday, November 26, 2017
On: Moer & Éric Moreau's web site

I really hate when HTML forms shows the label into the edit controls without any labels. It is frustrating when you have tools that automatically fill controls but once filled you don’t know if the right information has been set to the correct control. Without a label, you don’t know if the first name field is filled correctly!

But after this rant, this feature of showing a watermark is very useful to display additional information like the format or a mandatory indicator or … Lately, I found out that it was easy to add that kind of watermark to some Windows Forms controls. This article shows how to add watermark to Textbox and Combobox controls.

Figure 1: The demo application in action

The downloadable code

This month solution contains both VB and C# projects. The solution was created using Visual Studio 2017 but the code should work as well in older version of the.Net Framework because there isn’t really nothing very fancy.

Creating the form

Nothing fancy, just 3 textboxes and 1 combobox keeping default properties just to prove that the code is working.

The form load’s event simply contains 4 lines of code to set the watermark of each control:

textBox1.SetWatermark("first watermark")
textBox2.SetWatermark("second watermark")
textBox3.SetWatermark("third watermark")
ComboBox1.SetWatermark("combo watermark")
textBox1.SetWatermark("first watermark");
textBox2.SetWatermark("second watermark");
textBox3.SetWatermark("third watermark");
ComboBox1.SetWatermark("combo watermark");

So far, this code will not compile because the SetWatermark method does not exist.

The SetWatermark extension method

To use the method easily, it is best to create an extension method. In VB, we create the method in a Module (mine is called mTBWatermark) while in C#, we create a static class (mine is called cTBWatermark).

The trick here is simply to send a message (using SendMessage from user32.dll) to the control using EM_SETCUEBANNER or CB_SETCUEBANNER. Everything else will be managed automatically.

Option Strict On

Imports System.Runtime.InteropServices

Public Module mTBWatermark

    <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Function SendMessage(ByVal hWnd As HandleRef, 
                                        ByVal msg As UInteger, 
                                        ByVal wParam As IntPtr, 
                                        ByVal lParam As String) As IntPtr
    End Function
    
    <DebuggerStepThrough()>
    <Runtime.CompilerServices.Extension()>
    Public Sub SetWatermark(ByVal ctl As Control, ByVal text As String)
        Const EM_SETCUEBANNER as Int32 = &h1501
        Const CB_SETCUEBANNER As Int32 = &h1703
        
        Dim retainOnFocus As IntPtr = new IntPtr(1)
        Dim msg As uInteger = EM_SETCUEBANNER

        If TypeOf ctl is ComboBox
            msg = CB_SETCUEBANNER
        End If

        SendMessage(New HandleRef(ctl, ctl.Handle), msg, retainOnFocus, text)
    End Sub

End Module
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace EmoreauDemoWaterMarkCS
{
    public static class cWatermark
    {

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern Int32 SendMessage(HandleRef hWnd, uint msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);

        public static void SetWatermark(this Control ctl, string text)
        {
            const int EM_SETCUEBANNER = 0x1501;
            const int CB_SETCUEBANNER = 0x1703;

            IntPtr retainOnFocus = new IntPtr(1);
            uint msg = EM_SETCUEBANNER;
            if (ctl is ComboBox)
                msg = CB_SETCUEBANNER;

            SendMessage(new HandleRef(ctl, ctl.Handle), msg, retainOnFocus, text);
        }
    }
}

After this code as been added, your call to the SetWatermark method will now compile.

Conclusion

I have seen other code trying to handle the GotFocus/LostFocus events and checking if the control was empty, and … It ended up generating a lot of code and not always working correctly.

This implementation is short and sweet just like I like them.

Another fun and useful feature to add to your Textbox base control!


(Print this page)