Click here to Skip to main content
15,897,133 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 
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 
Hey burleyman24,

How nice of you to share your solution here! And I'm elated that someone actually found it useful. I still use this enhancement throughout my project. There is another solution here on CodeProject that makes the cursor "skip" over the literals, and I used that for a while, but I abandoned it. Can't remember why. Anyway, I like the way a cash register enters numbers.

Regarding your ingenious fix: I tried til my face was blue to overcome that flicker, and ended up putting a call to TheTextChanged in the OnKeyUp handler, so that when the backspace key was lifted, the last character disappeared. But it still looked strange.

Your solution is better! The key to the solution seems to be to call TheTextChanged *before* setting e.Handled=True

Well, since I submitted this article, a couple of things happened. First, I noticed that a number of people gave it a "1", which did discourage me a little, and so I didn't post any improvements. Since then I have concluded that since the code basically works and does fill a need, those friends must have given it a "1" because they didn't find it useful to them. I learned something from that. I will always give a rating to an article based on whether I find it well done, not on whether I intend to use it.

But I have enhanced the control in the meantime, and so here I post the latest code. It now works for all these number types.
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


So I'll just paste the code here, then if I can figure out how to do it, I'll update the article with the new code as well.

Thanks again and kudos to burleyman24!
(Code follows)
Bob

'Copyright © 2007-2008 Robert W. Macleod
'bobishkindaguy@hotmail.com

'thanks to burleyman24 for valuable enhancements to the code November 2008

'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
  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"
      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()

#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
    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

  '\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
  'thanks to burleyman24 Nov 2008   
  'code project CoolPhoneBox
  'http://www.codeproject.com/script/Forums/View.aspx?fid=438497&msg=2808588
  'was: This prevents the incorrect behaviour of the backspace key from leaving the control in the wrong state.
  ' (now remmed) 
  '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
  End Sub

#End Region

End Class


________
42 is definitely not the meaning of life.
I knew that when I turned 43.

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.