Introduction
One of the significant items missing from the collection of Microsoft WPF controls is the numeric up/down control. I am sure that almost all other WPF developers were equally frustrated with Microsoft when they did not include this as an important control in the new controls in the 2010 release of Visual Studio (only four new controls in Visual Studio 2010). I have seen a number of versions of the numeric up/down (spinner) control on the Internet, but most of them have to be customized if the look and feel is to be different from the implementation.
Background
To provide the type of flexibility I would like to see in such a control, it could not be created as a UserControl
, or a basic ControlTemplate
. In any case, a significant amount of code would be required to implement the functionality of the buttons and the arrow keys. I have seen some implementations of TextBox
controls that inherited from Control
, but that meant that all the normal properties of the TextBox
would not be available. It turns out that inheriting from a TextBox
actually works very well. The XAML required to inherit from the TextBox
is as follows:
<TextBox x:Class="CustomControls.NumericUpDownTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls"/>
When overriding the TextBox
, in addition to the TextBox
control, there are two Button
controls (or controls that emulate the Button functionality) required to give the user the ability to increment or decrement the value in the TextBox
with the mouse button. When inheriting from a TextBox
, the TextBox
capability is provided for free. All that is required is to create two Button
controls in code. This is done in the constructor. In addition to creating the controls, we also need to attach event handlers to the base TextBox
that are responsible to handling the up and down key presses, and ensure that input is limited to numbers (and negative sign if first character, and negative values are allowed):
public NumericUpDownTextBox()
{
InitializeComponent();
var buttons = new ButtonsProperties(this);
ButtonsViewModel = buttons;
upButton = new Button()
{
Cursor = Cursors.Arrow,
DataContext = buttons,
Tag = true
};
upButton.Click += upButton_Click;
downButton = new Button()
{
Cursor = Cursors.Arrow,
DataContext = buttons,
Tag = false
};
downButton.Click += downButton_Click;
controls = new VisualCollection(this);
controls.Add(upButton);
controls.Add(downButton);
this.PreviewTextInput += control_PreviewTextInput;
this.PreviewKeyDown += control_PreviewKeyDown;
this.LostFocus += control_LostFocus;
}
In the above code, it can be seen that I use the binding for properties for the button properties. I attempted to set properties directly, but this had to be reapplied each time the button was displayed because the Button
would lose the properties. Using binding means that the Button
s will only get the properties when they are needed. For some reason, the Cursor
is able to maintain the property, as does the Click
event. Another event I had trouble with was when I attempted to use a Border
as a button.
The most critical part of the code is the override of the ArrangeOverride
method. The method is responsible for positioning the controls on the allocated space. In this method, I do not paint the Button
s if the Width
is below a certain ratio with the Height
(I chose 1.5). The base ArrangeOverride
is called with the rectangle allocated for the base TextBox
and then uses the Arrange
method for the Button
s, providing the method the rectangle for each button.
protected override Size ArrangeOverride(Size arrangeSize)
{
double height = arrangeSize.Height;
double width = arrangeSize.Width;
showButtons = width > 1.5 * height;
if (showButtons)
{
double buttonWidth = 3 * height / 4;
Size buttonSize = new Size(buttonWidth, height / 2);
Size textBoxSize = new Size(width - buttonWidth, height);
double buttonsLeft = width - buttonWidth;
Rect upButtonRect = new Rect(new
Point(buttonsLeft, 0), buttonSize);
Rect downButtonRect = new Rect(new
Point(buttonsLeft, height / 2), buttonSize);
base.ArrangeOverride(textBoxSize);
upButton.Arrange(upButtonRect);
downButton.Arrange(downButtonRect);
return arrangeSize;
}
else
{
return base.ArrangeOverride(arrangeSize);
}
}
GetVisualChild
just needs to pass the base GetVisualChild
if the index argument is less than the base GetVisualChild
, and pass the button otherwise.
protected override Visual GetVisualChild(int index)
{
if (index < base.VisualChildrenCount)
return base.GetVisualChild(index);
return controls[index - base.VisualChildrenCount];
}
VisualChildrenCount
just needs to determine if the buttons are displayed or not, and either pass the base value, or the base value plus two.
protected override int VisualChildrenCount
{
get
{
if (showButtons)
return controls.Count + base.VisualChildrenCount;
else
return base.VisualChildrenCount;
}
}
Using the Code
Using the control is just like using any other custom control: a reference to the namespace has to be included as an attribute of the root element of the XAML, and the name assigned to the reference is then used to define the control in an element in the XAML. Within this element, all the properties of a TextBox
can be assigned using attributes or elements defined within this element. There are also a number of custom properties that can be set. The ones that control the appearance of the buttons all start with "Button.". If these specialized properties are not used, then either the values used by the TextBox
or the defaults are used. There are also several other properties to set the minimum (MinValue
) and maximum (MaxValue
) values, and a Value
property which interfaces with the TextBox
value as an integer:
<Window x:Class="WPFControlTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls"
Title="MainWindow" Height="192" Width="281">
<Grid Background="SteelBlue">
<local:NumericUpDownTextBox x:Name="textBox2" Height="25" Margin="20"
FontStyle="Italic" FontSize="10" BorderBrush="Green"
BorderThickness="2" Background="LightGray" Foreground="DarkBlue"
MinValue="100" MaxValue="1001"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
ButtonBackground="BlueViolet" ButtonBorderBrush="LightGreen"
ButtonForeground="Azure" ButtonBorderThickness="1,3,1,3"
ButtonMouseOverBackground="Aquamarine"
ButtonPressedBackground="Red"/>
</Grid>
</Window>
Points of Interest
I went through a lot of different implementations to attempt to get the buttons to inherit the TextBox
properties. Basically, I wanted the Background
, Foreground
, and Border
to be inherited from TextBox
. In addition, I wanted the content of the Button
s to be a polygon to represent the up or down arrow. My first iteration just use used two basic buttons, and this was good enough to get the functionality I wanted for the TextBox
. Unfortunately, a Button
's border is not changed when the border properties are changed. That means I had to come up with something else.
My initial idea was to use a border created in code instead, and then attach to the Mouse events to simulate the MouseOver
and MouseDown
events of the button. Unfortunately, this did not work because the events were not fired.
I had wanted to minimize the amount of XAML needed for this control. Because the other options I had looked at did not provide the look and feel I wanted, that meant that I had to create a ControlTemplate
for the Button
s as a Resource for the TextBox
as part of the XAML.
Well, this did work, but not as I thought it would. I initially attempted to create the Border
and Polygon
(for the arrows) in code and assign it to the content of the Button
. For some reason, I could not programmatically set the content of the TextBox
(maybe I am missing something), so I backtracked. I initially put the Border
inside the Template, and then the content inside that, but that still gave me problems setting the content from code, so I ended up defining the Polygon
for the arrows inside a Border
, which was inside the ControlTemplate
. The Border
then could inherit the properties for border Thickness
and Background
from the Button
. This worked, except I needed to customize the Polygon
for the inside size of the Border
.
So to get the Arrow Polygon, I needed to get at the properties of the Border
to determine the location of the Point
s for the Polygon
. I initially attempted to create a Binding
using a class derived from the IValueConverter
interface and passing the Border
as the Converter
parameter. This did not work because the PropertyChanged
event is only triggered when the property is changed, and for the Border
, that only occurs during the loading of the control, and at that point, the size of the control is still zero. That left the only option being to using the IMultiValueConverter
, and including the Border
's Height
, Width
, and BorderThickness
properties in the parameters. There was only one other piece of information I needed to create the arrow for each button: the direction of the arrow (up or down). Therefore, one more property was required, a Boolean. I decided to use the Button
's DataContext
to pass this information to the Polygon
. I also considered using the Tag
property, but figured that the DataContext
was a slightly easier to understand method. The ControlTemplate
ended up looking as follows:
<ControlTemplate TargetType="{x:Type Button}">
<Border Name="buttonBorder"
BorderBrush="{Binding BorderBrush}"
BorderThickness="{Binding BorderThickness}"
Background="{Binding Background}"
CornerRadius="3">
<Polygon Fill="{Binding Foreground}" >
<Polygon.Points>
<MultiBinding Converter="{StaticResource ArrowCreater}" >
</MultiBinding>
</Polygon.Points>
</Polygon>
</Border>
<ControlTemplate.Triggers>
-->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="buttonBorder" Property="Background"
Value="{Binding IsMouseOverBackground}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="buttonBorder" Property="Background"
</Trigger>
-->
</ControlTemplate.Triggers>
</ControlTemplate>
One important note on the control template, which is wrapped in a Style
with a TargetType
of Button
, is that TargetType
has to be reapplied to the ControlTemplate
, or the compiler will complain about the IsPressed
.
The IMultiValueConverter
class had only one little detail that I dealt with, and that was to check if the Border
's Height
or Width
was 0, which would be the case until the Border
was actually being laid out. In these cases, I just returned without processing the code required to create the arrows.
internal class ArrowCreater : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
double width = (double)values[0];
double height = (double)values[1];
if ((height == 0.0) || (width == 0.0)) return null;
Thickness borderThickness = (Thickness)values[2];
bool up = (bool)values[3];
double arrowHeight = height - borderThickness.Top -
borderThickness.Bottom;
double arrowWidth = width - borderThickness.Left -
borderThickness.Right;
return CreateArrow(arrowWidth, arrowHeight, up);
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
private PointCollection CreateArrow(double width,
{
double margin = height * .2;
double pointY;
double baseY;
if (isUp)
{
pointY = margin;
baseY = height - margin;
}
else
{
baseY = margin;
pointY = height - margin;
}
var pts = new PointCollection();
pts.Add(new Point(margin, baseY));
pts.Add(new Point(width / 2, pointY));
pts.Add(new Point(width - margin, baseY));
return pts;
}
}
I also included optional properties for buttons. If these properties are not set, the properties for the TextBox
are used when available (such as Background
, Foreground
, BorderBrush
, BorderThickness
), or defaults are used (CornerRadius
, Background
when button is pressed or on mouse over). I attempted to have a separate class to contain the button properties so that they could be set with the term "Buttons" followed by a period and the property name, but WPF apparently does not support having a property that contains properties, so I ended up just prefixing the property name with "Button.".
The buttons have their own DataContext
, and I used a special class for this DataContext
, which inherits the InotifyPropertyChanged
interface. This has a pointer to the base NumericUpDownTextBox
class, and the properties in this class only have a property Get. Each Get will either return the Button
specific property, or the property for the TextBox
, or the default if the Button
specific property has not been set. The Button
specific properties for NumericUpDownTextBox
handle the change event, and call the Button
's DataContext
class' public NotifyPropertyChanged
method to trigger the PropertyChangedEventHandler
for the Button
's DataContext
. This works very cleanly.
User Input
The only other significant detail was code to handle the user input. There are three ways for the user to change the value: the keyboard's number keys (and negative sign), the keyboard's up and down arrow keys, and the two Button
controls.
In this code, the user is prevented from using the keyboard to enter any text in the TextBox
that is not numeric, except the negative sign ('-'), and the negative sign can only be entered if the minimum allowed value is less than zero, the input caret is at the beginning of the TextBox
text, and a negative sign does not already exist. Also, if the user has entered keystrokes that will obviously create a value that will exceed the Maximum
(if the Maximum
is greater than zero) or the Minimum
(if the Minimum
is less than zero), then the value will be fixed. Part of the checking for validity of the TextBox
text has to account for the caret position and the selection length. For this, the StringBuilder
control makes it very easy to determine the value of the TextBox
text after user input. The PreviewTextInput
event is used to control the user's changes in the TextBox
:
private void control_PreviewTextInput(object sender,
TextCompositionEventArgs e)
{
if ("0123456789".IndexOf(e.Text) < 0)
{
if (e.Text == "-" && MinValue < 0)
{
if (this.Text.Length == 0 || (this.CaretIndex == 0 &&
this.Text[0] != '-'))
{
e.Handled = false;
return;
}
}
e.Handled = true;
}
else {
if (this.Text.Length > 0 && this.CaretIndex == 0 &&
this.Text[0] == '-' && this.SelectionLength == 0)
{
e.Handled = true;
}
else
{
StringBuilder sb = new StringBuilder(this.Text);
sb.Remove(this.CaretIndex, this.SelectionLength);
sb.Insert(this.CaretIndex, e.Text);
int newValue = int.Parse(sb.ToString());
if (FixValueKeyPress(newValue))
{
e.Handled = false;
}
else
{
e.Handled = true;
}
}
}
}
The method FixValueKeyPress
checks the resulting value of the user input, and will force correction, leaving the caret at the end of the text. If there are no issues with the user input, the TextBox
will be left unchanged.
The pressing of the keyboard up and down arrow keys are captured with the PreviewKeyDown
event:
private void control_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Down)
{
HandleModifiers(-1);
e.Handled = true;
}
else if (e.Key == Key.Up)
{
HandleModifiers(1);
e.Handled = true;
else if (e.Key == Key.Space)
e.Handled = true;
}
else
e.Handled = false;
}
private void HandleModifiers(int value)
{
if (Keyboard.Modifiers == ModifierKeys.Shift) value *= 10;
Add(value);
}
The event handler checks if the Key
in the event arguments is an up or down key, and then uses the HandleModifiers
method (this may be used with the up/down button also). This method is provided an integer value which will indicate if the up or down arrow was pressed. This value (absolute value of 1) is then multiplied by the value of a constant if modifier keys are pressed, and then adds the value passed to the content of the TextBox
. Note to also look for the space key in the preview since PreviewTextInput
does not catch the space key. We should check to see if any text is selected when the space key is pressed, and remove selected text, but it did not seem worth the complexity.
History
- 11/22/2010: Fixed update issues with
MaxValue
, MinValue
, and Value
. MaxValue
and MinValue
are now applied dynamically. Value
now is correct after making a change in the TextBox
and tabbing to the next control. Also added TextBox
es to the test form for Value
, MinValue
, and MaxValue
.
- 12/7/2010: Source code updated
The following changes were made to the code:
- Changed the type for the "
Value
" DependencyProperty
to int?
instead of int
, and set its initial value to null
. This was required to be able to initialize the value of the control to "0
". Initially the default value was 0
, which meant that there was no change in the value of "Value
" so the Text would not be updated.
- When the control loses focus, it is now initializes to
0
, or the MinValue
or MaxValue
if 0
is not between these values.
- Added repeat button functionality working after several attempts. Discovered that can capture the preview events, but not the other
RoutedEvent
s. The System.Windows.Timer
is used to generate the delay and interval, and the timer is initialized and disposed of on the PreviewMouseUp
and PreviewMouseDown
events. For MouseDown
and MouseUp
, it does not matter since the Button
s are the consumers anyway, but could not capture the MouseEnter
and MouseLeave
events, and there is no preview for these events. Therefore, I could not use an event to determine if the mouse was over the button, but had to check the mouse position each timer event for the repeat functionality. If you check the scroll bars, you will see that the scroll bars only scroll after the MouseDown
event on the ScrollBar
when the mouse is over ScrollBar
, and stop when it is not.
- Also made a few other changes in organization. Amazingly managed to keep the lines of code to about what they were before.
- 5/17/2011: Source code updated
- This update adds support for the mouse wheel and is thanks to AndreyA