260 lines
11 KiB
C#
260 lines
11 KiB
C#
|
|
using System.Windows.Input;
|
|||
|
|
|
|||
|
|
namespace NeoUI.Controls
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// NumberBox 控件提供了一个用于输入和显示数值的界面。它允许用户通过键盘、鼠标滚轮或界面上的增减按钮来调整数值。
|
|||
|
|
/// 此控件支持最小值、最大值设置,以及步长和精度控制,适用于需要精确数值输入的应用场景。
|
|||
|
|
/// </summary>
|
|||
|
|
[TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))]
|
|||
|
|
[TemplatePart(Name = "PART_IncreaseButton", Type = typeof(Button))]
|
|||
|
|
[TemplatePart(Name = "PART_DecreaseButton", Type = typeof(Button))]
|
|||
|
|
public class NumberBox : Control
|
|||
|
|
{
|
|||
|
|
private TextBox? textBox;
|
|||
|
|
private bool isUpdatingText; // 防止在更新文本时触发循环
|
|||
|
|
|
|||
|
|
static NumberBox()
|
|||
|
|
{
|
|||
|
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(NumberBox), new FrameworkPropertyMetadata(typeof(NumberBox)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region Dependency Properties
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件的值属性。此依赖属性允许双向数据绑定,并且其默认类型为可空十进制数(decimal?)。
|
|||
|
|
/// 当此属性的值发生变化时,将触发 OnValueChanged 方法,用于更新控件内部状态。
|
|||
|
|
/// </summary>
|
|||
|
|
public static readonly DependencyProperty ValueProperty =
|
|||
|
|
DependencyProperty.Register(nameof(Value), typeof(decimal?), typeof(NumberBox),
|
|||
|
|
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取或设置 NumberBox 控件的当前值。此属性支持双向数据绑定,其类型为可空十进制数(decimal?)。
|
|||
|
|
/// 当此属性的值发生变化时,控件将自动进行值的钳位和舍入处理,并更新显示文本以反映新的数值。
|
|||
|
|
/// </summary>
|
|||
|
|
public decimal? Value
|
|||
|
|
{
|
|||
|
|
get => (decimal?)GetValue(ValueProperty);
|
|||
|
|
set => SetValue(ValueProperty, value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件的最小值属性。此依赖属性定义了用户可以在控件中设置的最小数值,默认值为 decimal.MinValue。
|
|||
|
|
/// 通过设置此属性,可以限制用户输入或选择的数值范围,确保不会低于预设的最小值。
|
|||
|
|
/// </summary>
|
|||
|
|
public static readonly DependencyProperty MinValueProperty =
|
|||
|
|
DependencyProperty.Register(nameof(MinValue), typeof(decimal), typeof(NumberBox), new PropertyMetadata(decimal.MinValue));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件可以接受的最小值。此依赖属性允许设置控件值的下限,默认类型为十进制数(decimal)。当设置一个新值时,如果该值小于 MinValue,则实际值将被自动调整至 MinValue。此外,MinValue 与 MaxValue 一起使用,以确保控件内的数值保持在指定范围内。
|
|||
|
|
/// </summary>
|
|||
|
|
public decimal MinValue
|
|||
|
|
{
|
|||
|
|
get => (decimal)GetValue(MinValueProperty);
|
|||
|
|
set => SetValue(MinValueProperty, value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件的最大值属性。此依赖属性定义了用户可以在控件中设置的最大数值,默认值为 decimal.MaxValue。
|
|||
|
|
/// 通过设置此属性,可以限制用户输入或选择的数值范围,确保不会超出预设的最大值。
|
|||
|
|
/// </summary>
|
|||
|
|
public static readonly DependencyProperty MaxValueProperty =
|
|||
|
|
DependencyProperty.Register(nameof(MaxValue), typeof(decimal), typeof(NumberBox), new PropertyMetadata(decimal.MaxValue));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件允许设置的最大值。此依赖属性的默认值为 decimal.MaxValue。
|
|||
|
|
/// 通过设置此属性,可以限制用户输入或调整到的最大数值。当 Value 属性的值超过 MaxValue 时,将自动钳位至 MaxValue。
|
|||
|
|
/// </summary>
|
|||
|
|
public decimal MaxValue
|
|||
|
|
{
|
|||
|
|
get => (decimal)GetValue(MaxValueProperty);
|
|||
|
|
set => SetValue(MaxValueProperty, value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件的步长属性。此依赖属性定义了每次通过按钮或滚轮调整数值时增加或减少的具体数值,默认值为 1。
|
|||
|
|
/// 步长可以是任何正的小数,允许用户以更精细的方式控制数值的增减。
|
|||
|
|
/// </summary>
|
|||
|
|
public static readonly DependencyProperty StepProperty =
|
|||
|
|
DependencyProperty.Register(nameof(Step), typeof(decimal), typeof(NumberBox), new PropertyMetadata(1m));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件的步长属性。此依赖属性定义了用户每次点击增减按钮或使用键盘方向键时数值变化的大小,默认值为1。
|
|||
|
|
/// 通过设置不同的步长,可以控制数值增加或减少的具体幅度,从而满足不同应用场景下对数值精度的要求。
|
|||
|
|
/// </summary>
|
|||
|
|
public decimal Step
|
|||
|
|
{
|
|||
|
|
get => (decimal)GetValue(StepProperty);
|
|||
|
|
set => SetValue(StepProperty, value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件的精度属性。此依赖属性定义了数值显示的小数位数,类型为整数(int)。
|
|||
|
|
/// 当设置此属性时,控件会根据新的精度值重新格式化当前显示的数值,并确保数值的有效性。
|
|||
|
|
/// </summary>
|
|||
|
|
public static readonly DependencyProperty PrecisionProperty =
|
|||
|
|
DependencyProperty.Register(nameof(Precision), typeof(int), typeof(NumberBox), new PropertyMetadata(0, OnPrecisionChanged));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 NumberBox 控件的小数精度属性。此依赖属性定义了数值显示和处理时保留的小数位数,默认值为 0,即不保留小数。
|
|||
|
|
/// 当 Precision 属性的值发生变化时,控件会自动调整当前 Value 的显示格式,并根据新的精度重新舍入数值。
|
|||
|
|
/// </summary>
|
|||
|
|
public int Precision
|
|||
|
|
{
|
|||
|
|
get => (int)GetValue(PrecisionProperty);
|
|||
|
|
set => SetValue(PrecisionProperty, value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|||
|
|
{
|
|||
|
|
var control = (NumberBox)d;
|
|||
|
|
var newValue = (decimal?)e.NewValue;
|
|||
|
|
|
|||
|
|
if (newValue.HasValue)
|
|||
|
|
{
|
|||
|
|
var value = newValue.Value;
|
|||
|
|
// 1. 钳位 (Clamp)
|
|||
|
|
var clampedValue = Math.Max(control.MinValue, Math.Min(control.MaxValue, value));
|
|||
|
|
// 2. 舍入 (Round)
|
|||
|
|
var roundedValue = Math.Round(clampedValue, control.Precision);
|
|||
|
|
|
|||
|
|
// 如果经过处理的值与新值不同,则更新依赖属性
|
|||
|
|
if (roundedValue != newValue.Value)
|
|||
|
|
{
|
|||
|
|
control.Value = roundedValue;
|
|||
|
|
return; // 再次触发 OnValueChanged,但最终会同步并执行下面的 UpdateText
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
control.UpdateText();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void OnPrecisionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|||
|
|
{
|
|||
|
|
var control = (NumberBox)d;
|
|||
|
|
// 当精度变化时,重新验证并格式化当前值
|
|||
|
|
if (control.Value.HasValue)
|
|||
|
|
{
|
|||
|
|
control.Value = Math.Round(control.Value.Value, control.Precision);
|
|||
|
|
}
|
|||
|
|
control.UpdateText();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
public override void OnApplyTemplate()
|
|||
|
|
{
|
|||
|
|
base.OnApplyTemplate();
|
|||
|
|
|
|||
|
|
// 解除旧的事件处理器
|
|||
|
|
if (textBox != null)
|
|||
|
|
{
|
|||
|
|
textBox.LostFocus -= OnTextBoxLostFocus;
|
|||
|
|
textBox.KeyDown -= OnTextBoxKeyDown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
textBox = GetTemplateChild("PART_TextBox") as TextBox;
|
|||
|
|
if (textBox != null)
|
|||
|
|
{
|
|||
|
|
textBox.LostFocus += OnTextBoxLostFocus;
|
|||
|
|
textBox.KeyDown += OnTextBoxKeyDown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (GetTemplateChild("PART_IncreaseButton") is Button increaseButton)
|
|||
|
|
{
|
|||
|
|
increaseButton.Click += (_, _) => ChangeValue(Step);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (GetTemplateChild("PART_DecreaseButton") is Button decreaseButton)
|
|||
|
|
{
|
|||
|
|
decreaseButton.Click += (_, _) => ChangeValue(-Step);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
UpdateText(); // 初始加载时更新文本
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnTextBoxKeyDown(object sender, KeyEventArgs e)
|
|||
|
|
{
|
|||
|
|
if (e.Key != Key.Enter) return;
|
|||
|
|
ParseAndUpdateValue();
|
|||
|
|
// 将焦点移走,模拟失焦效果
|
|||
|
|
textBox?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnTextBoxLostFocus(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
ParseAndUpdateValue();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 解析文本框内容并更新Value属性,如果解析失败则恢复文本
|
|||
|
|
/// </summary>
|
|||
|
|
private void ParseAndUpdateValue()
|
|||
|
|
{
|
|||
|
|
if (textBox == null || isUpdatingText) return;
|
|||
|
|
|
|||
|
|
if (decimal.TryParse(textBox.Text, out var parsedValue))
|
|||
|
|
{
|
|||
|
|
Value = parsedValue;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// 输入无效,恢复为上次的有效值
|
|||
|
|
UpdateText();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 根据当前的Value和Precision格式化更新TextBox的文本
|
|||
|
|
/// </summary>
|
|||
|
|
private void UpdateText()
|
|||
|
|
{
|
|||
|
|
if (textBox == null) return;
|
|||
|
|
|
|||
|
|
isUpdatingText = true;
|
|||
|
|
textBox.Text = Value.HasValue ? Value.Value.ToString("F" + Precision) : string.Empty;
|
|||
|
|
isUpdatingText = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
protected override void OnKeyDown(KeyEventArgs e)
|
|||
|
|
{
|
|||
|
|
base.OnKeyDown(e);
|
|||
|
|
if (e.Key == Key.Up)
|
|||
|
|
{
|
|||
|
|
ChangeValue(Step);
|
|||
|
|
e.Handled = true;
|
|||
|
|
}
|
|||
|
|
else if (e.Key == Key.Down)
|
|||
|
|
{
|
|||
|
|
ChangeValue(-Step);
|
|||
|
|
e.Handled = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
protected override void OnMouseWheel(MouseWheelEventArgs e)
|
|||
|
|
{
|
|||
|
|
base.OnMouseWheel(e);
|
|||
|
|
if (IsKeyboardFocusWithin) // 仅当控件有焦点时响应滚轮
|
|||
|
|
{
|
|||
|
|
if (e.Delta > 0)
|
|||
|
|
{
|
|||
|
|
ChangeValue(Step);
|
|||
|
|
}
|
|||
|
|
else if (e.Delta < 0)
|
|||
|
|
{
|
|||
|
|
ChangeValue(-Step);
|
|||
|
|
}
|
|||
|
|
e.Handled = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ChangeValue(decimal step)
|
|||
|
|
{
|
|||
|
|
Value = (Value ?? 0) + step;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|