Click here to Skip to main content
15,896,513 members
Articles / Desktop Programming / Windows Forms
Tip/Trick

A Simple Number Entry Textbox

Rate me:
Please Sign up or sign in to vote.
1.08/5 (10 votes)
2 Dec 2009CPOL4 min read 50.9K   221   21   9
A simple entry box for various numbers, especially phone numbers.

Introduction

Here's a modified MaskedTextBox that makes it easy to type various numbers, since the numbers "push in" from the right as you type them, the way it does on a pocket calculator.

Background

Here's why I've made this control:

When tabbing into a phone number field or clicking in, I've noticed it's easy to "miss", and you have to do more clicking or arrow-ing to get to the spot where the number looks right when entered.

For example, if you place a MaskedTextBox on your form for telephone number entry, it would usually include the area code digit positions. But what if the user doesn't need to enter an area code?

(___) 555-1212

Problem is, when the user tabs in, the cursor by default would appear at the beginning of the area code portion. Or if the user clicks in, it's easy to click into the control at the wrong spot as well.

But there is an easier way. The way you enter text on your pocket calculator is better. The numbers always "push in" from the right side of the display.

So I decided to make a phone number entry box, which (during entry via the keyboard) only accepts digits and the backspace (or left-arrow does the same). If the number has 7 digits, it will look like the above. If you keep typing, you get all ten digits.

(Oh, you can set the value (the phone number value that is) using the A_Text property. Anyone wants to add the capability to paste a number in?)

By the way, if you click somewhere in the box when a number is already there, it deletes the characters to the right of the click position, for convenience.

Using the Code

Unzip the download and add the two items to your project folder, then open your project and right-click on the project, then "Add..", and finally locate and open CoolNumberBox.vb.

The existing code provides ten different masks for various numbers, including 7-digit, 10-digit, and 11-digit entry for (North American) long distance numbers that need a "1" on the front of the number.

VB.NET
Public Enum NumberMasks
   One_Digit_X
   Two_Digit_XX
   Three_Digit_XXX
   Four_Digit_XXXX
   Five_Digit_XXXXX
   Seven_Digit_555_1212
   Ten_Digit_800_555_1212
   Eleven_Digit_1_800_555_1212
   Area_Code_Plus_Exchange_XXX_XXX
   Time_as_Hours_and_Minutes_X_XX
End Enum

Also, if you wanted to add support for international telephone number entry, you would need to change the masks. If you do, please make sure you adjust the mMaxLen and mEmptyMask variables to agree with your mask, in the Set block of the A_PhoneNumberMask property.

The field mMaxLen represents the length of the "raw" number, so for example: (800) 555-1212 has a length of 10.

Accordingly, the mEmptyMask field has mMaxLen number of spaces.

Finally, the Mask property of MaskedTextBox is set (internally) according to standard masking principles, which you can get by looking up the MaskedTextBox and its properties.

By the way, you'll notice that I have prepended "A_" to my custom property names. This is to cause intellisense to put them at the top of the list. Why? Because, months later, I always forget what my custom property names were, but with this method, they're always right there, and grouped together as a bonus.

The code is annotated, so download the code and give it a try.

Points of Interest

burlyeman24 has found a better way of dealing with the problem of backspacing, which is discussed a bit further down. Before I discuss the problem (which is now fixed!), I'll show the new solution. The key is to call the TheTextChanged sub *before* setting e.Handled=True.

The older, inferior way of handling the problem was attempting to overcome the problem in the OnKeyUp handler:

VB.NET
Protected Overrides Sub OnKeyUp(ByVal e As System.Windows.Forms.KeyEventArgs)
   If Me.ReadOnly Then Exit Sub
   'this serves to refresh the text when the backspace key is let up
   If e.KeyCode = Keys.Back Then TheTextChanged(mTheText)
   MyBase.OnKeyUp(e)
End Sub 

The corrected, better way, located in the OnKeyDown handler, is first calling TheTextChanged, *then* setting e.Handled=True:

