diff --git a/src/Wpf.Ui.Gallery/ViewModels/Pages/BasicInput/NumericUpDownViewModel.cs b/src/Wpf.Ui.Gallery/ViewModels/Pages/BasicInput/NumericUpDownViewModel.cs new file mode 100644 index 000000000..e4fc7a882 --- /dev/null +++ b/src/Wpf.Ui.Gallery/ViewModels/Pages/BasicInput/NumericUpDownViewModel.cs @@ -0,0 +1,10 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +namespace Wpf.Ui.Gallery.ViewModels.Pages.BasicInput; + +public class NumericUpDownViewModel : ObservableObject +{ +} diff --git a/src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs b/src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs index 037a63721..b564fc2aa 100644 --- a/src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs +++ b/src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs @@ -58,6 +58,7 @@ public partial class MainWindowViewModel(IStringLocalizer localize new NavigationViewItem(nameof(ToggleSwitch), typeof(ToggleSwitchPage)), new NavigationViewItem(nameof(CheckBox), typeof(CheckBoxPage)), new NavigationViewItem(nameof(ComboBox), typeof(ComboBoxPage)), + new NavigationViewItem(nameof(NumericUpDown), typeof(NumericUpDownPage)), new NavigationViewItem(nameof(RadioButton), typeof(RadioButtonPage)), new NavigationViewItem(nameof(RatingControl), typeof(RatingPage)), new NavigationViewItem(nameof(ThumbRate), typeof(ThumbRatePage)), diff --git a/src/Wpf.Ui.Gallery/Views/Pages/BasicInput/NumericUpDownPage.xaml b/src/Wpf.Ui.Gallery/Views/Pages/BasicInput/NumericUpDownPage.xaml new file mode 100644 index 000000000..247b2b809 --- /dev/null +++ b/src/Wpf.Ui.Gallery/Views/Pages/BasicInput/NumericUpDownPage.xaml @@ -0,0 +1,67 @@ + + + + + + <ui:NumericUpDown + BorderThickness="1" + ButtonAlignment="Vertical" + ButtonWidth="22" + CornerRadius="4" + Decimals="0" + Format="N0" + IsEnabled="True" + IsReadOnly="False" + MaxValue="1000000" + MinValue="0" + Step="1" + Wrap="True" + Value="999999" /> + + + + + + + + + + + + + + diff --git a/src/Wpf.Ui.Gallery/Views/Pages/BasicInput/NumericUpDownPage.xaml.cs b/src/Wpf.Ui.Gallery/Views/Pages/BasicInput/NumericUpDownPage.xaml.cs new file mode 100644 index 000000000..3ecf28786 --- /dev/null +++ b/src/Wpf.Ui.Gallery/Views/Pages/BasicInput/NumericUpDownPage.xaml.cs @@ -0,0 +1,24 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using Wpf.Ui.Controls; +using Wpf.Ui.Gallery.ControlsLookup; +using Wpf.Ui.Gallery.ViewModels.Pages.BasicInput; + +namespace Wpf.Ui.Gallery.Views.Pages.BasicInput; + +[GalleryPage("NumericUpDown control", SymbolRegular.NumberSymbol16)] +public partial class NumericUpDownPage : INavigableView +{ + public NumericUpDownViewModel ViewModel { get; } + + public NumericUpDownPage(NumericUpDownViewModel viewModel) + { + ViewModel = viewModel; + DataContext = this; + + InitializeComponent(); + } +} diff --git a/src/Wpf.Ui/Controls/NumericUpDown/NumericUpDown.cs b/src/Wpf.Ui/Controls/NumericUpDown/NumericUpDown.cs new file mode 100644 index 000000000..db9a1b8ec --- /dev/null +++ b/src/Wpf.Ui/Controls/NumericUpDown/NumericUpDown.cs @@ -0,0 +1,432 @@ +using System.Windows.Input; +using RepeatButton = System.Windows.Controls.Primitives.RepeatButton; + +namespace Wpf.Ui.Controls; + +/// +/// A control that allows the user to select from a range of values by clicking the up/down buttons or by typing the value. +/// +/// +/// +/// <ui:NumericUpDown +/// BorderThickness="1" +/// ButtonAlignment = " Vertical" +/// ButtonWidth = "22" +/// CornerRadius = "4" +/// Decimals = "0" +/// Format = " N0" +/// IsEnabled = " True" +/// IsReadOnly = " False" +/// MaxValue = "1000000" +/// MinValue = "0" +/// Step = "1" +/// Wrap = " True" +/// Value = "999999" /> +/// +/// +[TemplatePart(Name = "PART_UpButton", Type = typeof(RepeatButton))] +[TemplatePart(Name = "PART_DownButton", Type = typeof(RepeatButton))] +[TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))] +public class NumericUpDown : System.Windows.Controls.Control +{ + static NumericUpDown() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown), new FrameworkPropertyMetadata(typeof(NumericUpDown))); + + BorderThicknessProperty.OverrideMetadata(typeof(NumericUpDown), new FrameworkPropertyMetadata(new Thickness(1), OnBorderThicknessChanged)); + } + + public NumericUpDown() + { + Loaded += (s, e) => + { + UpdateDisplayValue(); + UpdateTopButtonCornerRadius(); + UpdateBottomButtonCornerRadius(); + }; + } + + private static void OnBorderThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NumericUpDown self) + { + self.UpdateTopButtonCornerRadius(); + self.UpdateBottomButtonCornerRadius(); + } + } + + /// + /// Gets or sets the value of the control. + /// + public double Value + { + get => (double)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(double), typeof(NumericUpDown), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged, CoerceValue)); + + private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NumericUpDown self) + { + self.OnValueChanged(e); + } + } + + protected virtual void OnValueChanged(DependencyPropertyChangedEventArgs e) + { + UpdateDisplayValue(); + } + + private static object CoerceValue(DependencyObject d, object baseValue) + { + if (d is NumericUpDown self && baseValue is double doubleval) + { + return self.CoerceMyValue(doubleval); + } + + return 0.0; + } + + public string DisplayValue + { + get => (string)GetValue(DisplayValueProperty); + } + + /// Identifies the dependency property. + protected static readonly DependencyPropertyKey DisplayValuePropertyKey = DependencyProperty.RegisterReadOnly(nameof(DisplayValue), typeof(string), typeof(NumericUpDown), new FrameworkPropertyMetadata(default(string))); + + /// Identifies the dependency property. + public static readonly DependencyProperty DisplayValueProperty = DisplayValuePropertyKey.DependencyProperty; + + public double Step + { + get => (double)GetValue(StepProperty); + set => SetValue(StepProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty StepProperty = DependencyProperty.Register(nameof(Step), typeof(double), typeof(NumericUpDown), new PropertyMetadata(0.1d)); + + public int Decimals + { + get => (int)GetValue(DecimalsProperty); + set => SetValue(DecimalsProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty DecimalsProperty = DependencyProperty.Register(nameof(Decimals), typeof(int), typeof(NumericUpDown), new PropertyMetadata(2, OnDecimalsChanged)); + + private static void OnDecimalsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NumericUpDown self) + { + self.OnDecimalsChanged(e); + } + } + + protected virtual void OnDecimalsChanged(DependencyPropertyChangedEventArgs e) + { + UpdateDisplayValue(); + } + + public double MaxValue + { + get => (double)GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register(nameof(MaxValue), typeof(double), typeof(NumericUpDown), new PropertyMetadata(100.0, OnMaxValueChanged)); + + private static void OnMaxValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NumericUpDown self) + { + self.OnMaxValueChanged(e); + } + } + + protected virtual void OnMaxValueChanged(DependencyPropertyChangedEventArgs e) + { + if (MinValue > MaxValue) + { + SetCurrentValue(MinValueProperty, MaxValue); + } + + if (Value > MaxValue) + { + SetCurrentValue(ValueProperty, MaxValue); + } + } + + public double MinValue + { + get => (double)GetValue(MinValueProperty); + set => SetValue(MinValueProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register(nameof(MinValue), typeof(double), typeof(NumericUpDown), new PropertyMetadata(0.0, OnMinValueChanged)); + + private static void OnMinValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NumericUpDown self) + { + self.OnMinValueChanged(e); + } + } + + protected virtual void OnMinValueChanged(DependencyPropertyChangedEventArgs e) + { + if (MaxValue < MinValue) + { + SetCurrentValue(MaxValueProperty, MinValue); + } + + if (Value < MinValue) + { + SetCurrentValue(ValueProperty, MinValue); + } + } + + /// + /// gets or sets the corner radius of the control. + /// + public CornerRadius CornerRadius + { + get => (CornerRadius)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(NumericUpDown), new FrameworkPropertyMetadata(new CornerRadius(4), OnCornerRadiusChanged)); + + private static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NumericUpDown self) + { + self.UpdateTopButtonCornerRadius(); + self.UpdateBottomButtonCornerRadius(); + } + } + + internal CornerRadius TopButtonCornerRadius + { + get => (CornerRadius)GetValue(TopButtonCornerRadiusProperty); + } + + private static readonly DependencyPropertyKey TopButtonCornerRadiusPropertyKey = DependencyProperty.RegisterReadOnly(nameof(TopButtonCornerRadius), typeof(CornerRadius), typeof(NumericUpDown), new FrameworkPropertyMetadata(default(CornerRadius))); + + /// Identifies the dependency property. + internal static readonly DependencyProperty TopButtonCornerRadiusProperty = TopButtonCornerRadiusPropertyKey.DependencyProperty; + + protected void UpdateTopButtonCornerRadius() + { + double topButtonCornerRadiusTopRight = Math.Max(0, CornerRadius.TopRight - (0.5 * BorderThickness.Right)); + CornerRadius topButtonCornerRadius = ButtonAlignment == NumericUpDownButtonAlignment.Vertical + ? new(0, topButtonCornerRadiusTopRight, 0, 0) + : new(0, 0, 0, 0); + SetValue(TopButtonCornerRadiusPropertyKey, topButtonCornerRadius); + } + + internal CornerRadius BottomButtonCornerRadius + { + get => (CornerRadius)GetValue(BottomButtonCornerRadiusProperty); + } + + private static readonly DependencyPropertyKey BottomButtonCornerRadiusPropertyKey = DependencyProperty.RegisterReadOnly(nameof(BottomButtonCornerRadius), typeof(CornerRadius), typeof(NumericUpDown), new FrameworkPropertyMetadata(default(CornerRadius))); + + /// Identifies the dependency property. + internal static readonly DependencyProperty BottomButtonCornerRadiusProperty = BottomButtonCornerRadiusPropertyKey.DependencyProperty; + + protected void UpdateBottomButtonCornerRadius() + { + double bottomButtonCornerRadiusBottomRight = Math.Max(0, CornerRadius.TopRight - (0.5 * BorderThickness.Right)); + CornerRadius bottomButtonCornerRadius = ButtonAlignment == NumericUpDownButtonAlignment.Vertical + ? new(0, 0, bottomButtonCornerRadiusBottomRight, 0) + : new(0, bottomButtonCornerRadiusBottomRight, bottomButtonCornerRadiusBottomRight, 0); + SetValue(BottomButtonCornerRadiusPropertyKey, bottomButtonCornerRadius); + } + + /// + /// Gets or sets the width of the up and down buttons. + /// + public double ButtonWidth + { + get => (double)GetValue(ButtonWidthProperty); + set => SetValue(ButtonWidthProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty ButtonWidthProperty = DependencyProperty.Register(nameof(ButtonWidth), typeof(double), typeof(NumericUpDown), new FrameworkPropertyMetadata(22.0)); + + /// + /// gets or sets the alignment of the buttons. + /// + public NumericUpDownButtonAlignment ButtonAlignment + { + get => (NumericUpDownButtonAlignment)GetValue(ButtonAlignmentProperty); + set => SetValue(ButtonAlignmentProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty ButtonAlignmentProperty = DependencyProperty.Register(nameof(ButtonAlignment), typeof(NumericUpDownButtonAlignment), typeof(NumericUpDown), new FrameworkPropertyMetadata(default(NumericUpDownButtonAlignment), OnButtonAlignmentChanged)); + + private static void OnButtonAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NumericUpDown self) + { + self.UpdateTopButtonCornerRadius(); + self.UpdateBottomButtonCornerRadius(); + } + } + + /// + /// Gets or sets a value indicating whether the control automatically wraps the value to the Minimum or Maximum when the value exceeds the range. + /// + public bool Wrap + { + get => (bool)GetValue(WrapProperty); + set => SetValue(WrapProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty WrapProperty = DependencyProperty.Register(nameof(Wrap), typeof(bool), typeof(NumericUpDown), new FrameworkPropertyMetadata(false)); + + /// + /// gets or sets a value indicating whether the control is read-only. + /// + public bool IsReadOnly + { + get => (bool)GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(NumericUpDown), new FrameworkPropertyMetadata(false)); + + /// + /// gets or sets the format of the display value. + /// + /// + /// specifying a format will preclude use of the property. + /// + public string Format + { + get => (string)GetValue(FormatProperty); + set => SetValue(FormatProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty FormatProperty = DependencyProperty.Register(nameof(Format), typeof(string), typeof(NumericUpDown), new FrameworkPropertyMetadata(string.Empty)); + + private double CoerceMyValue(double val) + { + double clampedValue = Math.Max(MinValue, Math.Min(MaxValue, val)); + /*double roundedValue = Math.Round(clampedValue, Decimals);*/ + + return clampedValue; + } + + protected virtual void UpdateDisplayValue() + { + CultureInfo culture = CultureInfo.CurrentCulture; + string format = string.IsNullOrEmpty(Format) + ? "F" + Decimals + : Format; + double roundedValue = Math.Round(Value, Decimals); + string displayValue = roundedValue.ToString(format, culture); + SetValue(DisplayValuePropertyKey, displayValue); + } + + private string _userInput = string.Empty; + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if (GetTemplateChild("PART_DownButton") is NumericUpDownButton downButton) + { + downButton.Click += (sender, e) => + { + double newValue = Value - Step; + newValue = Wrap && newValue < MinValue ? MaxValue : newValue; + SetCurrentValue(ValueProperty, newValue); + }; + } + + if (GetTemplateChild("PART_UpButton") is NumericUpDownButton upButton) + { + upButton.Click += (sender, e) => + { + double newValue = Value + Step; + newValue = Wrap && newValue > MaxValue ? MinValue : newValue; + SetCurrentValue(ValueProperty, newValue); + }; + } + + if (GetTemplateChild("PART_TextBox") is System.Windows.Controls.TextBox textBox) + { + textBox.GotFocus += (sender, e) => + { + if (!IsReadOnly) + { + SetValue(DisplayValuePropertyKey, string.Empty); + this._userInput = string.Empty; + } + }; + + textBox.TextChanged += (sender, e) => + { + this._userInput = textBox.Text; + }; + + textBox.KeyDown += (sender, e) => + { + if (e.Key == Key.Enter && !IsReadOnly) + { + ProcessUserInput(); + _ = Focus(); + } + }; + + textBox.LostFocus += (sender, e) => + { + if (!IsReadOnly) + { + ProcessUserInput(); + } + }; + } + } + + private void ProcessUserInput() + { + if (double.TryParse(this._userInput, NumberStyles.Any, CultureInfo.CurrentCulture, out double parsedVal)) + { + if (parsedVal != Value) + { + SetCurrentValue(ValueProperty, parsedVal); + } + else + { + ForceUpdateDisplayValue(); + } + } + else + { + ForceUpdateDisplayValue(); + } + + this._userInput = string.Empty; + } + + private void ForceUpdateDisplayValue() + { + SetValue(DisplayValuePropertyKey, string.Empty); + UpdateDisplayValue(); + } +} \ No newline at end of file diff --git a/src/Wpf.Ui/Controls/NumericUpDown/NumericUpDown.xaml b/src/Wpf.Ui/Controls/NumericUpDown/NumericUpDown.xaml new file mode 100644 index 000000000..fbaf898cf --- /dev/null +++ b/src/Wpf.Ui/Controls/NumericUpDown/NumericUpDown.xaml @@ -0,0 +1,161 @@ + + + + + + + + +