Click here to Skip to main content
Licence CPOL
First Posted 12 Oct 2006
Views 138,457
Bookmarked 143 times

C# TextBox with Outlook 2007-style prompt

By | 16 Oct 2006 | Article
An article on creating a prompted textbox in the style of Outlook 2007, IE7, and Firefox 2.0.

PromptedTextBox Sample

Introduction

You've probably seen these types of textboxes on your travels around the web, such as the eBay search box, which is a textbox with a prompt in it like "Enter your search string here". As soon as you click in the box, the prompt disappears, leaving an empty textbox where you can type your search string. Microsoft even has an AJAX example of this control on their Atlas web site, called the TextBoxWatermark control.

More recently, this type of control has started to appear in Windows applications like Outlook 2007, IE7, and also Firefox 2.0. This control can be very handy, as it basically works like a Textbox and a Label in one control without taking up a bunch of screen real estate. On the web, this nifty feature is usually handled by using JavaScript, but on Windows, we're left to our own devices to come up with something that provides the same functionality.

Background

In a web application, the developer would typically add JavaScript code to the onBlur and onFocus events in order to put the prompt in the TextBox. The developer must also be aware that the form submit (or "postback" in ASP.NET) may cause the prompt text to be included in the postdata, so the code must be aware of this. In addition, the JavaScript code is forced to manipulate the "value" property of the TextBox in order to accomplish this, so even the JavaScript solution has a "hack" feel to it.

The standard WinForms TextBox does not support this functionality natively, so this class will address that shortcoming by inheriting from System.Windows.Forms.TextBox and handling the display of the prompt text ourselves.

Using the code

Overriding the WndProc may seem like overkill, but in this case the code turns out to be fairly simple. For this control, we only need to be concerned about the WM_SETFOCUS and WM_KILLFOCUS messages, which should be the same as the GotFocus and SetFocus events, and WM_PAINT:

protected override void WndProc(ref System.Windows.Forms.Message m)
{
    switch (m.Msg)
    {
        case WM_SETFOCUS:
            _drawPrompt = false;
            break;

        case WM_KILLFOCUS:
            _drawPrompt = true;
            break;
    }

    base.WndProc(ref m);

    // Only draw the prompt on the WM_PAINT event
    // and when the Text property is empty
    if (m.Msg == WM_PAINT && _drawPrompt && this.Text.Length == 0 && 
                      !this.GetStyle(ControlStyles.UserPaint))
        DrawTextPrompt();
}

DrawTextPrompt does most of the work. It determines the client rectangle in which to draw the prompt and any offset based on the HorizontalAlignment, then uses TextRenderer to draw the PromptText inside the rectangle:

protected virtual void DrawTextPrompt(Graphics g)
{
    TextFormatFlags flags = TextFormatFlags.NoPadding | 
      TextFormatFlags.Top | TextFormatFlags.EndEllipsis;
    Rectangle rect = this.ClientRectangle;

    // Offset the rectangle based on the HorizontalAlignment, 
    // otherwise the display looks a little strange
    switch (this.TextAlign)
    {
        case HorizontalAlignment.Center:
            flags = flags | TextFormatFlags.HorizontalCenter;
            rect.Offset(0, 1);
            break;

        case HorizontalAlignment.Left:
            flags = flags | TextFormatFlags.Left;
            rect.Offset(1, 1);
            break;

        case HorizontalAlignment.Right:
            flags = flags | TextFormatFlags.Right;
            rect.Offset(0, 1);
            break;
    }

    // Draw the prompt text using TextRenderer
    TextRenderer.DrawText(g, _promptText, this.Font, rect, 
                      _promptColor, this.BackColor, flags);
}

Points of Interest

My first thought when tackling this control was to override OnGotFocus/OnLostFocus and just swap the Text and PromptText values (in addition to changing the ForeColor). This immediately turned out to be a bad idea, because as soon as I tested it I noticed that the Text property at design-time was suddenly replaced with the PromptText (and so was the ForeColor replaced with the PromptForeColor). This effectively removed the ability for the developer to set a default Text value, so it was time for a new approach.