VB.NET
Case Keys.Left, Keys.Back
   'remove the last character from our text
   If Len(mTheText) Then mTheText = Mid(mTheText, 1, Len(mTheText) - 1)
   'thanks to burleyman24
   TheTextChanged(mTheText
   e.Handled = True
   Exit Sub 

I used the SelectionStart property of the MaskedTextBox control to force the cursor to sit at the end of the text of the MaskedTextBox. This is basic to my philosophy here, of "pushing in" the characters from the right side, like in a cash register.

VB.NET
Private Sub TheTextChanged(ByVal ControlText As String)
    'left-fill the text of the MaskedTextBox with spaces. 
    'this has the effect of right-justifying the text 

    Text = Mid(mEmptyMask, 1, mMaxLen - Len(ControlText)) + ControlText

    'put the cursor at the end 
    SelectionStart = Mask.Length
End Sub 

I want you to feel free to use and change this code in your own project. If you wish to acknowledge my work within your project, such as in a Thanks To: box, that would be nice. As well, you should respect my Copyright statement at the top of the code, by leaving it there to remind you where it came from.

History

  • July 13, 2007 - First version
  • July 14, 2007 - Converted the project to a component instead of a UserControl
  • November 17, 2008 - Implemented a superior way of backspacing, thanks to burleyman24
  • Dec 2, 2009 - Editing changes for clarity, updated zip to the latest version I have

License

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


Written By
Software Developer (Senior) Binary Meld Communications
Canada Canada
Started programming the Apple II+, then Mac, then Windows.
Still love the creation / invention process.

Comments and Discussions

 
GeneralMaskedTextBox Del key Problem Pin
EMYNA28-Oct-09 10:14
EMYNA28-Oct-09 10:14 
GeneralRe: MaskedTextBox Del key Problem Pin
bobishkindaguy28-Oct-09 11:22
bobishkindaguy28-Oct-09 11:22 
Hi EMYNA,

I'm not sure what problem you are having related to "e.SuppressKeyPress = True don't work". You should give more information about that.

Regarding the Date Format:

Just in case the article doesn't have the latest code published, here is the latest code I have, which works for all the formats currently defined.

I have added some comments for you in the code below. Search for "EMYNA".

I added the date pattern to Public Enum NumberMasks.
I added the definition of the date format mask to Public Property A_NumberMask.

What if you want to validate the date? I added the event CoolTextChanged to the component.

I also added a RaiseEvent statement in Private Sub TheTextChanged.

You must handle that event within your code. You could put a label on your form, below the control, that has a text property of "Invalid" until you are happy that the date is correct, at which point you could change the label text to "Valid".

Please let me know if you are successful, and I will consider adding and publishing the code for others to use.

Here is the latest code:

'Copyright © 2007 Robert W. Macleod
'bobishkindaguy@hotmail.com
'Please feel free to use this code in your projects.
'But please leave this copyright here so you know who originally authored it.

Public Class CoolNumberBox
  Inherits MaskedTextBox

#Region "Properties/Fields"

  ''' <summary>
  ''' Used to pad the output to make it right-justified.
  ''' </summary>
  ''' <remarks></remarks>
  Private mEmptyMask As String

  ''' <summary>
  ''' The maximum length of the raw digits maintained by the control.
  ''' </summary>
  ''' <remarks></remarks>
  Private mMaxLen As Integer

  ''' <summary>
  ''' Had to do this because the built-in readonly property 
  ''' makes the control look different
  ''' </summary>
  ''' <remarks></remarks>
  Private mReadOnly As Boolean

  ''' <summary>
  ''' The currently available masks. If you add more, amend the A_NumberMask property accordingly.
  ''' </summary>
  ''' <remarks></remarks>
  Public Enum NumberMasks
    One_Digit_X
    Two_Digit_XX
    Three_Digit_XXX
    Four_Digit_XXXX
    Five_Digit_XXXXX
    Seven_Digit_555_1212
    Ten_Digit_800_555_1212
    Eleven_Digit_1_800_555_1212
    Area_Code_Plus_Exchange_XXX_XXX
    Time_as_Hours_and_Minutes_X_XX
    'EMYNA
    Date_XX/XX/XXXX
  End Enum

  ''' <summary>
  ''' Holds the current mask
  ''' </summary>
  ''' <remarks></remarks>
  Private mNumberMask As NumberMasks

  ''' <summary>
  ''' Sets or Gets the current NumberMask.
  ''' </summary>
  ''' <value></value>
  ''' <returns></returns>
  ''' <remarks></remarks>
  Public Property A_NumberMask() As NumberMasks
    Get
      Return mNumberMask
    End Get
    Set(ByVal value As NumberMasks)
      mNumberMask = value
      Select Case value
        Case NumberMasks.One_Digit_X
          mMaxLen = 1
          Mask = "9"
        Case NumberMasks.Two_Digit_XX
          mMaxLen = 2
          Mask = "99"
        Case NumberMasks.Three_Digit_XXX
          mMaxLen = 3
          Mask = "999"
        Case NumberMasks.Four_Digit_XXXX
          mMaxLen = 4
          Mask = "9999"
        Case NumberMasks.Five_Digit_XXXXX
          mMaxLen = 5
          Mask = "99999"
        Case NumberMasks.Seven_Digit_555_1212
          mMaxLen = 7
          Mask = "999-9999"
        Case NumberMasks.Ten_Digit_800_555_1212
          mMaxLen = 10
          Mask = "(999) 999-9999"
        Case NumberMasks.Eleven_Digit_1_800_555_1212
          mMaxLen = 11
          Mask = "9-(999) 999-9999"
        Case NumberMasks.Area_Code_Plus_Exchange_XXX_XXX
          mMaxLen = 6
          Mask = "(999) 999"
        Case NumberMasks.Time_as_Hours_and_Minutes_X_XX
          mMaxLen = 3
          Mask = "9:99"

        'EMYNA
        Case NumberMasks.Date_XX/XX/XXXX
          mMaxLen = 8
          Mask = "99/99/9999"

      End Select
      'set the empty mask to the size for the mask chosen.
      'the empty mask is used to pad the output to make it
      'right-justified.
      'mEmptyMask = Space(mMaxLen)
      mEmptyMask = Replace(Space(mMaxLen), Space(1), "_")
      'now in case mMaxLen is less than it was before,
      'reduce the size of the control's text accordingly.
      'also, remove the left-most characters, since a smaller mask
      'tends to be reduced on the left-hand side.  
      If mTheText.Length > mMaxLen Then
        mTheText = Mid(mTheText, mTheText.Length - mMaxLen + 1, mMaxLen)
      End If
      'now, update the user interface accordingly.
      TheTextChanged(mTheText)
    End Set
  End Property

  ''' <summary>
  ''' Contains the internal raw characters of the phone number.
  ''' </summary>
  ''' <remarks></remarks>
  Private mTheText As String = ""

  ''' <summary>
  ''' Allows controlled access to our mTheText field. 
  ''' </summary>
  ''' <value></value>
  ''' <returns></returns>
  ''' <remarks></remarks>
  Public Property A_Text() As String
    Get
      Return mTheText
    End Get
    Set(ByVal value As String)
      mTheText = ""
      For i As Integer = 1 To Len(value)
        'only add numeric parts of the incoming value
        If IsNumeric(Mid(value, i, 1)) Then
          mTheText += Mid(value, i, 1)
          'prevent overfilling if incoming value is too big for the box
          If Len(mTheText) = mMaxLen Then Exit For
        End If
      Next
      TheTextChanged(mTheText)
    End Set
  End Property

  ''' <summary>
  ''' Returns a nicely formatted string according to the current mask
  ''' </summary>
  ''' <value></value>
  ''' <returns></returns>
  ''' <remarks></remarks>
  Public ReadOnly Property A_FormattedText() As String
    Get
      TextMaskFormat = MaskFormat.IncludeLiterals
      Return Text
      TextMaskFormat = MaskFormat.ExcludePromptAndLiterals
    End Get
  End Property

  ''' <summary>
  ''' Had to do this because the built-in readonly property 
  ''' makes the control look different
  ''' </summary>
  ''' <value></value>
  ''' <returns></returns>
  ''' <remarks></remarks>
  Public Property A_ReadOnly() As Boolean
    Get
      Return mReadOnly
    End Get
    Set(ByVal value As Boolean)
      mReadOnly = value
    End Set
  End Property

  ''' <summary>
  ''' We don't allow the built-in readonly to  work.
  ''' </summary>
  ''' <param name="e"></param>
  ''' <remarks></remarks>
  Protected Overrides Sub OnReadOnlyChanged(ByVal e As System.EventArgs)
    Me.ReadOnly = False
    'and don't let this go through
    MyBase.OnReadOnlyChanged(e)
  End Sub

  ''' <summary>
  ''' Gets raised when user selects some text with mouse,
  ''' which is then removed. Could confuse user.
  ''' </summary>
  ''' <remarks></remarks>
  Public Event SelectedTextRemoved()

  'EMYNA
  Public Event CoolTextChanged

#End Region

#Region "Overrides"

  ''' <summary>
  ''' Sets some startup values
  ''' </summary>
  ''' <remarks></remarks>
  Protected Overrides Sub OnCreateControl()
    MyBase.OnCreateControl()
    AllowPromptAsInput = False
    Me.TextMaskFormat = MaskFormat.ExcludePromptAndLiterals
    Me.CutCopyMaskFormat = MaskFormat.ExcludePromptAndLiterals
  End Sub

  ''' <summary>
  ''' Control the user's keypresses. We allow digits, the left-arrow and backspace keys.
  ''' </summary>
  ''' <param name="e"></param>
  ''' <remarks></remarks>
  Protected Overrides Sub OnKeyDown(ByVal e As System.Windows.Forms.KeyEventArgs)

    If Me.mReadOnly Then
      TheTextChanged(mTheText)
      Beep() : Exit Sub
    End If

    'this stops the key from being processed by the control itself. We are in control instead.
    'unfortunately, this scheme does not work for the backspace key, so see below how we work around that.
    e.SuppressKeyPress = True

    Select Case e.KeyCode
      Case Keys.Right, Keys.Down, Keys.Up, Keys.Delete
        'we don't permit these
      Case Keys.Left, Keys.Back
        'for the backpace key. we have to do special things to make this go properly. see OnKeyUp.
        'remove the last character from our text
        If Len(mTheText) > 0 Then mTheText = Mid(mTheText, 1, Len(mTheText) - 1)
        '\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
        'thanks to burleyman24 Nov 2008   
        'code project CoolPhoneBox
        'http://www.codeproject.com/script/Forums/View.aspx?fid=438497&msg=2808588
        TheTextChanged(mTheText)
        e.Handled = True
        Exit Sub
        '\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
      Case Keys.NumPad0 To Keys.NumPad9
        'only allow entry to the max length permitted
        If Len(mTheText) < mMaxLen Then
          'convert the number pad key to ordinary numbers
          mTheText += Chr(e.KeyCode - 48)
        End If
      Case Keys.D0 To Keys.D9
        'only allow entry to the max length permitted
        If Len(mTheText) < mMaxLen Then
          'just an ordinary number keypress (the numbers at the top of the keyboard)
          mTheText += Chr(e.KeyCode)
        End If
    End Select

    'do our own adding to the masked textbox
    TheTextChanged(mTheText)
    MyBase.OnKeyDown(e)
  End Sub

  '\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
  'thanks to burleyman24 Nov 2008   
  'code project CoolPhoneBox
  'http://www.codeproject.com/script/Forums/View.aspx?fid=438497&msg=2808588
  Private Sub CoolNumberBox_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Click
    'SelectionStart = Mask.Length
  End Sub

  ''' <summary>
  ''' Ensures that the cursor is at the end if the user clicks in
  ''' </summary>
  ''' <param name="mevent"></param>
  ''' <remarks></remarks>
  Protected Overrides Sub OnMouseUp(ByVal mevent As System.Windows.Forms.MouseEventArgs)
    MyBase.OnMouseUp(mevent)
    'nov 2007 trying to delete to end when user selects some of the number
    If mReadOnly Then Exit Sub
    Me.Select(SelectionStart, Mask.Length - SelectionStart)
    Me.SelectedText = ""
    Me.mTheText = Trim(Me.Text)
    TheTextChanged(mTheText)
    SelectionStart = Mask.Length
    RaiseEvent SelectedTextRemoved()
  End Sub

  ''' <summary>
  ''' Ensures that the cursor is at the end if the user tabs in
  ''' </summary>
  ''' <param name="e"></param>
  ''' <remarks></remarks>
  Protected Overrides Sub OnEnter(ByVal e As System.EventArgs)
    MyBase.OnEnter(e)
    SelectionStart = Mask.Length
  End Sub

  ''' <summary>
  '''  Ensures that the cursor is at the end if the program gives it focus
  ''' </summary>
  ''' <param name="e"></param>
  ''' <remarks></remarks>
  Protected Overrides Sub OnGotFocus(ByVal e As System.EventArgs)
    MyBase.OnGotFocus(e)
    SelectionStart = Mask.Length
  End Sub

  'was: This prevents the incorrect behaviour of the backspace key from leaving the control in the wrong state.
  '\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
  'thanks to burleyman24 Nov 2008   
  'code project CoolPhoneBox
  'http://www.codeproject.com/script/Forums/View.aspx?fid=438497&msg=2808588
  'Protected Overrides Sub OnKeyUp(ByVal e As System.Windows.Forms.KeyEventArgs)

  '  If Me.ReadOnly Then Exit Sub

  '  'this serves to refresh the text when the backspace key is let up
  '  'If e.KeyCode = Keys.Back Then TheTextChanged(mTheText)
  '  MyBase.OnKeyUp(e)
  'End Sub

#End Region

#Region "Private subs"

  ''' <summary>
  ''' Right-justify the text in the control and ensure that the cursor is at the right-hand side.
  ''' </summary>
  ''' <param name="ControlText"></param>
  ''' <remarks></remarks>
  Private Sub TheTextChanged(ByVal ControlText As String)
    'left-fill the text of the maskedtextbox with spaces.
    'this has the effect of right-justifying the text
    Text = Mid(mEmptyMask, 1, mMaxLen - Len(ControlText)) + ControlText
    'put the cursor at the end
    SelectionStart = Mask.Length

    'EMYNA
    RaiseEvent CoolTextChanged

  End Sub

#End Region

End Class

QuestionRe: MaskedTextBox Pin
theojero4-Nov-09 7:47
theojero4-Nov-09 7:47 
AnswerRe: MaskedTextBox Pin
bobishkindaguy2-Dec-09 17:59
bobishkindaguy2-Dec-09 17:59 
GeneralBetter backspace - no flicker [modified] Pin
burleyman2416-Nov-08 18:11
burleyman2416-Nov-08 18:11 
GeneralRe: Better backspace - no flicker Pin
bobishkindaguy17-Nov-08 6:28
bobishkindaguy17-Nov-08 6:28 
GeneralRe: Better backspace - no flicker Pin
burleyman2418-Nov-08 4:35
burleyman2418-Nov-08 4:35 
QuestionVC8.0 ? Pin
toxcct15-Jul-07 22:19
toxcct15-Jul-07 22:19 
AnswerRe: VC8.0 ? Pin
bobishkindaguy16-Jul-07 6:15
bobishkindaguy16-Jul-07 6:15 

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

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