从头开始创建一个数字增减控件
展示了在 WPF 中创建功能齐全的 NumericUpDown 控件的整个过程。
引言
在 CodeProject 潜伏了仿佛永恒的时间之后,我决定是时候开始为这个伟大的网站做出贡献了。
这是我写的第一篇文章,但我可以诚实地说,我已经尽了最大的努力写好它。如果我有什么做得不好或解释不清楚的地方,请告诉我,以便我改进这篇文章,并可能提高我的技能。
关于本文性质的说明
我真的不知道这算是一个教程还是一个分步指南。我想两者兼而有之吧。我猜我把它写成了一个分步指南,面向中级程序员,并穿插了面向初学者的教程解释。代码都在这里,不过,可下载的源代码中没有任何未解释的惊喜。
我希望您能完整地阅读这篇文章,它从来都不是那种“来选择你喜欢的功能”的指南,这些功能往往相互依赖。此外,它们的实现顺序并非按重要性或有用性排列——一些明显缺失的功能,例如 TextBox 中的默认文本,在最后才实现,因为它们不是很有趣或很重要,只是完成控件所必需的(它们可以等待)。
您至少需要 .NET Framework 3.5,但建议使用 .NET Framework 4.0 或更高版本(即 4.5)。
您将学到什么
您将学习如何创建名为 NumericUpDown 的自定义控件,并附带 Generic.xaml 中的默认外观和使用该控件所有方面的 Demo 应用程序(如下所示)。
图 1 - 演示应用程序的屏幕截图,显示了我们将要创建的 NumericUpDown。所有按钮都与控件的各个方面相关。
除了上面明显的功能之外,这个 NumericUpDown 还支持
- 通过按 Esc 取消键入的未确认更改
- 通过按 Enter 确认键入的更改
- 使用键盘箭头或 PageUp/PageDown 增加和减少值
- 当侧面的两个按钮被右键单击时,将值归零
创建基本控件
我们将需要一个名为 CustomControls 的解决方案,其中包含两个项目
- CustomControl(一个 WPF 自定义控件库)
- Demo(一个 WPF 应用程序)
我假设您下载了源代码并使用了附带的 Demo 源代码。将其作为 Demo 应用程序的基础,并在实现 individual 方法后,简单地取消 MainWindow.xaml.cs 中的注释。
打开 CustomControl1.cs 并将其重命名为 NumericUpDown.cs。您的代码应如下所示
namespace CustomControls
{
public class NumericUpDown : Control
{
static NumericUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof (NumericUpDown),
new FrameworkPropertyMetadata(
typeof (NumericUpDown)));
}
}
}
OverrideMetadata
是负责在 Generic.xaml 中应用默认主题的函数,所以我们确实想保留它。
说到 Generic.xaml,现在我们来修改它。这个模板实际上只是一个 TextBox 和两个 RepeatButtons,带有一些自定义图形。最重要的部分是名称。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls">
<Style TargetType="{x:Type local:NumericUpDown}">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="BorderBrush" Value="Gray" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="26" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:NumericUpDown}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Focusable="False">
<Grid Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
VerticalAlignment="Center"
Focusable="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox x:Name="PART_TextBox"
VerticalAlignment="Center"
HorizontalContentAlignment="Right" />
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<RepeatButton x:Name="PART_IncreaseButton"
Grid.Row="0"
Width="20"
Margin="0, 1, 2, 0">
<Path Margin="1"
Data="M 0 20 L 35 -20 L 70 20 Z"
Fill="#FF202020"
Stretch="Uniform" />
</RepeatButton>
<RepeatButton x:Name="PART_DecreaseButton"
Grid.Row="1"
Width="20"
Margin="0, 0, 2, 1">
<Path Margin="1"
Data="M 0 0 L 35 40 L 70 0 Z"
Fill="#FF202020"
Stretch="Uniform" />
</RepeatButton>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
正如您所看到的,TextBox 和两个 RepeatButtons 的名称都以“PART_”开头。我稍后会解释这些奇怪名称的用途。目前,重要的是上面的模板绘制的 NumericUpDown 与第一个屏幕截图中显示的几乎相同。我认为除了 RepeatButtons 中的 Path.Data
变量之外,其他任何东西都不应该令人惊讶。如果您以前从未使用 Path
绘制过自定义图形,这里是必要的信息。
以下是 NumericUpDown 组成的层:
图 2 - 显示了构成 NumericUpDown 控件的不同层。
将它们联系起来
我们有一个骨架 NumericUpDown 类,它没有任何有趣的功能,并且在 Generic.xaml 中有一个模板,由前面提到的 OverrideMetadata
方法应用。一切似乎都很顺利,对吗?嗯,唯一的问题是,我们无法访问 TextBox 和两个 RepeatButtons!它们没有像人们预期的那样作为变量添加到代码中,那么我们应该如何获取它们的引用呢?嗯,这里就需要 TemplatePartAttribute 了。正如链接所说,TemplatePartAttributes 基本上是宣传我们期望在模板中拥有的元素。我们需要三个部分,所以我们也需要三个属性
[TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))]
[TemplatePart(Name = "PART_IncreaseButton", Type = typeof(RepeatButton))]
[TemplatePart(Name = "PART_DecreaseButton", Type = typeof(RepeatButton))]
您使用的名称不重要(尽管它们确实必须与 XAML 中的名称匹配,显然),但最好以 PART_ 开头模板部件名称以表明它们的用途。 现在,仅仅宣传这个控件有三个模板部件是不够的,我们需要获取一个可以使用的引用——这有点复杂。
我们不能仅仅在构造函数中“附加”(获取对)这些模板部分,因为如果有人应用了不同的模板,这些引用将失效。相反,我们需要在模板应用的那一刻进行操作。OnApplyTemplate()
非常适合此目的。重写该方法并如下修改它
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
AttachToVisualTree();
}
AttachToVisualTree()
只是一个方便的方法,用于调用执行实际附加的三个实际方法。它们是
private void AttachToVisualTree()
{
AttachTextBox();
AttachIncreaseButton();
AttachDecreaseButton();
}
不言自明,不是吗?让我们逐一检查这些函数,并将这三个模板部分与代码隐藏关联起来。
附加模板部件
获取 TextBox 的引用就像调用 GetTemplateChild()
并传入模板部件的名称(在本例中为“PART_TextBox”)一样简单。现在就来做。
protected TextBox TextBox;
private void AttachTextBox()
{
var textBox = GetTemplateChild("PART_TextBox") as TextBox;
// A null check is advised
if (textBox != null)
{
TextBox = textBox;
}
}
此时,附加 RepeatButtons 是轻而易举的事。
protected RepeatButton IncreaseButton;
private void AttachIncreaseButton()
{
var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
if (increaseButton != null)
{
IncreaseButton = increaseButton;
IncreaseButton.Focusable = false;
}
}
protected RepeatButton DecreaseButton;
private void AttachDecreaseButton()
{
var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
if (decreaseButton != null)
{
DecreaseButton = decreaseButton;
DecreaseButton.Focusable = false;
}
}
别忘了存储所有三个引用,我们稍后会用到它们。另请注意,我们如何将 Focusable
属性设置为 false,它们不能接收任何焦点。
创建基本功能
为了显示和修改值,我们需要
- 创建一个依赖属性
- Value - 保存实际的 Decimal 数字,执行所有强制转换和通知
- 创建命令
- _minorIncreaseValueCommand - 表示增加值的命令
- _minorDecreaseValueCommand - 表示减少值的命令
- 将命令绑定到使用它们的模板部件
要在一个屏幕上显示一个数字,需要做很多事情,那我们开始吧。
创建依赖属性
NumericUpDown 控件就是关于改变 Value
,对吗?可惜,不是。尽管强制转换(纠正)逻辑将在 Value
内部,但我们必须使 TextBox 与 Value
内部数字的当前表示保持同步。让我们从创建 Value
开始
#region Value
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof (Decimal), typeof (NumericUpDown),
new PropertyMetadata(0m, OnValueChanged, CoerceValue));
public Decimal Value
{
get { return (Decimal) GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static void OnValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
private static object CoerceValue(DependencyObject element, object baseValue)
{
var value = (Decimal) baseValue;
return value;
}
#endregion
这看起来相当复杂,但实际上并不那么糟糕——大部分只是样板代码。ValueProperty
基本上是 Value
属性实例的所有元数据的字典(我们可以通过 get/set 访问器访问),正如 Robert Rossney 编写的精彩描述中解释的那样——请务必阅读。正如我确信您已经注意到的那样,上面的属性指定了三个参数作为 PropertyMetadata。
第一个参数是 DependencyProperty 创建时的默认值,在本例中为 0m(0 decimal)。第二个是 PropertyChangedCallback
委托,仅当属性真正更改时才会被调用(当设置相同的值时不会触发)。最后一个是 CoerceValueCallback
委托,当 Value
设置时会被调用。它允许您将输入强制(纠正)为可用值,然后将其分配给属性。
创建命令
此时您可能会问,为什么不直接对所有事情都使用事件处理程序呢?嗯,事件处理程序实际上会更复杂和冗长。稍后您就会明白。我们需要两个命令——一个用于增加 Value
,一个用于减少 Value
。
using System.Windows.Input;
private readonly RoutedUICommand _minorIncreaseValueCommand =
new RoutedUICommand("MinorIncreaseValue", "MinorIncreaseValue", typeof(NumericUpDown));
private readonly RoutedUICommand _minorDecreaseValueCommand =
new RoutedUICommand("MinorDecreaseValue", "MinorDecreaseValue", typeof(NumericUpDown));
请参阅此处快速参考构造函数的参数。我们还需要指定什么“触发”这些命令。两个 RepeatButtons 中的每一个都有一个 Command
属性——非常适合我们的需求。让我们立即分配命令。
private void AttachIncreaseButton()
{
var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
if (increaseButton != null)
{
IncreaseButton = increaseButton;
IncreaseButton.Focusable = false;
IncreaseButton.Command = _minorIncreaseValueCommand;
}
}
private void AttachDecreaseButton()
{
var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
if (decreaseButton != null)
{
DecreaseButton = decreaseButton;
DecreaseButton.Focusable = false;
DecreaseButton.Command = _minorDecreaseValueCommand;
}
}
命令已创建,并在需要时触发,但它们仍然什么也不做。我们需要告诉命令它们的回调是什么——它们在触发时调用的函数是什么。为此,我们创建一个名为 AttachCommands()
的新函数。在这个函数中,我们将最终附加随着我们添加更多功能而需要的所有各种命令。
private void AttachCommands()
{
CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue()));
CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue()));
}
那么我们刚才做了什么?首先,我们创建了一个 CommandBinding
,它将每个命令绑定到其各自的函数,即 IncreaseValue()
和 DecreaseValue()
,然后将这些命令绑定添加到我们的 NumericUpDown 控件的 CommandBindings
集合中。最后剩下的事情是创建我们调用的两个函数——IncreaseValue()
和 DecreaseValue()
。
private void IncreaseValue()
{
Value++;
}
private void DecreaseValue()
{
Value--;
}
请理解这些函数远未完成。我们将随着程序的进展添加更多内容,但为了获得最基本的功能(增加和减少 Value
),我们至少需要这么多。
现在,我们仍然需要做一件事才能在 TextBox 中显示内容。我们可能已经将命令绑定到增加和减少 Value
,但没有任何东西更新 TextBox,所以它仍然是空的。获取 Value
并创建我们可以使用的字符串表示的最佳位置是我们在之前创建的 CoerceValue()
方法。如下更改 CoerceValue()
using System.Globalization;
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.TextBox.Text = value.ToString(CultureInfo.CurrentCulture);
return value;
}
由于这些方法是静态的,我们必须获取对当前 NumericUpDown 的引用,并处理该引用的变量。
别忘了从 OnApplyTemplate()
调用 AttachCommands()
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
AttachToVisualTree();
AttachCommands();
}
如果您现在编译并启动,您仍然会看到一个空的 NumericUpDown。但是,如果您按下侧面的两个按钮,您将看到一个数字。
添加输入绑定
通过小 RepeatButtons 操作 Value
很好,但这还不够。如果我们可以使用键盘箭头,那就太棒了。谢天谢地,由于命令的存在,这是最容易的事情。我们所要做的就是以某种方式在按下相应的键盘箭头时触发 _minorIncreaseValueCommand
和 _minorDecreaseValueCommand
- 这就是 InputBindings 发挥作用的地方。我们只需将 InputBinding 注册到我们的命令,每次按下键盘箭头时都会触发该命令。如下修改 AttachCommands()
private void AttachCommands()
{
CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue()));
CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue()));
CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
CommandManager.RegisterClassInputBinding(typeof(TextBox),
new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
}
就是这样!如果您现在编译并启动,您只需按键盘箭头即可增加/减少值(不要忘记 TextBox 必须获得焦点)。
更新
正如用户 Ichters 所指出的那样,通过 CommandManager
注册输入绑定在单个窗口上使用多个 NumericUpDowns 时将无法正常工作,导致命令仅对最后一个 NumericUpDown 生效。
我非常抱歉,但我真的没有时间通读这篇文章,更改每一个出现的地方,并检查它是否编译/工作,然后对可下载的源代码进行相同的操作。我所能做的就是建议您按如下方式注册命令
TextBox.InputBindings.Add(new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up))); TextBox.InputBindings.Add(new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
我猜所有其他命令都应该以相同的方式注册,我无法做出任何保证。
祝您好运!
键盘输入
任何 NumericUpDown 控件都不会缺少直接将值输入到 TextBox 的功能。遗憾的是,这不仅仅是每次 TextBox.Text
更改时将其解析为 Decimal 并将其分配给 Value
的简单问题,我们只有在用户真正完成编辑 TextBox 时才需要更新 Value
。
有三种方法可以检测用户是否已完成编辑 TextBox
- NumericUpDown 失去焦点
- 用户使用键盘箭头或侧面的按钮来增加/减少
Value
- 用户按下回车键
在处理各个情况之前,我们需要创建一个方法,将文本输入转换为我们可以使用的 Decimal 数字。让我们创建一个方法来完成这项工作
private Decimal ParseStringToDecimal(String source)
{
Decimal value;
Decimal.TryParse(source, out value);
return value;
}
此函数只是将 source
解析为数字。TryParse()
方法很棒,因为无论我们尝试解析什么,它总是返回一个数字(它从不抛出异常)。
让我们处理第一个也是最简单的情况 - NumericUpDown 失去焦点。为此,我们需要拦截 LostFocus
事件,如下所示
private void AttachTextBox()
{
var textBox = GetTemplateChild("PART_TextBox") as TextBox;
// A null check is advised
if (textBox != null)
{
TextBox = textBox;
TextBox.LostFocus += TextBoxOnLostFocus;
}
}
创建 TextBoxOnLostFocus()
方法,并用 TextBox 的内容更新 Value
private void TextBoxOnLostFocus(object sender, RoutedEventArgs routedEventArgs)
{
Value = ParseStringToDecimal(TextBox.Text);
}
如果 TextBox 失去焦点,Value
将自动用 TextBox 的内容更新,然后 TextBox.Text
和 ValueString
将设置为 Value
。过程如下所示
现在我们已经处理了第一种情况,让我们继续第二种。在我们增加或减少一个数字之前,我们必须确保我们是在最新的数字上进行操作,它始终在 TextBox 中(即使 Value
通过程序设置,OnValueChanged()
也会立即更新 TextBox)。最简单的做法是简单地获取 TextBox 当前内容的 Decimal 表示并对其进行操作,如下所示
private void IncreaseValue()
{
// Get the value that's currently in the TextBox.Text
var value = ParseStringToDecimal(TextBox.Text);
value++;
Value = value;
}
private void DecreaseValue()
{
// Get the value that's currently in the _textBox.Text
var value = ParseStringToDecimal(TextBox.Text);
value--;
Value = value;
}
我们不知道用户是否更改了 TextBox 中的数字,或者它只是连续的增加/减少,使用了未更改的先前 Value
,所以我们总是获取最新的 Decimal 表示并对其进行操作。
为了处理第三种情况,我们需要处理回车键。我们需要一个新的命令来执行此操作
private readonly RoutedUICommand _updateValueStringCommand =
new RoutedUICommand("UpdateValueString", "UpdateValueString", typeof (NumericUpDown));
该命令必须绑定到其 InputBinding
private void AttachCommands()
{
CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue()));
CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue()));
CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
CommandManager.RegisterClassInputBinding(typeof(TextBox),
new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));
}
这里没有什么好奇怪的,这和我们之前做过两次的事情一样。唯一剩下的就是告诉命令做什么。那么,它到底应该做什么呢?看下面的代码
private void AttachCommands()
{
CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue()));
CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue()));
CommandBindings.Add(new CommandBinding(_updateValueStringCommand, (a, b) =>
{
Value = ParseStringToDecimal(TextBox.Text);
}));
CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));
}
当您按下回车键时,Value
会被赋予 TextBox.Text
中内容的最新 Decimal 表示。如果您键入一些随机乱码或格式错误的数字,如 "--4",TryParse()
方法在 ParseStringToDecimal()
内部将只返回 0,因此任何无效数字都无法进入 Value
。
移除焦点
每次点击其中一个 RepeatButton 时,我们需要从 TextBox 中移除焦点,而不使按钮可获得焦点(以消除难看的键盘焦点光晕)。我们需要拦截 RepeatButtons 上的鼠标左键单击,如下所示:
private void AttachIncreaseButton()
{
var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
if (increaseButton != null)
{
IncreaseButton = increaseButton;
IncreaseButton.Focusable = false;
IncreaseButton.Command = _minorIncreaseValueCommand;
IncreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus();
}
}
private void AttachDecreaseButton()
{
var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
if (decreaseButton != null)
{
DecreaseButton = decreaseButton;
DecreaseButton.Focusable = false;
DecreaseButton.Command = _minorDecreaseValueCommand;
IncreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus();
}
}
RemoveFocus()
方法非常简单。如下创建并填写
private void RemoveFocus()
{
// Passes focus here and then just deletes it
Focusable = true;
Focus();
Focusable = false;
}
这就是全部了。焦点被传递到 NumericUpDown,然后通过关闭焦点能力将其“销毁”。
添加 MaxValue 和 MinValue 边界
现在是实现 Value
所能包含的数字限制的绝佳时机。顾名思义,MaxValue
表示 Value
可以拥有的最大数字,而 MinValue
则恰好相反。
我们将需要两个 DependencyProperties。让我们先从 MaxValue
开始
public static readonly DependencyProperty MaxValueProperty =
DependencyProperty.Register("MaxValue", typeof (Decimal), typeof (NumericUpDown),
new PropertyMetadata(1000000000m, OnMaxValueChanged,
CoerceMaxValue));
public Decimal MaxValue
{
get { return (Decimal) GetValue(MaxValueProperty); }
set { SetValue(MaxValueProperty, value); }
}
private static void OnMaxValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
private static object CoerceMaxValue(DependencyObject element, Object baseValue)
{
var maxValue = (Decimal) baseValue;
return maxValue;
}
这里没有什么令人惊讶的,所以我们马上添加另一个
public static readonly DependencyProperty MinValueProperty =
DependencyProperty.Register("MinValue", typeof (Decimal), typeof (NumericUpDown),
new PropertyMetadata(0m, OnMinValueChanged,
CoerceMinValue));
public Decimal MinValue
{
get { return (Decimal) GetValue(MinValueProperty); }
set { SetValue(MinValueProperty, value); }
}
private static void OnMinValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
private static object CoerceMinValue(DependencyObject element, Object baseValue)
{
var minValue = (Decimal) baseValue;
return minValue;
}
现在我们有了这两个基本的 DependencyProperties,我们可以开始构建一个健壮的边界系统了。正如我确信您知道的,当边界的两个对立端相遇时,一个应该“推动”另一个——如下图所示
图 4 - 显示了一个基本边界系统。请记住,此图中的所有数字都是随机选择的,以展示问题。
那么我们到底要做什么才能实现这个功能呢?实际上非常简单。我们只需要确保如果一个边界越过另一个边界,我们也移动它。为了实现这一点,您只需如下编辑 OnMaxValueChanged()
和 OnMinValueChanged()
private static void OnMaxValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var maxValue = (Decimal) e.NewValue;
// If maxValue steps over MinValue, shift it
if (maxValue < control.MinValue)
{
control.MinValue = maxValue;
}
}
private static void OnMinValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var minValue = (Decimal) e.NewValue;
// If minValue steps over MaxValue, shift it
if (minValue > control.MaxValue)
{
control.MaxValue = minValue;
}
}
我们有边界,让我们在每次 Value
更改时应用它们。让我们创建一个方便的函数来为我们完成这项工作
private void CoerceValueToBounds(ref Decimal value)
{
if (value < MinValue)
{
value = MinValue;
}
else if (value > MaxValue)
{
value = MaxValue;
}
}
此方法返回一个保持在 <MinValue
; MaxValue
> 区间内的值。
让我们在强制 Value
时使用此方法
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.CoerceValueToBounds(ref value);
control.TextBox.Text = value.ToString(CultureInfo.CurrentCulture);
return value;
}
在它按计划工作之前还有一些工作要做。如下修改 IncreaseValue()
和 DecreaseValue()
private void IncreaseValue()
{
// Get the value that's currently in the TextBox.Text
var value = ParseStringToDecimal(TextBox.Text);
// Coerce the value to min/max
CoerceValueToBounds(ref value);
// Only change the value if it has any meaning
if (value <= MaxValue)
{
value++;
}
Value = value;
}
private void DecreaseValue()
{
// Get the value that's currently in the TextBox.Text
var value = ParseStringToDecimal(TextBox.Text);
// Coerce the value to min/max
CoerceValueToBounds(ref value);
// Only change the value if it has any meaning
if (value >= MinValue)
{
value--;
}
Value = value;
}
这里发生了很多事情。首先,我们获取 TextBox 的内容并从中提取一个数字。然后将该数字强制转换为 MaxValue
和 MinValue
边界。到目前为止,没有什么应该令人惊讶。
if 语句确保我们不会在不合理的情况下尝试更改 value
。如果 MinValue
是 0 且 Value
也是 0,则尝试减少 Value
没有意义。最后,我们简单地分配 Value
。
最后,我们需要确保如果 MaxValue
和 MinValue
发生变化,Value
会立即更新(如果它突然超出边界)。但是有一个问题,想象一下 MinValue
为 4,MaxValue
为 7,Value
已设置为 2,并考虑下图
图 5 - 显示了“期望值”如何影响依赖属性。
虽然此功能可能在其他地方有用,但对我们的用途来说是高度不希望的。我们可以很容易地摆脱它,如下面两个修改后的方法所示
private static void OnMaxValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var maxValue = (Decimal) e.NewValue;
// If maxValue steps over MinValue, shift it
if (maxValue < control.MinValue)
{
control.MinValue = maxValue;
}
if (maxValue <= control.Value)
{
control.Value = maxValue;
}
}
private static void OnMinValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var minValue = (Decimal) e.NewValue;
// If minValue steps over MaxValue, shift it
if (minValue > control.MaxValue)
{
control.MaxValue = minValue;
}
if (minValue >= control.Value)
{
control.Value = minValue;
}
}
现在,每次其中一个值改变时,Value
都会在必要时自动纠正以保持在边界内。如果边界(MinValue
和 MaxValue
)的改变导致 Value
超出范围,Value
将被明确设置为它本来会被强制转换到的数字。当 Value
被明确设置时,其期望值也会设置为相同的数字,所以即使您稍后放宽边界,Value
也不会移动(因为它已经处于期望值)。这是一个非常适合我们需求的巧妙技巧。
现在这个过程是这样的。向 TextBox 的反向赋值不再无用,而是必不可少的。
图 6 - 显示了 TextBox 失去焦点后发生的情况。强制转换 Value 并将其重新分配给 TextBox 的过程在此时非常重要。
添加小数位数
最后我们来到了指定 TextBox 显示多少小数位数的能力。
一如既往,我们必须从 DependencyProperty 开始
public static readonly DependencyProperty DecimalPlacesProperty =
DependencyProperty.Register("DecimalPlaces", typeof (Int32), typeof (NumericUpDown),
new PropertyMetadata(0, OnDecimalPlacesChanged,
CoerceDecimalPlaces));
public Int32 DecimalPlaces
{
get { return (Int32) GetValue(DecimalPlacesProperty); }
set { SetValue(DecimalPlacesProperty, value); }
}
private static void OnDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
private static object CoerceDecimalPlaces(DependencyObject element, Object baseValue)
{
var decimalPlaces = (Int32) baseValue;
return decimalPlaces;
}
现在我们有了 DependencyProperty,让我们思考一下。我们究竟要如何改变小数位数呢?好吧,这一切都归结为 CoerceValue()
方法中的一行代码
control.TextBox.Text = value.ToString(CultureInfo.CurrentCulture);
上面的 ToString()
所做的是根据 CurrentCulture
的规则将 value
中的 Decimal 数字格式化为 String。试想一下,您可以用多少种不同的方式以正确的格式输入一个值。
- 1,000 (一千)
- 1,000,000.000 (一百万)
- 1.5 (一又二分之一)
上面这些数字有什么共同点?文化!它们都根据 en-US 文化格式进行格式化。接下来看看这些值
- 1 000 (一千)
- 1 000 000,000 (一百万)
- 1,5 (一又二分之一)
糟糕,事情变得复杂了。看来像丹麦、捷克共和国和俄罗斯这样的国家使用这种奇怪的方式来分隔数字,甚至使用逗号而不是句点作为小数点。
“你为什么要提起这个?”你可能会问。好吧,因为如果你只为美国人编程 NumericUpDown,而来自(比方说)捷克共和国的人试图使用它们,他们就会很难理解为什么他们的小数逗号会消失(因为它被视为千位分隔符)。
我相信还有更多文化具有更奇怪的文化格式,那么我们应该如何处理所有这些呢?好吧,如上所述,我们可以访问 CultureInfo.CurrentCulture
来获取大部分相关信息。正如 CurrentCulture
这个名字所暗示的,它提供了当前执行我们应用程序的计算机的文化信息。
CurrentCulture
不仅仅是这样。当我们在格式化值时,我们还可以用它让我们的生活变得更轻松。怎么做?好吧,CurrentCulture
实际上指定了当数字解析为 String 时,有多少小数位是可见的!我们所要做的就是获取 CurrentCulture
的副本,修改其小数位数,然后使用它而不是 CultureInfo.CurrentCulture
。
让我们首先为我们自己的文化信息创建一个字段
protected readonly CultureInfo Culture;
现在我们需要创建 CultureInfo.CurrentCulture
的副本。最好的位置是实例构造函数。如下创建并修改它
public NumericUpDown() { Culture = (CultureInfo) CultureInfo.CurrentCulture.Clone(); }
默认情况下(至少在我的系统上),CultureInfo.CurrentCulture
指定 2 位小数。我们必须覆盖此行为,并使用我们的 DecimalPlaces
DependencyProperty。恰好,构造函数也是实现此目的的地方,所以让我们进一步修改它,如下所示
public NumericUpDown() { Culture = (CultureInfo) CultureInfo.CurrentCulture.Clone(); Culture.NumberFormat.NumberDecimalDigits = DecimalPlaces; }
不过,有一个小小的复杂之处——小数位数不能是负数,并且受架构限制,最多只能有 28 位小数。因此,我们必须强制转换 DecimalPlaces
中的输入。如下修改 CoerceDecimalPlaces()
private static object CoerceDecimalPlaces(DependencyObject element, Object baseValue)
{
var decimalPlaces = (Int32) baseValue;
if (decimalPlaces < 0)
{
decimalPlaces = 0;
}
else if (decimalPlaces > 28)
{
decimalPlaces = 28;
}
return decimalPlaces;
}
任何不符合这两个限制的数字在分配给 DecimalPlaces
属性之前都会被强制转换。
剩下的就是每次 DecimalPlaces
更改时更新我们的 Culture
变量。如下修改 OnDecimalPlacesChanged()
private static void OnDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var decimalPlaces = (Int32) e.NewValue;
control.Culture.NumberFormat.NumberDecimalDigits = decimalPlaces;
}
变量 decimalPlaces
已在强制转换方法中检查过,因此此赋值没有问题。
正如我所说,所有格式化都由一个函数完成,即 ToString()
。目前,它的使用方式如下
control.TextBox.Text = value.ToString(CultureInfo.CurrentCulture);
我们已经知道必须用我们的 Culture
变量替换 CultureInfo.CurrentCulture
,但这还不够。我们还必须指定一个格式,通过它(duh)格式化值。在我们的例子中,它是 "F",代表 Fixed-point(定点)。您可以在这里阅读更多关于格式的信息。
要指定格式并使用我们自己的 Culture
,请如下修改 CoerceValue()
方法
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.CoerceValueToBounds(ref value);
if (control.TextBox != null)
{
control.TextBox.Text = value.ToString("F", control.Culture);
}
return value;
}
太棒了,Value
从此完全按照我们自己的规则格式化。 别忘了添加空值检查,因为此方法首次运行时 TextBox 可能不存在。
最后剩下的一件事是每次 DecimalPlaces
更改时更新 Value
。如下修改 OnDecimalPlacesChanged()
private static void OnDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var decimalPlaces = (Int32) e.NewValue;
control.Culture.NumberFormat.NumberDecimalDigits = decimalPlaces;
control.InvalidateProperty(ValueProperty);
}
InvalidateProperty()
对我们来说是新的。它所做的只是告诉 Value
使自己失效(再次运行 CoerceValue()
方法)。这为什么好呢?好吧,我们的格式化函数(ToString()
)就在那里。当它格式化 value
时,它使用我们更新的 Culture
变量,带有不同的小数位数——这正是我们所需要的。
限制小数位数
限制小数位数肯定会派上用场,所以我们来实现它。
出乎意料的是,它都始于两个 DependencyProperties,MaxDecimalPlaces
和 MinDecimalPlaces
。
public static readonly DependencyProperty MaxDecimalPlacesProperty =
DependencyProperty.Register("MaxDecimalPlaces", typeof(Int32), typeof(NumericUpDown),
new PropertyMetadata(28, OnMaxDecimalPlacesChanged,
CoerceMaxDecimalPlaces));
public Int32 MaxDecimalPlaces
{
get { return (Int32)GetValue(MaxDecimalPlacesProperty); }
set { SetValue(MaxDecimalPlacesProperty, value); }
}
private static void OnMaxDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
private static object CoerceMaxDecimalPlaces(DependencyObject element, Object baseValue)
{
var maxDecimalPlaces = (Int32) baseValue;
return maxDecimalPlaces;
}
public static readonly DependencyProperty MinDecimalPlacesProperty =
DependencyProperty.Register("MinDecimalPlaces", typeof(Int32), typeof(NumericUpDown),
new PropertyMetadata(0, OnMinDecimalPlacesChanged,
CoerceMinDecimalPlaces));
public Int32 MinDecimalPlaces
{
get { return (Int32)GetValue(MinDecimalPlacesProperty); }
set { SetValue(MinDecimalPlacesProperty, value); }
}
private static void OnMinDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
private static object CoerceMinDecimalPlaces(DependencyObject element, Object baseValue)
{
var minDecimalPlaces = (Int32) baseValue;
return minDecimalPlaces;
}
MaxDecimalPlaces
默认设置为 28,MinDecimalPlaces
默认设置为 0,两者都将使用 PropertyChangedCallback
和 CoerceValueCallback
。
首先,我们添加明显的强制转换——MaxDecimalPlaces
不能超过 28,MinDecimalPlaces
必须大于 0。我们基本上只是将强制转换从 DecimalPlaces
属性转移到这两个属性中。如下修改 CoerceMaxDecimalPlaces()
private static object CoerceMaxDecimalPlaces(DependencyObject element, Object baseValue)
{
var maxDecimalPlaces = (Int32)baseValue;
var control = (NumericUpDown) element;
if (maxDecimalPlaces > 28)
{
maxDecimalPlaces = 28;
}
else if (maxDecimalPlaces < 0)
{
maxDecimalPlaces = 0;
}
else if (maxDecimalPlaces < control.MinDecimalPlaces)
{
control.MinDecimalPlaces = maxDecimalPlaces;
}
return maxDecimalPlaces;
}
这里没什么特别的,只需注意第二个 if 语句,确保 MaxDecimalPlaces
不会越过另一个边界(因为否则我们可能会通过 MaxDecimalPlaces
将 MinDecimalPlaces
推到负数)。
让我们继续下一个
private static object CoerceMinDecimalPlaces(DependencyObject element, Object baseValue)
{
var minDecimalPlaces = (Int32)baseValue;
var control = (NumericUpDown) element;
if (minDecimalPlaces < 0)
{
minDecimalPlaces = 0;
}
else if (minDecimalPlaces > 28)
{
minDecimalPlaces = 28;
}
else if (minDecimalPlaces > control.MaxDecimalPlaces)
{
control.MaxDecimalPlaces = minDecimalPlaces;
}
return minDecimalPlaces;
}
再次注意第二个 if 语句。否则,一切都如你所料。
在继续之前,让我们从 DecimalPlaces
中删除(现在已过时的)边界检查,并用我们新的边界系统替换它们。如下修改 CoerceDecimalPlaces()
private static object CoerceDecimalPlaces(DependencyObject element, Object baseValue)
{
var decimalPlaces = (Int32) baseValue;
var control = (NumericUpDown) element;
if (decimalPlaces < control.MinDecimalPlaces)
{
decimalPlaces = control.MinDecimalPlaces;
}
else if (decimalPlaces > control.MaxDecimalPlaces)
{
decimalPlaces = control.MaxDecimalPlaces;
}
return decimalPlaces;
}
代码应该很直观,我们只是检查任意边界末端,而不是硬编码的 28 和 0。
DecimalPlaces
实际上会利用期望值,所以没有必要避免它。在这种情况下,我们可以在每次任一边界末端改变时使 DecimalPlaces
失效。如下修改 OnMaxDecimalPlaces()
和 OnMinDecimalPlacesChanged()
private static void OnMaxDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
control.InvalidateProperty(DecimalPlacesProperty);
}
private static void OnMinDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
control.InvalidateProperty(DecimalPlacesProperty);
}
就是这样。现在每次 DecimalPlaces
更改时,Value
都会以正确的小数位数更新,并且每次任一小数边界末端更改时,DecimalPlaces
都会更新,从而更新 Value
(原谅冗余的解释)。这是一个幸福的大循环。
截断溢出的小数位
我们有一个具有一些很酷功能的 NumericUpDown,但请考虑一个小的场景。如果 DecimalPlaces
是 0,但某个用户在 TextBox 中输入“0.01”怎么办?好吧,Value
会设置为 0.01!您看不到它,因为 DecimalPlaces
会影响格式化以不显示小数位,但它们确实存在。而且,如果您将某个东西绑定到 Value
,当您发现它保留了小数部分并自豪地持有数字 0.01 时,您会大吃一惊。
解决方案?去除不属于的小数位。听起来很简单,但并非如此,因为我们必须以某种方式计算 Value
所拥有的小数位数。几乎唯一的方法是创建文本表示并找出小数点后有多少个数字。下面的方法就是这样做的
using System.Linq;
public Int32 GetDecimalPlacesCount(String valueString)
{
return valueString.SkipWhile(c => c.ToString(Culture)
!= Culture.NumberFormat.NumberDecimalSeparator).Skip(1).Count();
}
这个丑陋的 LINQ 恶魔子嗣实际上找到了第一个小数点,然后计算后面的所有字符。如果您不知道 LINQ 是什么,可以查阅 dotnetperls 作为起点。
我们需要实现的第二个方法是从一个 Decimal 数字中移除任意小数位数。请看下面的方法
private Decimal TruncateValue(String valueString, Int32 decimalPlaces) { var endPoint = valueString.Length - (decimalPlaces - DecimalPlaces); var tempValueString = valueString.Substring(0, endPoint); return Decimal.Parse(tempValueString, Culture); }
此方法只是从末尾删除不需要的字符(不需要的小数位),然后将更改后的字符串解析回 Decimal。虽然不是最干净的解决方案,但它适用于所有可能的数字。
让我们使用这两个方法,这样您就可以看到它们是如何协同工作的。如下修改 CoerceValue()
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.CoerceValueToBounds(ref value);
// Get the text representation of Value
var valueString = value.ToString(control.Culture);
// Count all decimal places
var decimalPlaces = control.GetDecimalPlacesCount(valueString);
if (decimalPlaces > control.DecimalPlaces)
{
// Remove all overflowing decimal places
value = control.TruncateValue(valueString, decimalPlaces);
}
if (control.TextBox != null)
{
control.TextBox.Text = value.ToString("F", control.Culture);
}
return value;
}
其中最难的部分无疑是这两个方法,所以其余的应该轻而易举。首先,我们获取 value
的文本表示。这里不需要指定格式,因为包含小数点的数字会自动格式化。然后我们计算小数位数,并检查 value
是否拥有的小数位数多于允许的位数(由 DecimalPlaces
属性决定)。如果多于,我们移除不属于的小数位,最后将值格式化到 TextBox 中。
动态小数点
现在,如果 DecimalPlaces
为 0 且用户输入 3.14,则输入将被截断为 3。如果我们想允许用户自己决定小数位数,以便如果他希望操作具有两位小数的数字,他只需通过输入具有该位数小数的数字来更改它,那该怎么办?听起来很酷,让我们来研究一下。
首先,我们需要实现一个 DependencyProperty,它将决定用户是否允许通过输入更改 DecimalPlaces
,或者输入是否被截断。我们将此属性命名为 IsDecimalPointDynamic
public static readonly DependencyProperty IsDecimalPointDynamicProperty =
DependencyProperty.Register("IsDecimalPointDynamic", typeof(Boolean), typeof(NumericUpDown),
new PropertyMetadata(false));
public Boolean IsDecimalPointDynamic
{
get { return (Boolean)GetValue(IsDecimalPointDynamicProperty); }
set { SetValue(IsDecimalPointDynamicProperty, value); }
}
每次分配 Value 时都会检查此属性。如下修改 CoerceValue()
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.CoerceValueToBounds(ref value);
// Get the text representation of Value
var valueString = value.ToString(control.Culture);
// Count all decimal places
var decimalPlaces = control.GetDecimalPlacesCount(valueString);
if (decimalPlaces > control.DecimalPlaces)
{
if (control.IsDecimalPointDynamic)
{
// Assigning DecimalPlaces will coerce the number
control.DecimalPlaces = decimalPlaces;
// If the specified number of decimal places is still too much
if (decimalPlaces > control.DecimalPlaces)
{
value = control.TruncateValue(valueString, control.DecimalPlaces);
}
}
else
{
// Remove all overflowing decimal places
value = control.TruncateValue(valueString, decimalPlaces);
}
}
else if (control.IsDecimalPointDynamic)
{
control.DecimalPlaces = decimalPlaces;
}
if (control.TextBox != null)
{
control.TextBox.Text = value.ToString("F", control.Culture);
}
return value;
}
我们检查 DecimalPlaces
属性是否可以动态更改。如果可以,一旦我们知道 valueString
有多少小数位,就必须立即分配 DecimalPlaces
。为什么?因为我们需要知道 valueString
的小数位数是否超过 MinDecimalPlaces
和 MaxDecimalPlaces
(通过分配 DecimalPlaces
,数字会被强制转换,还记得吗?)。如果不是,很好——我们直接进行值格式化。另一方面,如果是,我们需要截断溢出的小数位(这次是截断到 DecimalPlaces
中的强制转换值)。如果小数位较少,则第二个 if 语句会负责更改 DecimalPlaces
。
在它正确工作之前,我们需要做的最后一件事如下所示
private static void OnDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var decimalPlaces = (Int32) e.NewValue;
control.Culture.NumberFormat.NumberDecimalDigits = decimalPlaces;
if (control.IsDecimalPointDynamic)
{
control.IsDecimalPointDynamic = false;
control.InvalidateProperty(ValueProperty);
control.IsDecimalPointDynamic = true;
}
else
{
control.InvalidateProperty(ValueProperty);
}
}
为什么要进行这样的检查?好吧,假设用户输入“3.14”,IsDecimalPointDynamic
设置为 true 且 DecimalPoint
没有限制。在这种情况下,DecimalPlaces
会愉快地设置为 2(3.14 中的小数位数),一切看起来都正常。然而,事实并非如此。如果您随后尝试以任何方式更改 DecimalPlaces
,Value
将覆盖您的所有尝试。
DecimalPlaces
的每次更改都会使 Value
失效(如果您还记得,在 OnDecimalPlacesChanged()
中调用了 InvalidateProperty()
),因此,当 value
经过其 CoerceValue()
方法时,它会注意到 IsDecimalPointDynamic
为 true,并将 DecimalPlaces
分配为 2(TextBox 中数字的小数位数),从而覆盖之前设置的任何 DecimalPlaces
。呼,这有点令人困惑。
添加千位分隔符
千位分隔符对于较大的数字当然非常有用。如果您不知道千位分隔符是什么,只需快速瞥一眼这里就足够了。
让我们从一个名为 IsThousandSeparatorVisible
的 DependendyProperty 开始
public static readonly DependencyProperty IsThousandSeparatorVisibleProperty =
DependencyProperty.Register("IsThousandSeparatorVisible", typeof(Boolean), typeof(NumericUpDown),
new PropertyMetadata(false, OnIsThousandSeparatorVisibleChanged));
public Boolean IsThousandSeparatorVisible
{
get { return (Boolean)GetValue(IsThousandSeparatorVisibleProperty); }
set { SetValue(IsThousandSeparatorVisibleProperty, value); }
}
private static void OnIsThousandSeparatorVisibleChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
我们将使用 PropertyChangedCallback 来使 Value
失效(它会重新格式化 TextBox,带上(或不带)千位分隔符)。
如下修改 OnIsThousandSeparatorVisibleChanged()
private static void OnIsThousandSeparatorVisibleChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown)element;
control.InvalidateProperty(ValueProperty);
}
现在我们必须根据这个 DependencyProperty 实际更改格式类型。如下修改 CoerceValue()
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.CoerceValueToBounds(ref value);
// Get the text representation of Value
var valueString = value.ToString(control.Culture);
// Count all decimal places
var decimalPlaces = control.GetDecimalPlacesCount(valueString);
if (decimalPlaces > control.DecimalPlaces)
{
if (control.IsDecimalPointDynamic)
{
// Assigning DecimalPlaces will coerce the number
control.DecimalPlaces = decimalPlaces;
// If the specified number of decimal places is still too much
if (decimalPlaces > control.DecimalPlaces)
{
value = control.TruncateValue(valueString, control.DecimalPlaces);
}
}
else
{
// Remove all overflowing decimal places
value = control.TruncateValue(valueString, decimalPlaces);
}
}
else if (control.IsDecimalPointDynamic)
{
control.DecimalPlaces = decimalPlaces;
}
// Change formatting based on this flag
if (control.IsThousandSeparatorVisible)
{
if (control.TextBox != null)
{
control.TextBox.Text = value.ToString("N", control.Culture);
}
}
else
{
if (control.TextBox != null)
{
control.TextBox.Text = value.ToString("F", control.Culture);
}
}
return value;
}
是的,没错,添加千位分隔符的秘诀是将格式从 "F" 更改为 "N" - Number。同样,不要忘记空值检查。
添加次要和主要增量
要添加此功能,我们需要修改我们命令的两个回调。此外,这里出现了两个全新的命令
- _majorIncreaseValueCommand
- _majorDecreaseValueCommand
因此,如果您一直在想为什么之前的命令名称以“minor”开头,答案就在这里:次要命令使用 MinorDelta
,主要命令使用 MajorDelta
。
首先,让我们为每个增量创建 DependencyProperties
public static readonly DependencyProperty MinorDeltaProperty =
DependencyProperty.Register("MinorDelta", typeof(Decimal), typeof(NumericUpDown),
new PropertyMetadata(1m, OnMinorDeltaChanged,
CoerceMinorDelta));
public Decimal MinorDelta
{
get { return (Decimal)GetValue(MinorDeltaProperty); }
set { SetValue(MinorDeltaProperty, value); }
}
private static void OnMinorDeltaChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
private static object CoerceMinorDelta(DependencyObject element, Object baseValue)
{
var minorDelta = (Decimal)baseValue;
return minorDelta;
}
这两个属性唯一不同的部分是默认值,1m 和 10m。永远不要忘记 m 说明符,否则控件将拒绝编译!
public static readonly DependencyProperty MajorDeltaProperty =
DependencyProperty.Register("MajorDelta", typeof(Decimal), typeof(NumericUpDown),
new PropertyMetadata(10m, OnMajorDeltaChanged,
CoerceMajorDelta));
public Decimal MajorDelta
{
get { return (Decimal)GetValue(MajorDeltaProperty); }
set { SetValue(MajorDeltaProperty, value); }
}
private static void OnMajorDeltaChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
}
private static object CoerceMajorDelta(DependencyObject element, Object baseValue)
{
var majorDelta = (Decimal)baseValue;
return majorDelta;
}
现在我们有了这些属性,我们需要编写标准边界系统。如下修改 OnMinorDeltaChanged()
private static void OnMinorDeltaChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var minorDelta = (Decimal)e.NewValue;
var control = (NumericUpDown)element;
if (minorDelta > control.MajorDelta)
{
control.MajorDelta = minorDelta;
}
}
OnMajorDeltaChanged()
的作用也大致相同
private static void OnMajorDeltaChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var majorDelta = (Decimal) e.NewValue;
var control = (NumericUpDown) element;
if (majorDelta < control.MinorDelta)
{
control.MinorDelta = majorDelta;
}
}
我们已经做过类似的事情了,所以真的没什么好说的。一个边界推动另一个边界,以确保它们永远不会交叉——非常简单。
我们可以立即使用这两个增量。如下修改 IncreaseValue()
和 DecreaseValue()
private void IncreaseValue(Boolean minor) { // Get the value that's currently in the _textBox.Text decimal value = ParseStringToDecimal(TextBox.Text); // Coerce the value to min/max CoerceValueToBounds(ref value); // Only change the value if it has any meaning if (value >= MinValue) { if (minor) { value += MinorDelta; } else { value += MajorDelta; } } Value = value; }
private void DecreaseValue(Boolean minor) { // Get the value that's currently in the _textBox.Text decimal value = ParseStringToDecimal(TextBox.Text); // Coerce the value to min/max CoerceValueToBounds(ref value); // Only change the value if it has any meaning if (value <= MaxValue) { if (minor) { value -= MinorDelta; } else { value -= MajorDelta; } } Value = value; }
我们不是简单地将 Value
增加/减少 1,而是根据 minor
标志使用 MinorDelta
和 MajorDelta
。
为了提供决定使用哪个增量的布尔值,我们需要新的命令
private readonly RoutedUICommand _majorDecreaseValueCommand = new RoutedUICommand("MajorDecreaseValue", "MajorDecreaseValue", typeof(NumericUpDown)); private readonly RoutedUICommand _majorIncreaseValueCommand = new RoutedUICommand("MajorIncreaseValue", "MajorIncreaseValue", typeof(NumericUpDown));
现在我们不仅需要绑定新命令,还需要由于添加的参数而修改 IncreaseValue()
和 DecreaseValue()
的调用方式。
private void AttachCommands()
{
CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue(true)));
CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue(true)));
CommandBindings.Add(new CommandBinding(_majorIncreaseValueCommand, (a, b) => IncreaseValue(false)));
CommandBindings.Add(new CommandBinding(_majorDecreaseValueCommand, (a, b) => DecreaseValue(false)));
CommandBindings.Add(new CommandBinding(_updateValueStringCommand, (a, b) =>
{
Value = ParseStringToDecimal(TextBox.Text);
}));
CommandManager.RegisterClassInputBinding(typeof (TextBox),
new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
CommandManager.RegisterClassInputBinding(typeof (TextBox),
new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
CommandManager.RegisterClassInputBinding(typeof(TextBox),
new KeyBinding(_majorIncreaseValueCommand, new KeyGesture(Key.PageUp)));
CommandManager.RegisterClassInputBinding(typeof(TextBox),
new KeyBinding(_majorDecreaseValueCommand, new KeyGesture(Key.PageDown)));
CommandManager.RegisterClassInputBinding(typeof (TextBox),
new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));
}
我们不再调用不带任何参数的 IncreaseValue()
和 DecreaseValue()
。我们传递的标志正是 minor
标志,这样 IncreaseValue()
和 DecreaseValue()
就能使用适当的增量来修改 Value
。此外,请注意,两个主要命令都绑定到 PageUp/PageDown 键。您可以立即尝试,将焦点放在 NumericUpDown 上并按下其中一个键,将 Value
增加 10(默认值)。
允许自动选择
虽然我不太喜欢这个功能,但我经常在各种用户界面中看到它。我承认,大多数时候你可能想输入一个完全不同的数字而不是编辑单个数字,所以我能理解它为什么可能有用。
一开始,总会有一个 DependencyProperty
public static readonly DependencyProperty IsAutoSelectionActiveProperty =
DependencyProperty.Register("IsAutoSelectionActive", typeof(Boolean), typeof(NumericUpDown),
new PropertyMetadata(false));
public Boolean IsAutoSelectionActive
{
get { return (Boolean)GetValue(IsAutoSelectionActiveProperty); }
set { SetValue(IsAutoSelectionActiveProperty, value); }
}
不需要 PropertyChangedCallback 或 CoerceValueCallback。
我们还需要知道 TextBox 何时被点击,所以让我们拦截 PreviewMouseLeftButtonUp
事件。为此,如下修改 AttachTextBox()
方法
private void AttachTextBox()
{
var textBox = GetTemplateChild("PART_TextBox") as TextBox;
// A null check is advised
if (textBox != null)
{
TextBox = textBox;
TextBox.LostFocus += TextBoxOnLostFocus;
TextBox.PreviewMouseLeftButtonUp += TextBoxOnPreviewMouseLeftButtonUp;
}
}
TextBoxOnPreviewMouseLeftButtonUp()
方法(是不是很顺口?)执行以下操作
private void TextBoxOnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
if (IsAutoSelectionActive)
{
TextBox.SelectAll();
}
}
很简单——每次 TextBox 被点击时(前提是 IsAutoSelectionActive
为 true),我们都会选择 TextBox 的全部内容。第二次点击时选择会自动消失,所以它仍然允许您编辑单个数字。这是一个双赢的局面。
值环绕
环绕通过将 Value
设置到边界的末端来实现,该末端与 Value
在增加或减少时穿过的末端相反。这里没有太大的问题,理解起来应该不难。
我们需要一个 DependencyProperty 来知道我们是否可以环绕
public static readonly DependencyProperty IsValueWrapAllowedProperty = DependencyProperty.Register("IsValueWrapAllowed", typeof(Boolean), typeof(NumericUpDown), new PropertyMetadata(false)); public Boolean IsValueWrapAllowed { get { return (Boolean)GetValue(IsValueWrapAllowedProperty); } set { SetValue(IsValueWrapAllowedProperty, value); } }
不需要 CoerceValueCallback 或 PropertyChangedCallback。
我们需要包装 Value
。如下修改 IncreaseValue()
和 DecreaseValue()
private void IncreaseValue(Boolean minor) { // Get the value that's currently in the _textBox.Text decimal value = ParseStringToDecimal(TextBox.Text); // Coerce the value to min/max CoerceValueToBounds(ref value); // Only change the value if it has any meaning if (value >= MinValue) { if (minor) { if (IsValueWrapAllowed && value + MinorDelta > MaxValue) { value = MinValue; } else { value += MinorDelta; } } else { if (IsValueWrapAllowed && value + MajorDelta > MaxValue) { value = MinValue; } else { value += MajorDelta; } } } Value = value; }
private void DecreaseValue(Boolean minor) { // Get the value that's currently in the _textBox.Text decimal value = ParseStringToDecimal(TextBox.Text); // Coerce the value to min/max CoerceValueToBounds(ref value); // Only change the value if it has any meaning if (value <= MaxValue) { if (minor) { if (IsValueWrapAllowed && value - MinorDelta < MinValue) { value = MaxValue; } else { value -= MinorDelta; } } else { if (IsValueWrapAllowed && value - MajorDelta < MinValue) { value = MaxValue; } else { value -= MajorDelta; } } } Value = value; }
就是这样。现在,每次您尝试将 Value
增加或减少超出其中一个边界时(前提是 IsValueWrapAllowed
为 true),它都会被设置为相反的边界末端。
默认的 TextBox 内容
啊,是的。直到现在,NumericUpDown 总是以空值启动。我知道,我知道,这很容易补救。只需如下修改实例构造函数
public NumericUpDown() { Culture = (CultureInfo) CultureInfo.CurrentCulture.Clone(); Culture.NumberFormat.NumberDecimalDigits = DecimalPlaces; Loaded += OnLoaded; }
是的,我们必须拦截这个事件,因为只有这样我们才能确定 TextBox 已经附加。
OnLoaded()
方法如下所示
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs) { InvalidateProperty(ValueProperty); }
我们在这里所要做的就是使 Value 失效,这会自动填充 TextBox。非常简单。
右键清零
让我们实现一个我看到后立刻喜欢上的功能。基本上,您可以右键单击侧面的任何一个按钮,然后将 Value
清零。这非常方便,可以节省大量时间。
我们所要做的就是拦截两个 RepeatButtons 上的右键单击,同时移除焦点
private void AttachIncreaseButton()
{
var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
if (increaseButton != null)
{
IncreaseButton = increaseButton;
IncreaseButton.Focusable = false;
IncreaseButton.Command = _minorIncreaseValueCommand;
IncreaseButton.PreviewMouseRightButtonDown += ButtonOnPreviewMouseRightButtonDown;
IncreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus();
}
}
private void AttachDecreaseButton()
{
var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
if (decreaseButton != null)
{
DecreaseButton = decreaseButton;
DecreaseButton.Focusable = false;
DecreaseButton.Command = _minorDecreaseValueCommand;
DecreaseButton.PreviewMouseRightButtonDown += ButtonOnPreviewMouseRightButtonDown;
DecreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus();
}
}
我们对这两个按钮使用相同的事件处理程序。如下创建 ButtonOnPreviewMouseRightButtonDown()
private void ButtonOnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
Value = 0;
}
就是这样。我们无需关心 Value
是否可以设置为 0。如果不能,Value
会自动强制转换。
添加取消未确认更改的功能
此功能非常有用。当您在 TextBox 中输入内容但不确认时,您可以通过按 Esc 键摆脱输入的值,从而取消未确认的更改。
首先,我们需要设置我们的 TextBox 以允许撤销。如下修改 AttachTextBox()
private void AttachTextBox() { var textBox = GetTemplateChild("PART_TextBox") as TextBox; // A null check is advised if (textBox != null) { TextBox = textBox; TextBox.LostFocus += TextBoxOnLostFocus; TextBox.PreviewMouseLeftButtonUp += TextBoxOnPreviewMouseLeftButtonUp; TextBox.UndoLimit = 1; TextBox.IsUndoEnabled = true; } }
这将允许我们撤销精确地一次更改。
接下来,我们需要创建一个名为 _cancelChangesCommand 的命令,一个非常描述性的名称。如下添加它
private readonly RoutedUICommand _cancelChangesCommand =
new RoutedUICommand("CancelChanges", "CancelChanges", typeof(NumericUpDown));
正如您可能已经知道的,现在我们需要将命令绑定到其各自的回调并为其分配一个 InputBinding。如下修改 AttachCommands()
private void AttachCommands()
{
CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue(true)));
CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue(true)));
CommandBindings.Add(new CommandBinding(_majorIncreaseValueCommand, (a, b) => IncreaseValue(false)));
CommandBindings.Add(new CommandBinding(_majorDecreaseValueCommand, (a, b) => DecreaseValue(false)));
CommandBindings.Add(new CommandBinding(_updateValueStringCommand, (a, b) =>
{
Value = ParseStringToDecimal(TextBox.Text);
}));
CommandBindings.Add(new CommandBinding(_cancelChangesCommand, (a, b) => CancelChanges()));
CommandManager.RegisterClassInputBinding(typeof (TextBox),
new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
CommandManager.RegisterClassInputBinding(typeof (TextBox),
new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
CommandManager.RegisterClassInputBinding(typeof(TextBox),
new KeyBinding(_majorIncreaseValueCommand, new KeyGesture(Key.PageUp)));
CommandManager.RegisterClassInputBinding(typeof(TextBox),
new KeyBinding(_majorDecreaseValueCommand, new KeyGesture(Key.PageDown)));
CommandManager.RegisterClassInputBinding(typeof (TextBox),
new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));
CommandManager.RegisterClassInputBinding(typeof(TextBox),
new KeyBinding(_cancelChangesCommand, new KeyGesture(Key.Escape)));
}
_cancelChangedCommand 调用 CancelChanges()
- 一个非常简单的方法,如下所示
private void CancelChanges()
{
TextBox.Undo();
}
这个方法非常原始,我想不需要解释。
不过,我们还需要做最后一件事。如下修改 OnValueChanged()
private static void OnValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
control.TextBox.UndoLimit = 0;
control.TextBox.UndoLimit = 1;
}
将 UndoLimit
设置为 0 会擦除 TextBox 保存的任何撤销信息。为什么要这样做?因为如果我们不这样做,您将能够撤销任何更改(甚至是已确认的更改),而这不是我们需要的。擦除撤销信息后,我们将限制恢复为 1。
这个功能当然不复杂,但我发现它非常有用。
结论
就是这样。现在我们应该有一个功能齐全的 NumericUpDown 控件可以使用了。 我希望您阅读这篇文章和我写这篇文章一样愉快(实际上,校对它可能是我今年做过的最无聊的事情)。