After mulling over a couple of alternatives, I decided to override the OnPaint method and just manually draw the prompt over top of the TextBox region. This would solve the "hack" nature of trying to manipulate the Text/ForeColor properties. So I developed the DrawTextPrompt function and added a call in OnPaint. Unfortunately, this also turned out to be a problematic solution (and I have left the code in so you can test it for yourself). The prompt would simply not draw properly over top of the TextBox, displaying various weird behaviors such as disappearing text, incorrect fonts, etc. My first thought was that I had coded DrawTextPrompt incorrectly, but a simple test showed that it was indeed drawing the prompt correctly.

So after pulling my hair out some more, I finally rolled up my sleeves and decided to override WndProc. I knew that I would have to figure out which message was the key to the solution (WM_PAINT was the obvious choice, but wasn't that what OnPaint was supposed to do?). After a few hours of frustration, and not having any other candidates, I decided to try and see if WM_PAINT would produce any different results. So I added the call to DrawTextPrompt and, lo and behold, it worked!!! I don't yet have an explanation as to why WM_PAINT works and OnPaint doesn't, but you can try it for yourself by un-commenting the SetStyle line in the constructor. My working theory is that the SetStyle call disables additional behavior that I have not accounted for.

As you go through the code, notice all the places where there is a call to Invalidate(). This is so the control will redraw itself as the design-time properties change. The control should behave the same at design-time as it does at run-time. If there is a value in the Text property, the PromptText will not be displayed. If you change the PromptText, PromptForeColor or the TextAlign properties, the control should change right away.

I also added a FocusSelect boolean property (a feature I have longed for since VB3, where inheritance was not an option) that, when set to true, will select all the text in the control when it receives the focus.

Compared to EM_SETCUEBANNER

As pointed out in the user comments below, Windows XP and newer has a message that you can send to a TextBox control that will accomplish almost the same goal, and that is EM_SETCUEBANNER. Using this message has a few advantages:

  • Also supported by the ComboBox control
  • Forward compatibility with future versions of Windows
  • Better support for different UI enhancements and layouts, including things like themes, Aero, TabletPC, etc.

But, there are also some disadvantages:

  • Only supported on Windows XP and newer.
  • Windows CE/Mobile is not supported.
  • Doesn't work with a multiline TextBox.
  • Developer must also include a manifest for Comctl32.dll with the EXE.
  • Developer has no control over the prompt's font properties or color.
  • Apparently, there is a bug that causes EM_SETCUEBANNER not to work if you install one of the Asian language packs on XP.

I haven't verified that PromptedTextBox runs on all the older platforms (or CE), but the approach doesn't require anything special. In fact, the theory should apply as far back as Windows 3.1, but of course .NET only supports Windows 98 and above.

In short, if you find a situation where PromptedTextBox doesn't behave as expected and you have the option, see if EM_SETCUEBANNER will do the trick. If not, drop me a line and I will see what I can do.

Release History

  • Version 1.0: 04-Oct-2006 - Initial posting.
  • Version 1.1: 16-Oct-2006 - Added the PromptFont property to allow more developer control of the prompt display. Also changed the default prompt color to SystemColors.GrayText.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Matthew Noonan

Web Developer

United States United States

Member

I have been a software developer for almost 20 years, focusing primarily on Microsoft based technologies. I recently started my own consulting company and haven't looked back.
 
I also developed a product called EasyObjects.NET to enable developers to produce better code in a shorter time frame.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralCool! PinmemberDiamonddrake8:15 17 Dec '10  
GeneralMy vote of 4 Pinmembersopheak.programming16:06 13 Nov '10  
GeneralThanks.... PinmemberArjen H.21:19 27 Sep '10  
GeneralNIcely done Matt PinmemberElkay18:16 15 Mar '09  
QuestionHow to do it for the System.Windows.Forms.ToolStripTextBox? PinmemberMartin Radu11:06 9 Mar '08  
GeneralRe: How to do it for the System.Windows.Forms.ToolStripTextBox? PinmemberMatthew Noonan11:43 9 Mar '08  
GeneralFocusSelect PinmemberJohnny J.21:34 8 Nov '07  
GeneralRe: FocusSelect PinmemberJohnny J.21:34 8 Nov '07  
QuestionHow to do it for the ComboBox PinmemberAbo Yahya13:11 26 Oct '07  
AnswerRe: How to do it for the ComboBox PinmemberMatthew Noonan19:18 26 Oct '07  
GeneralRe: How to do it for the ComboBox PinmemberAbo Yahya23:56 26 Oct '07  
I used some code from CodeProject to build a ReadOnly ComboBox (you can copy the code as is though ofcourse I am using VB.NET)
1- Build a new PromptedComboBox that inherits from ComboBox
 
2- Add/Overrides properties and methods same as done for the propmpted TextBox
 
3- add the following properties/methods to the class
 
   Friend ReadOnly Property DrawPrompt() As Boolean
         Get
            Return _drawPrompt
         End Get
   End Property
 
   Private _promptTextAlign As HorizontalAlignment = HorizontalAlignment.Left
   Public Property PromptTextAlign() As HorizontalAlignment
         Get
            Return _promptTextAlign
         End Get
         Set(ByVal value As HorizontalAlignment)
            _promptTextAlign = value
            Me.Invalidate()
         End Set
   End Property
 
   Friend Function GetControlStyle(ByVal flag As ControlStyles) As Boolean
         Return MyBase.GetStyle(flag)
   End Function
 
5- Override the HandleCreated to hook the messege listener
 
   Private _wmIntercept As ComboBoxInterceptWindowMessages = Nothing
   Protected Overrides Sub OnHandleCreated(ByVal args As EventArgs)
         MyBase.OnHandleCreated(args)
 
         ' Create the 'ComboBoxNativeWindow' class to handle interception of windows messages.
         _wmIntercept = New ComboBoxInterceptWindowMessages(Me)
   End Sub
 
6- In a separate class file, build this class to intercept the comboBoxe's messages and expose its handles
 
Imports System.Runtime.InteropServices
 
Friend Class ComboBoxInterceptWindowMessages
 
   Private Enum NativeWindowType As Integer
         ComboCtl = 0   ' the ComboBox control as a whole
         EditBox            ' the EditBox portion of the ComboBox
         ListBox            ' the ListBox (DropDown) portion of the ComboBox
   End Enum
 
   Private m_cbxinfo As Win32.ComboBoxInfo
   Private m_nwCtrl As ComboBoxExNativeWindow = Nothing
   Private m_nwEdit As ComboBoxExNativeWindow = Nothing
   Private m_nwList As ComboBoxExNativeWindow = Nothing
 
   Public Sub New(ByVal parentComboBox As ComboBox)
 
         m_cbxinfo = New Win32.ComboBoxInfo()
         m_cbxinfo.cbSize = Marshal.SizeOf(m_cbxinfo)
         Win32.GetComboBoxInfo(parentComboBox.Handle, m_cbxinfo)
 
         m_nwCtrl = New ComboBoxExNativeWindow(parentComboBox, NativeWindowType.ComboCtl, parentComboBox.Handle)
         m_nwEdit = New ComboBoxExNativeWindow(parentComboBox, NativeWindowType.EditBox, m_cbxinfo.hwndEdit)
         m_nwList = New ComboBoxExNativeWindow(parentComboBox, NativeWindowType.ListBox, m_cbxinfo.hwndList)
   End Sub
 
   Public ReadOnly Property hwndCombo() As IntPtr
         Get
            Return m_cbxinfo.hwndCombo
         End Get
   End Property
 
   Public ReadOnly Property hwndEdit() As IntPtr
         Get
            Return m_cbxinfo.hwndEdit
         End Get
   End Property
 
   Public ReadOnly Property hwndList() As IntPtr
         Get
            Return m_cbxinfo.hwndList
         End Get
   End Property
 

#Region "Class: ComboBoxExNativeWindow "
 
   Private Class ComboBoxExNativeWindow
         Inherits System.Windows.Forms.NativeWindow
 
         Const WM_PAINT As Integer = 15
 
         Const WM_COMMAND As Int32 = &H111
         Const WM_USER As Int32 = &H400
         Const OCM__BASE As Int32 = WM_USER + &H1C00
         Const OCM_COMMAND As Int32 = OCM__BASE + WM_COMMAND
 

         Private m_parentComboBox As ComboBox = Nothing
         Private m_nwType As NativeWindowType
 
         Public Sub New(ByVal parentComboBox As ComboBox, ByVal nwType As NativeWindowType, ByVal hwnd As IntPtr)
            m_parentComboBox = parentComboBox
            m_nwType = nwType
 
            If Not Me.Handle.Equals(IntPtr.Zero) Then
                  Me.ReleaseHandle()
            End If
            Me.AssignHandle(hwnd)
         End Sub
 
         Protected Overloads Overrides Sub WndProc(ByRef message As System.Windows.Forms.Message)
 
            MyBase.WndProc(message)
 
            If m_nwType = NativeWindowType.EditBox AndAlso TypeOf m_parentComboBox Is PromptedComboBox Then
 
                  Dim pcb As PromptedComboBox = DirectCast(m_parentComboBox, PromptedComboBox)
 
                  ' Only draw the prompt on the WM_PAINT event and when the Text property is empty
                  If message.Msg = WM_PAINT AndAlso pcb.DrawPrompt AndAlso pcb.Text.Length = 0 AndAlso Not pcb.GetControlStyle(ControlStyles.UserPaint) Then
                     pcb.DrawTextPrompt()
                  End If
 
                  If (message.Msg = WM_PAINT AndAlso pcb.DrawPrompt AndAlso Not pcb.GetControlStyle(ControlStyles.UserPaint)) _
                  OrElse (message.Msg = OCM_COMMAND AndAlso pcb.DrawPrompt AndAlso Not pcb.GetControlStyle(ControlStyles.UserPaint)) Then
                     pcb.DrawTextPrompt()
                  End If
 
            End If
         End Sub
 
   End Class
 
#End Region
 
End Class
 
7- This is the Win32 module exposing native windows methods
 
Imports System.Runtime.InteropServices
 
Friend Module Win32
 
   <StructLayout(LayoutKind.Sequential)> _
   Public Structure RECT
         Public left As Integer
         Public top As Integer
         Public right As Integer
         Public bottom As Integer
   End Structure
 
   <StructLayout(LayoutKind.Sequential)> _
   Public Structure ComboBoxInfo
         Public cbSize As Integer
         Public rcItem As RECT
         Public rcButton As RECT
         Public stateButton As IntPtr
         Public hwndCombo As IntPtr
         Public hwndEdit As IntPtr
         Public hwndList As IntPtr
   End Structure
 
   <DllImport("user32")> _
   Public Function GetComboBoxInfo(ByVal hwndCombo As IntPtr, ByRef info As ComboBoxInfo) As Boolean
   End Function
 
End Module
 
As you see, I handled the same messages as in the prompted TextBox but somehow the prompt-text does not appear in the combo-box although debugging shows it is been drawn correctly?
 
I will still try the EM_SETCUEBANNER although I prefer your approach to avoid its short-comings.
 
Many thanks for the help and for sharing your wisdom Smile | :)
GeneralSuggestion: keep prompt until text entered Pinmembermitooki5:51 6 Jul '07  
GeneralRe: Suggestion: keep prompt until text entered Pinmemberk^s1:34 17 Sep '07  
GeneralRe: Suggestion: keep prompt until text entered Pinmemberveritas guy11:16 7 Dec '07  
GeneralPrompted text with MaskTextBox PinmemberN U Reddy7:53 13 Jun '07  
GeneralRe: Prompted text with MaskTextBox PinmemberMatthew Noonan11:32 19 Jun '07  
GeneralSuggestion PinmemberSk8tz22:29 23 May '07  
GeneralRe: Suggestion PinmemberN U Reddy7:56 13 Jun '07  
GeneralRe: Suggestion PinmemberMatthew Noonan11:34 19 Jun '07  
GeneralAnother suggestion Pinmemberlextm20:59 19 Apr '07  
GeneralRe: Another suggestion PinmemberMatthew Noonan11:38 19 Jun '07  
QuestionNot working for Windows 2000 PinmemberJason Law20:30 21 Jan '07  
AnswerRe: Not working for Windows 2000 PinmemberMatthew Noonan14:12 5 Feb '07  
GeneralRe: Not working for Windows 2000 Pinmembervachaun5:55 26 Apr '07  
GeneralRe: Not working for Windows 2000 PinmemberMatthew Noonan11:40 19 Jun '07  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web01 | 2.5.120529.1 | Last Updated 16 Oct 2006
Article Copyright 2006 by Matthew Noonan
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid