Click here to Skip to main content
15,885,757 members
Articles / Web Development / HTML

Extended NumericUpDown Control

Rate me:
Please Sign up or sign in to vote.
4.89/5 (36 votes)
23 Sep 2020CPOL5 min read 256.5K   2.8K   124   66
An extended NumericUpDown control with better focus and mouse wheel management.

Image 1

Introduction

If you have ever written a data-entry application, there's a big chance you used the NumericUpDown control. This control is great to provide a field to enter numeric values, with advanced features like up-down buttons and accelerating auto-repeat.

The other side of the coin is that NumericUpDown is not really mouse-aware. I experienced some bugs and bad behaviors:

  • I need to select all the text when it gets focus (see below), but it misses some of the TextBox properties, like SelectedText, SelectionStart, SelectionLength (an AutoSelect property will be useful).
  • Some of the standard events are not working properly (see below): MouseEnter, MouseLeave.
  • Rotating the mouse wheel when the control is focused causes its value to change. A property to change this behavior, like InterceptArrowsKeys for up/down keys, will be useful.

That's why I decided to subclass it, fixing these points and adding missing features and properties.

Missing TextBox Properties

I needed some missing TextBox properties when I was asked to select all the text in the control when it gets the focus.

Yes, NumericUpDown exposes a Select(int Start, int Length) method you can call to select all text. At first try, I attached to the GotFocus event to call Select(0, x) but, hey, wait a moment... what should I use for x? It seems that any value is accepted, even if greater than the text length. OK, let's say x=100 and proceed. This works well with the keyboard focus keys (like TAB), but it's completely useless with the mouse: a mouse click raises the GotFocus event (where I select all the text), but as soon as you release the button, a zero-selection is done, leaving the control with no selection. OK, I thought, let's add a SelectAll on the MouseUp event too, but this way, the user cannot perform a partial selection anymore; each time the mouse button is released, all the text is selected. I need to know if a partial selection exists; in a TextBox, I can test it with SelectionLength > 0, so I need to access the underlying TextBox control.

Now comes the tricky part: NumericUpDown is a composite control, a TextBox and a button box. Looking inside it through the Reflector, we can find the internal field which holds the textbox part:

VB.NET
Friend upDownEdit As UpDownEdit  ' UpDownEdit inherits from TextBox

We'll obtain a reference to this field using the underlying Controls() collection. Note that we should add some safety checks because future .NET Framework implementations could change things.

VB.NET
''' <summary>
''' object creator
''' </summary>
Public Sub New()
    MyBase.New()
    ' get a reference to the underlying UpDownButtons field
    ' Underlying private type is System.Windows.Forms.UpDownBase+UpDownButtons
    _upDownButtons = MyBase.Controls(0)
    If _upDownButtons Is Nothing _
           OrElse _upDownButtons.GetType().FullName <> _
           "System.Windows.Forms.UpDownBase+UpDownButtons" Then
        Throw New ArgumentNullException(Me.GetType.FullName & _
        ": Can't a reference to internal UpDown buttons field.")
    End If
    ' Get a reference to the underlying TextBox field.
    ' Underlying private type is System.Windows.Forms.UpDownBase+UpDownButtons
    _textbox = TryCast(MyBase.Controls(1), TextBox)
    If _textbox Is Nothing _
           OrElse _textbox.GetType().FullName <> _
           "System.Windows.Forms.UpDownBase+UpDownEdit" Then
        Throw New ArgumentNullException(Me.GetType.FullName & _
        ": Can't get a reference to internal TextBox field.")
    End If
End Sub 

Now that we have the underlying TextBox, it is possible to export some missing properties:

VB.NET
<Browsable(False)>
<DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
Public Property SelectionStart() As Integer
    Get
        Return _textbox.SelectionStart
    End Get
    Set(ByVal value As Integer)
        _textbox.SelectionStart = value
    End Set
End Property

And finally, we can have a perfectly working mouse management:

VB.NET
' MouseUp will kill the SelectAll made on GotFocus.
' Will restore it, but only if user have not made
' a partial text selection.
Protected Overrides Sub OnMouseUp(ByVal mevent As MouseEventArgs)
    If _autoSelect AndAlso _textbox.SelectionLength = 0 Then
        _textbox.SelectAll()
    End If
    MyBase.OnMouseUp(mevent)
End Sub

Mouse Events Not Raised Properly

The original MouseEnter and MouseLeave events are raised in couples: a MouseEnter immediately followed by a MouseLeave. Maybe that's why, to discourage their use, they are marked with a <Browsable(False)> attribute. Since I need the MouseEnter event to update my StatusBar caption, I investigated a little on this "bug".

As said above, NumericUpDown is a composite control (red rectangle in the following picture) containing a TextBox (left green rectangle) and some other controls:

Image 2

The "control" area is the one between the red and the green rectangles; when you fly over it with the mouse, you'll receive the MouseEnter event while between the red and the green, then MouseLeave when inside the green rectangle. The same happens when you leave.

The better way to raise these events, now that we can access the underlying TextBox, is to re-raise the MouseEnter and MouseLeave events as raised from the TextBox itself; this is what NumericUpDownEx does.

MouseWheel Management

NumericUpDown's management of the mouse wheel is, sometimes, really annoying. Suppose you have an application which displays some kind of chart, with a topmost dialog (toolbox) to let the user change some parameters of the graph. In this dialog, the only controls which can keep the focus are NumericUpDown ones:

Image 3

After your user puts the focus inside one of them, the mouse wheel is captured by the NumericUpDown. When the user wheels to, say, scroll the graph, the effect is that the focused field value is changed; this behavior is really annoying.

A fix could be to kill the WM_MOUSEWHEEL message for the control, but this will kill even "legal" wheelings.

The NumericUpDown has a property which allows WM_MOUSEWHEEL messages to pass only if the mouse pointer is over the control, making sure that the user is wheeling to change the control value.

This is done by keeping track of the mouse state in the MouseEnter-MouseLeave events, then killing WM_MOUSEWHEEL messages accordingly.

Image 4

How to Use the Control

Simply include NumericUpDownEx.vb in your project and use the control like you'll do with the standard NumericUpDown. If you have a C# project, you could reference the CoolSoft.NumericUpDownEx.dll assembly or, better, try to convert the code to C# (it should not be so difficult). I could provide a C# version upon request.

Updates

v1.6 (06/Jan/2016)

  • Added "Never" value to ShowUpDownButtonsMode enum to always hide UpDown spinner control

v1.5 (28/Mar/2014)

  • Removed reflection code, now underlying controls are retrieved with managed code only (thanks to JWhattam for this suggestion)

v1.4 (17/Dec/2012)

  • New option to show up/down buttons when the control has focus (regardless of mouseover), thanks to Fred Kreppert for his suggestion

v1.3 (15/Mar/2010)

  • Added new WrapValue property: when set, if Maximum is reached during an increment, Value will restart from Minimum (and vice versa)
    (feature suggested by YosiHakel here)
  • Cleaned up the C# version

v1.2 (10/Feb/2010)

  • Added two new events BeforeValueDecrement and BeforeValueIncrement, as suggested by andrea@gmi. This will allow to give different increment/decrement depending on the current control value
  • Added a C# version of the control to the ZIP

License

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


Written By
Technical Lead CoolSoft
Italy Italy
I started programming in the early 1984, when I was 12, using each and every version of VB, from QuickBasic (1985) to VB.NET 11.0, but also C#, C++, PHP, JavaScript, Fortran, Pascal, Modula2, together with a lot of frameworks like Symfony, jQuery, Drupal.

My most popular project is VirtualMIDISynth, a software MIDI synthesizer (using .sf2 soundfont files) implemented as a virtual device driver. It works on all modern Windows OS from XP to 8, both 32 and 64 bit.

Another project of mine is DeCodEx (DEsigner CODe EXtractor), a free tool to split VisualStudio 2003 forms and controls source code into the new 2005/2008 partial classes format (*.vb and *.Designer.vb, or *.cs and *.Designer.cs).

I like writing tools to make my seveloper and SysAdmin life easier.

You can find them here: http://coolsoft.altervista.org.

Comments and Discussions

 
SuggestionMore enhancements ... Pin
geth447-Nov-18 7:38
geth447-Nov-18 7:38 
BugMore enhancements (bug correction) ... Pin
geth449-Nov-18 0:15
geth449-Nov-18 0:15 
GeneralRe: More enhancements ... Pin
Fred Kreppert12-Dec-18 8:16
Fred Kreppert12-Dec-18 8:16 
Hi Thierry,

I really like your LargeIncrement enhancement. I preferred to use the CTRL key rather than the Shift key though. The Shift key can be used, even up and down, to move the cursor across the value (up moves left and down moves right). The CTRL key however does nothing within the edit field, so using it made more sense to me. To make this change, modify the following:

In ProcessCmdKey, DownButton and UpBotton routines, change Keys.Shift to Keys.Control

In WndProc change MK_SHIFT to MK_CTRL in all 3 places and set its constant value to 0x0008.

I believe that is all that I needed to make that change. One could probably add a new property to select at design time (or at runtime) which is to be used as well, and add the appropriate code to select either one. This way either could be used, depending upon the developer, the user, and the way someone wants to make it work. I have not added that code yet, but if I do I will post the modifications that are needed.

I also discovered that you don’t really allow for the WrapValue feature when using the LargeIncrement. The problem is that if you increment to the max value, you simply make the value the max value. If you decrement to the min value, you simply make the value the min value. This seems counterproductive to having the wrap around feature. If the value reaches the minimum or maximum it should wrap around with whatever amount is left after it reaches the minimum or maximum.

To correct this I changed the code in DoIncrement and DoDecrement to be as follows:

private void DoDecrement(bool Large, int Count = 1)
{
    if (IsNull)
    {
        IsNull = false;
        if (base.Value == Maximum) // already set to maximum ?
        {
        base.Value = Maximum - Increment;  // to force refresh value, otherwise ValueChanged not raised and text not displayed
        }
        base.Value = Maximum;
        UpdateEditText();
    }
    else
    {
        decimal CurIncrement = (Large ? (LargeIncrement == 0 ? Increment : LargeIncrement) : Increment) * Count;
        // The following 6 lines were modified
        if (WrapValue && base.Value - CurIncrement  Maximum)
            base.Value = base.Value + CurIncrement - Maximum + Minimum - 1;
        else if (!WrapValue && base.Value + CurIncrement > Maximum)
            base.Value = Maximum;
        else
            base.Value += CurIncrement;
    }
    UpdateEditText();
}

private void DoIncrement(bool Large, int Count = 1)
{
    if (IsNull)
    {
        IsNull = false;
        if (base.Value == Minimum)
        {
            base.Value = Minimum + Increment;
        }
        base.Value = Minimum;
        UpdateEditText();
    }
    else
    {
        decimal CurIncrement = (Large ? (LargeIncrement == 0 ? Increment : LargeIncrement) : Increment) * Count;
        // The following 6 lines were modified
        if (WrapValue && base.Value + CurIncrement > Maximum)
            base.Value = base.Value + CurIncrement - Maximum + Minimum - 1;
        else if (!WrapValue && base.Value + CurIncrement > Maximum)
            base.Value = Maximum;
        else
            base.Value += CurIncrement;
    }
    UpdateEditText();
}

I also discovered a bug. In the OnKeyUp routine, if the field is not Nullable and the value is at the minimum value, if someone highlights the field and presses the Delete key or if they use the Backspace key to entirely remove the value in the field (essentially making it a string with zero length or an empty field), you set the base.Value to Minimum. Since the value was already at the Minimum value it leaves the visible field on the screen with a length of zero and does not display the minimum value. This is because base.Value did not actually change so it doesn’t update the screen.

To fix this bug, you need to set base.Value to something other than minimum, then set it to the minimum value. You describe doing this same thing in a comment under DoDecrement. Here I used the following code:

C#
base.Value = Minimum + 1;
base.Value = Minimum;

Finally, I chose to move the entire code that you have for OnKeyUp to OnLostFocus. The purpose of this is because as I described above, some people like to delete the entire field and then manually type in a new value. Having the code in OnKeyUp causes the field to always display at least the minimum value, even when they intend to delete the entire field and change it. Moving the code to OnLostFocus, which is already defined in the code for the control, makes sure that when the focus is moved from the field, it always displays the minimum value, even if they delete the field and then move to another field without replacing the value. It also doesn’t annoy those people who want to delete the entire field and then retype it since this functionality is now allowed. If Nullable is enabled, deleting the field and moving off of it still leaves it as a null value. This still meets the functionality of the Nullable field.

As an example of what I am describing, set the Minimum to 100 and the Maximum to 499. If you delete the 100 value entirely and intend to replace it with 250, your code will notice that it is a blank field but not a nullable field and set the value back to 100. The 100 value is also not highlighted in blue when this happens, and typing a 2 value will cause it to now read 2100, which exceeds the Maximum value, which will now be replaced with 499. This is clearly not what the user intended and they get annoyed with the software because it doesn’t do what they want it to do.

Moving your code from OnKeyUp to OnLostFocus resolves the above problem and causes it to function as expected.

I hope others will find these modifications useful. Big Grin | :-D

Fred

modified 12-Dec-18 22:04pm.

Bugmouse event control not correct. Pin
Dong Fei4-Jan-18 16:26
Dong Fei4-Jan-18 16:26 
Questionbutton width Pin
Konstantin Samsonov22-Jan-17 0:51
Konstantin Samsonov22-Jan-17 0:51 
QuestionReally nice control Pin
us471114-Jan-16 7:35
us471114-Jan-16 7:35 
AnswerRe: Really nice control Pin
Claudio Nicora14-Jan-16 10:19
Claudio Nicora14-Jan-16 10:19 
QuestionPermanently Hide The Spinner? Pin
MJ_Karas6-Jan-16 4:39
MJ_Karas6-Jan-16 4:39 
AnswerRe: Permanently Hide The Spinner? Pin
Claudio Nicora6-Jan-16 10:19
Claudio Nicora6-Jan-16 10:19 
GeneralRe: Permanently Hide The Spinner? Pin
MJ_Karas6-Jan-16 17:43
MJ_Karas6-Jan-16 17:43 
GeneralRe: Permanently Hide The Spinner? Pin
Claudio Nicora7-Jan-16 3:02
Claudio Nicora7-Jan-16 3:02 
QuestionDecimal places hide when the up down arrows hide... Also, tab doesnt cause validation (standard nud issue) Pin
DDnDD25-Aug-15 7:13
DDnDD25-Aug-15 7:13 
AnswerRe: Decimal places hide when the up down arrows hide... Also, tab doesnt cause validation (standard nud issue) Pin
Claudio Nicora25-Aug-15 22:31
Claudio Nicora25-Aug-15 22:31 
QuestionNumericUpDOwn control in c# Pin
Member 939044415-May-15 0:32
Member 939044415-May-15 0:32 
AnswerRe: NumericUpDOwn control in c# Pin
Claudio Nicora15-May-15 1:31
Claudio Nicora15-May-15 1:31 
GeneralRe: NumericUpDOwn control in c# Pin
Member 939044415-May-15 2:44
Member 939044415-May-15 2:44 
SuggestionEasier way to access contained controls Pin
JWhattam25-Mar-14 17:45
JWhattam25-Mar-14 17:45 
GeneralRe: Easier way to access contained controls Pin
Claudio Nicora27-Mar-14 23:21
Claudio Nicora27-Mar-14 23:21 
GeneralRe: Easier way to access contained controls Pin
Claudio Nicora25-Aug-15 22:25
Claudio Nicora25-Aug-15 22:25 
Questionx86 to x64 produces designer errors? Pin
codetowns23-Jun-13 0:36
codetowns23-Jun-13 0:36 
AnswerRe: x86 to x64 produces designer errors? Pin
Claudio Nicora23-Jun-13 20:55
Claudio Nicora23-Jun-13 20:55 
GeneralRe: x86 to x64 produces designer errors? Pin
codetowns23-Jun-13 21:24
codetowns23-Jun-13 21:24 
GeneralMouse wheel usage Pin
cvogt6145720-Mar-13 3:35
cvogt6145720-Mar-13 3:35 
SuggestionAddtional feature - display updown buttons with focus Pin
Fred Kreppert15-Dec-12 8:14
Fred Kreppert15-Dec-12 8:14 
GeneralRe: Addtional feature - display updown buttons with focus Pin
Claudio Nicora17-Dec-12 1:33
Claudio Nicora17-Dec-12 1:33 

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.