using HwControlLib.UtilitiesHelper; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; namespace HwControlLib { /// /// 数字输入框 /// Author /// [DefaultEvent("ValueChanged"), DefaultProperty("Value")] [TemplatePart(Name = TextBoxTemplateName, Type = typeof(TextBox))] [TemplatePart(Name = NumericUpTemplateName, Type = typeof(RepeatButton))] [TemplatePart(Name = NumericDownTemplateName, Type = typeof(RepeatButton))] public class NumericBox : Control { private static readonly Type _typeofSelf = typeof(NumericBox); private const double DefaultInterval = 1d; private const int DefaultDelay = 500; private const string TextBoxTemplateName = "PART_TextBox"; private const string NumericUpTemplateName = "PART_NumericUp"; private const string NumericDownTemplateName = "PART_NumericDown"; private static RoutedCommand _increaseCommand = null; private static RoutedCommand _decreaseCommand = null; private TextBox _valueTextBox; private RepeatButton _repeatUp; private RepeatButton _repeatDown; private double _internalLargeChange = DefaultInterval; private double _intervalValueSinceReset = 0; private double? _lastOldValue = null; private bool _isManual; private bool _isBusy; static NumericBox() { InitializeCommands(); DefaultStyleKeyProperty.OverrideMetadata(_typeofSelf, new FrameworkPropertyMetadata(_typeofSelf)); } #region Command private static void InitializeCommands() { _increaseCommand = new RoutedCommand("Increase", _typeofSelf); _decreaseCommand = new RoutedCommand("Decrease", _typeofSelf); CommandManager.RegisterClassCommandBinding(_typeofSelf, new CommandBinding(_increaseCommand, OnIncreaseCommand, OnCanIncreaseCommand)); CommandManager.RegisterClassCommandBinding(_typeofSelf, new CommandBinding(_decreaseCommand, OnDecreaseCommand, OnCanDecreaseCommand)); } public static RoutedCommand IncreaseCommand { get { return _increaseCommand; } } public static RoutedCommand DecreaseCommand { get { return _decreaseCommand; } } private static void OnIncreaseCommand(object sender, RoutedEventArgs e) { var numericBox = sender as NumericBox; numericBox.ContinueChangeValue(true); } private static void OnCanIncreaseCommand(object sender, CanExecuteRoutedEventArgs e) { var numericBox = sender as NumericBox; e.CanExecute = (!numericBox.IsReadOnly && numericBox.IsEnabled && DoubleUtil.LessThan(numericBox.Value, numericBox.Maximum)); } private static void OnDecreaseCommand(object sender, RoutedEventArgs e) { var numericBox = sender as NumericBox; numericBox.ContinueChangeValue(false); } private static void OnCanDecreaseCommand(object sender, CanExecuteRoutedEventArgs e) { var numericBox = sender as NumericBox; e.CanExecute = (!numericBox.IsReadOnly && numericBox.IsEnabled && DoubleUtil.GreaterThan(numericBox.Value, numericBox.Minimum)); } #endregion #region RouteEvent public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), _typeofSelf); public event RoutedPropertyChangedEventHandler ValueChanged { add { AddHandler(ValueChangedEvent, value); } remove { RemoveHandler(ValueChangedEvent, value); } } #endregion #region Properties public static readonly DependencyProperty DisabledValueChangedWhileBusyProperty = DependencyProperty.Register("DisabledValueChangedWhileBusy", typeof(bool), _typeofSelf, new PropertyMetadata(false)); [Category("Common")] [DefaultValue(true)] public bool DisabledValueChangedWhileBusy { get { return (bool)GetValue(DisabledValueChangedWhileBusyProperty); } set { SetValue(DisabledValueChangedWhileBusyProperty, value); } } public static readonly DependencyProperty IntervalProperty = DependencyProperty.Register("Interval", typeof(double), _typeofSelf, new FrameworkPropertyMetadata(DefaultInterval, IntervalChanged, CoerceInterval)); [Category("Behavior")] [DefaultValue(DefaultInterval)] public double Interval { get { return (double)GetValue(IntervalProperty); } set { SetValue(IntervalProperty, value); } } private static object CoerceInterval(DependencyObject d, object value) { var interval = (double)value; return DoubleUtil.IsNaN(interval) ? 0 : Math.Max(interval, 0); } private static void IntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var numericBox = (NumericBox)d; numericBox.ResetInternal(); } public static readonly DependencyProperty SpeedupProperty = DependencyProperty.Register("Speedup", typeof(bool), _typeofSelf, new PropertyMetadata(true)); [Category("Common")] [DefaultValue(true)] public bool Speedup { get { return (bool)GetValue(SpeedupProperty); } set { SetValue(SpeedupProperty, value); } } public static readonly DependencyProperty DelayProperty = DependencyProperty.Register("Delay", typeof(int), _typeofSelf, new PropertyMetadata(DefaultDelay, null, CoerceDelay)); [DefaultValue(DefaultDelay)] [Category("Behavior")] public int Delay { get { return (int)GetValue(DelayProperty); } set { SetValue(DelayProperty, value); } } private static object CoerceDelay(DependencyObject d, object value) { var delay = (int)value; return Math.Max(delay, 0); } public static readonly DependencyProperty UpDownButtonsWidthProperty = DependencyProperty.Register("UpDownButtonsWidth", typeof(double), _typeofSelf, new PropertyMetadata(20d)); [Category("Appearance")] [DefaultValue(20d)] public double UpDownButtonsWidth { get { return (double)GetValue(UpDownButtonsWidthProperty); } set { SetValue(UpDownButtonsWidthProperty, value); } } public static readonly DependencyProperty TextAlignmentProperty = TextBox.TextAlignmentProperty.AddOwner(_typeofSelf); [Category("Common")] public TextAlignment TextAlignment { get { return (TextAlignment)GetValue(TextAlignmentProperty); } set { SetValue(TextAlignmentProperty, value); } } public static readonly DependencyProperty IsReadOnlyProperty = TextBoxBase.IsReadOnlyProperty.AddOwner(_typeofSelf, new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits, IsReadOnlyPropertyChangedCallback)); [Category("Appearance")] public bool IsReadOnly { get { return (bool)GetValue(IsReadOnlyProperty); } set { SetValue(IsReadOnlyProperty, value); } } private static void IsReadOnlyPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (e.OldValue == e.NewValue || e.NewValue == null) return; ((NumericBox)d).ToggleReadOnlyMode((bool)e.NewValue); } public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register("Precision", typeof(int?), _typeofSelf, new PropertyMetadata(null, OnPrecisionChanged, CoercePrecision)); [Category("Common")] public int? Precision { get { return (int?)GetValue(PrecisionProperty); } set { SetValue(PrecisionProperty, value); } } private static object CoercePrecision(DependencyObject d, object value) { var precision = (int?)value; return (precision.HasValue && precision.Value < 0) ? 0 : precision; } private static void OnPrecisionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var numericBox = (NumericBox)d; var newPrecision = (int?)e.NewValue; var roundValue = numericBox.CorrectPrecision(newPrecision, numericBox.Value); if (DoubleUtil.AreClose(numericBox.Value, roundValue)) numericBox.InternalSetText(roundValue); else numericBox.Value = roundValue; } public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(double), _typeofSelf, new PropertyMetadata(double.MinValue, OnMinimumChanged)); [Category("Common")] public double Minimum { get { return (double)GetValue(MinimumProperty); } set { SetValue(MinimumProperty, value); } } private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var numericBox = (NumericBox)d; numericBox.CoerceValue(MaximumProperty, numericBox.Maximum); numericBox.CoerceValue(ValueProperty, numericBox.Value); } public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(double), _typeofSelf, new PropertyMetadata(double.MaxValue, OnMaximumChanged, CoerceMaximum)); [Category("Common")] public double Maximum { get { return (double)GetValue(MaximumProperty); } set { SetValue(MaximumProperty, value); } } private static object CoerceMaximum(DependencyObject d, object value) { var minimum = ((NumericBox)d).Minimum; var val = (double)value; return DoubleUtil.LessThan(val, minimum) ? minimum : val; } private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var numericBox = (NumericBox)d; numericBox.CoerceValue(ValueProperty, numericBox.Value); } public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), _typeofSelf, new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged, CoerceValue)); [Category("Common")] public double Value { get { return (double)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } private static object CoerceValue(DependencyObject d, object value) { var numericBox = (NumericBox)d; var val = (double)value; if (DoubleUtil.LessThan(val, numericBox.Minimum)) return numericBox.Minimum; if (DoubleUtil.GreaterThan(val, numericBox.Maximum)) return numericBox.Maximum; return numericBox.CorrectPrecision(numericBox.Precision, val); } private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var numericBox = (NumericBox)d; numericBox.OnValueChanged((double)e.OldValue, (double)e.NewValue); } #endregion #region Virtual protected virtual void OnValueChanged(double oldValue, double newValue) { InternalSetText(newValue); InvalidateRequerySuggested(newValue); if ((!_isBusy || !DisabledValueChangedWhileBusy) && !DoubleUtil.AreClose(oldValue, newValue)) { RaiseEvent(new TextBoxValueChangedEventArgs(oldValue, newValue, _isManual, _isBusy, ValueChangedEvent)); } _isManual = false; } #endregion #region Override public override void OnApplyTemplate() { base.OnApplyTemplate(); UnsubscribeEvents(); _valueTextBox = GetTemplateChild(TextBoxTemplateName) as TextBox; _repeatUp = GetTemplateChild(NumericUpTemplateName) as RepeatButton; _repeatDown = GetTemplateChild(NumericDownTemplateName) as RepeatButton; if (_valueTextBox == null || _repeatUp == null || _repeatDown == null) { throw new NullReferenceException(string.Format("You have missed to specify {0}, {1} or {2} in your template", NumericUpTemplateName, NumericDownTemplateName, TextBoxTemplateName)); } _repeatUp.PreviewMouseUp += OnRepeatButtonPreviewMouseUp; _repeatDown.PreviewMouseUp += OnRepeatButtonPreviewMouseUp; ToggleReadOnlyMode(IsReadOnly); OnValueChanged(Value, Value); } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); if (Focusable && !IsReadOnly) { Focused(); SelectAll(); } } protected override void OnPreviewMouseWheel(MouseWheelEventArgs e) { base.OnPreviewMouseWheel(e); if (e.Delta != 0 && (IsFocused || _valueTextBox.IsFocused)) ContinueChangeValue(e.Delta >= 0, false); } protected override void OnPreviewKeyDown(KeyEventArgs e) { base.OnPreviewKeyDown(e); switch (e.Key) { case Key.Enter: DealInputText(_valueTextBox.Text); SelectAll(); e.Handled = true; break; case Key.Up: ContinueChangeValue(true); e.Handled = true; break; case Key.Down: ContinueChangeValue(false); e.Handled = true; break; } } protected override void OnPreviewKeyUp(KeyEventArgs e) { base.OnPreviewKeyUp(e); switch (e.Key) { case Key.Down: case Key.Up: ResetInternal(); break; } } #endregion #region Event private void OnRepeatButtonPreviewMouseUp(object sender, MouseButtonEventArgs e) { ResetInternal(); } private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (Focusable && !IsReadOnly && !_valueTextBox.IsKeyboardFocusWithin) { e.Handled = true; Focused(); SelectAll(); } } private void OnTextBoxLostFocus(object sender, RoutedEventArgs e) { var tb = (TextBox)sender; DealInputText(tb.Text); } private void OnValueTextBoxPaste(object sender, DataObjectPastingEventArgs e) { var textBox = (TextBox)sender; var textPresent = textBox.Text; if (!e.SourceDataObject.GetDataPresent(DataFormats.Text, true)) return; var text = e.SourceDataObject.GetData(DataFormats.Text) as string; var newText = string.Concat(textPresent.Substring(0, textBox.SelectionStart), text, textPresent.Substring(textBox.SelectionStart + textBox.SelectionLength)); double number; if (!double.TryParse(newText, out number)) e.CancelCommand(); } #endregion #region Private private void UnsubscribeEvents() { if (_valueTextBox != null) { _valueTextBox.LostFocus -= OnTextBoxLostFocus; _valueTextBox.PreviewMouseLeftButtonDown -= OnPreviewMouseLeftButtonDown; DataObject.RemovePastingHandler(_valueTextBox, OnValueTextBoxPaste); } if (_repeatUp != null) _repeatUp.PreviewMouseUp -= OnRepeatButtonPreviewMouseUp; if (_repeatDown != null) _repeatDown.PreviewMouseUp -= OnRepeatButtonPreviewMouseUp; } private void Focused() { _valueTextBox?.Focus(); } private void SelectAll() { _valueTextBox?.SelectAll(); } private void DealInputText(string inputText) { double convertedValue; if (double.TryParse(inputText, out convertedValue)) { if (DoubleUtil.AreClose(Value, convertedValue)) { InternalSetText(Value); return; } _isManual = true; if (convertedValue > Maximum) { if (DoubleUtil.AreClose(Value, Maximum)) OnValueChanged(Value, Value); else Value = Maximum; } else if (convertedValue < Minimum) { if (DoubleUtil.AreClose(Value, Minimum)) OnValueChanged(Value, Value); else Value = Minimum; } else Value = convertedValue; } else InternalSetText(Value); } private void MoveFocus() { var request = new TraversalRequest((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift ? FocusNavigationDirection.Previous : FocusNavigationDirection.Next); var elementWithFocus = Keyboard.FocusedElement as UIElement; elementWithFocus?.MoveFocus(request); } private void ToggleReadOnlyMode(bool isReadOnly) { if (_valueTextBox == null) return; if (isReadOnly) { _valueTextBox.LostFocus -= OnTextBoxLostFocus; _valueTextBox.PreviewMouseLeftButtonDown -= OnPreviewMouseLeftButtonDown; DataObject.RemovePastingHandler(_valueTextBox, OnValueTextBoxPaste); } else { _valueTextBox.LostFocus += OnTextBoxLostFocus; _valueTextBox.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown; DataObject.AddPastingHandler(_valueTextBox, OnValueTextBoxPaste); } } private void InternalSetText(double newValue) { var text = newValue.ToString(GetPrecisionFormat()); if (_valueTextBox != null && !Equals(text, _valueTextBox.Text)) _valueTextBox.Text = text; } private string GetPrecisionFormat() { return Precision.HasValue == false ? "g" : (Precision.Value == 0 ? "#0" : ("#0.0" + string.Join("", Enumerable.Repeat("#", Precision.Value - 1)))); } private void CoerceValue(DependencyProperty dp, object localValue) { SetCurrentValue(dp, localValue); CoerceValue(dp); } private double CorrectPrecision(int? precision, double originValue) { return Math.Round(originValue, precision ?? 0, MidpointRounding.AwayFromZero); } private void ContinueChangeValue(bool isIncrease, bool isContinue = true) { if (IsReadOnly || !IsEnabled) return; if (isIncrease && DoubleUtil.LessThan(Value, Maximum)) { if (!_isBusy && isContinue) { _isBusy = true; if (DisabledValueChangedWhileBusy) _lastOldValue = Value; } _isManual = true; Value = (double)CoerceValue(this, Value + CalculateInterval(isContinue)); } if (!isIncrease && DoubleUtil.GreaterThan(Value, Minimum)) { if (!_isBusy && isContinue) { _isBusy = true; if (DisabledValueChangedWhileBusy) _lastOldValue = Value; } _isManual = true; Value = (double)CoerceValue(this, Value - CalculateInterval(isContinue)); } } private double CalculateInterval(bool isContinue = true) { if (!Speedup || !isContinue) return Interval; if (DoubleUtil.GreaterThan((_intervalValueSinceReset += _internalLargeChange), _internalLargeChange * 100)) _internalLargeChange *= 10; return _internalLargeChange; } private void ResetInternal() { _internalLargeChange = Interval; _intervalValueSinceReset = 0; _isBusy = false; if (_lastOldValue.HasValue) { _isManual = true; OnValueChanged(_lastOldValue.Value, Value); _lastOldValue = null; } } private void InvalidateRequerySuggested(double value) { if (_repeatUp == null || _repeatDown == null) return; if (DoubleUtil.AreClose(value, Maximum) && _repeatUp.IsEnabled || DoubleUtil.AreClose(value, Minimum) && _repeatDown.IsEnabled) CommandManager.InvalidateRequerySuggested(); else { if (!_repeatUp.IsEnabled || !_repeatDown.IsEnabled) CommandManager.InvalidateRequerySuggested(); } } #endregion } }