WPF数字输入框和IP地址输入框
数字输入框
简介
在业务中,我们经常需要限制用户的输入,比如限制输入长度,限制只能输入数字等等。限制输入长度WPF内置的TextBox已经帮我们解决了,但是限制输入数字却并未在WPF中内置解决方案。使用第三方的控件又要多增加一个引用,于是决定自己写一个。
在写的过程中发现需要考虑的问题比较多,比如限制输入法、部分限制输入小数点和负号、限制输入字母和其它符号、粘贴时做特殊处理等等。值得一提的是,将文本绑定到Double型且将UpdateSourceTrigger设为PropertyChanged时,出现了界面上包含小数点,但是通过Text获取的文本却并不包含小数点的情况,猜测原因是因为绑定更新和自动类型转换出现的问题。
数字输入框提供了设置最小值(可用)、最大值(可用)、精度(小数点后位数)的功能。不合法的按键将会被过滤掉,输入的值小于最小值或者大于最大值时,其输入都将视为无效。
代码
代码较简单,且有注释,不再多说。
namespace YiYan127.WPF.Controls { using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Input; /// <summary> /// 输入数值的文本框 /// </summary> public class NumbericTextBox : TextBox { #region Fields #region DependencyProperty /// <summary> /// 最大值的依赖属性 /// </summary> public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register( "MaxValue", typeof(double), typeof(NumbericTextBox), new PropertyMetadata(double.MaxValue)); /// <summary> /// 最小值的依赖属性 /// </summary> public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register( "MinValue", typeof(double), typeof(NumbericTextBox), new PropertyMetadata(double.MinValue)); /// <summary> /// 精度的依赖属性 /// </summary> public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register( "Precision", typeof(ushort), typeof(NumbericTextBox), new PropertyMetadata((ushort)2)); #endregion DependencyProperty /// <summary> /// 先前合法的文本 /// </summary> private string lastLegalText; /// <summary> /// 是否为粘贴 /// </summary> private bool isPaste; public event EventHandler<TextChangedEventArgs> PreviewTextChanged; #endregion Fields #region Constructor /// <summary> /// 构造函数 /// </summary> public NumbericTextBox() { this.PreviewTextInput += this.NumbericTextBoxPreviewTextInput; this.TextChanged += this.NumbericTextBoxTextChanged; this.PreviewKeyDown += this.NumbericTextBox_PreviewKeyDown; this.LostFocus += this.NumbericTextBoxLostFocus; InputMethod.SetIsInputMethodEnabled(this, false); this.Loaded += this.NumbericTextBoxLoaded; } #endregion Constructor #region Properties /// <summary> /// 最大值,可取 /// </summary> public double MaxValue { get { return (double)this.GetValue(MaxValueProperty); } set { this.SetValue(MaxValueProperty, value); } } /// <summary> /// 最小值,可取 /// </summary> public double MinValue { get { return (double)this.GetValue(MinValueProperty); } set { this.SetValue(MinValueProperty, value); } } /// <summary> /// 精度,即精确到小数点后的位数 /// </summary> public ushort Precision { get { return (ushort)this.GetValue(PrecisionProperty); } set { this.SetValue(PrecisionProperty, value); } } #endregion Properties protected virtual void OnPreviewTextChanged(TextChangedEventArgs e) { if (this.PreviewTextChanged != null) { this.PreviewTextChanged(this, e); } } #region Private Methods /// <summary> /// 处理粘贴的情况 /// </summary> protected virtual void HandlePaste() { this.isPaste = false; // 处理符号的标志 bool handledSybmol = false; // 处理小数点的标志 bool handledDot = false; // 当前位对应的基数 double baseNumber = 1; // 转换后的数字 double number = 0; // 上一次合法的数字 double lastNumber = 0; // 小数点后的位数 double precision = 0; foreach (var c in this.Text) { if (!handledSybmol && (c == '-')) { baseNumber = -1; handledSybmol = true; } if ((c >= '0') && (c <= '9')) { int digit = c - '0'; if (!handledDot) { number = (number * baseNumber) + digit; baseNumber = 10; } else { baseNumber = baseNumber / 10; number += digit * baseNumber; } // 正负号必须位于最前面 handledSybmol = true; } if (c == '.') { // 精度已经够了 if (precision + 1 > this.Precision) { break; } handledDot = true; // 此时正负号不能起作用 handledSybmol = true; baseNumber = 0.1; precision++; } if ((number < this.MinValue) || (number > this.MaxValue)) { this.Text = lastNumber.ToString(CultureInfo.InvariantCulture); this.SelectionStart = this.Text.Length; return; } lastNumber = number; } this.Text = number.ToString(CultureInfo.InvariantCulture); this.SelectionStart = this.Text.Length; } #endregion Private Methods #region Overrides of TextBoxBase #endregion #region Events Handling private void NumbericTextBoxLoaded(object sender, RoutedEventArgs e) { if (this.MinValue > this.MaxValue) { this.MinValue = this.MaxValue; } if (string.IsNullOrEmpty(this.Text)) { double val = (this.MaxValue + this.MinValue) / 2; val = Math.Round(val, this.Precision); this.Text = val.ToString(CultureInfo.InvariantCulture); } this.isPaste = true; } /// <summary> /// The numberic text box preview text input. /// </summary> /// <param name="sender"> The sender.</param> /// <param name="e"> The e.</param> private void NumbericTextBoxPreviewTextInput(object sender, TextCompositionEventArgs e) { // 如果是粘贴不会引发该事件 this.isPaste = false; short val; // 输入非数字 if (!short.TryParse(e.Text, out val)) { // 小于0时,可输入负号 if ((this.MinValue < 0) && (e.Text == "-")) { int minusPos = this.Text.IndexOf('-'); // 未输入负号且负号在第一位 if ((minusPos == -1) && (0 == this.SelectionStart)) { return; } } // 精度大于0时,可输入小数点 if ((this.Precision > 0) && (e.Text == ".")) { // 解决UpdateSourceTrigger为PropertyChanged时输入小数点文本与界面不一致的问题 if (this.SelectionStart > this.Text.Length) { e.Handled = true; return; } // 小数点位置 int dotPos = this.Text.IndexOf('.'); // 未存在小数点可输入 if (dotPos == -1) { return; } // 已存在小数点但处于选中状态,也可输入小数点 if ((this.SelectionStart >= dotPos) && (this.SelectionLength > 0)) { return; } } e.Handled = true; } else { int dotPos = this.Text.IndexOf('.'); int cursorIndex = this.SelectionStart; // 已经存在小数点,且小数点在光标后 if ((dotPos != -1) && (dotPos < cursorIndex)) { // 不允许输入超过精度的数 if (((this.Text.Length - dotPos) > this.Precision) && (this.SelectionLength == 0)) { e.Handled = true; } } } } /// <summary> /// The numberic text box text changed. /// </summary> /// <param name="sender"> The sender.</param> /// <param name="e"> The e.</param> private void NumbericTextBoxTextChanged(object sender, TextChangedEventArgs e) { if (this.lastLegalText == this.Text) { return; } this.OnPreviewTextChanged(e); // 允许为空 if (string.IsNullOrEmpty(this.Text)) { return; } // 粘贴而来的文本 if (this.isPaste) { this.HandlePaste(); this.lastLegalText = this.Text; return; } double val; if (double.TryParse(this.Text, out val)) { // 保存光标位置 int selectIndex = this.SelectionStart; if ((val > this.MaxValue) || (val < this.MinValue)) { this.Text = this.lastLegalText; this.SelectionStart = selectIndex; return; } this.lastLegalText = this.Text; } this.isPaste = true; } /// <summary> /// The numberic text box_ preview key down. /// </summary> /// <param name="sender"> The sender.</param> /// <param name="e"> The e.</param> private void NumbericTextBox_PreviewKeyDown(object sender, KeyEventArgs e) { // 过滤空格 if (e.Key == Key.Space) { e.Handled = true; } } /// <summary> /// The numberic text box_ lost focus. /// </summary> /// <param name="sender"> The sender.</param> /// <param name="e"> The e.</param> private void NumbericTextBoxLostFocus(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(this.Text)) { this.Text = this.lastLegalText; } } #endregion Events Handling } }
IP地址输入框
IP地址输入框使用了上面的数字输入框,按点时可跳转到IP地址下一部分输入,支持IP地址的粘贴。代码较简单,且有注释,不再多说。
XAML
<UserControl x:Class="YiYan127.WPF.Controls.IpAddressControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:YiYan127.WPF.Controls"> <UniformGrid Columns="4" TextBoxBase.GotFocus="TextBox_OnGotFocus"> <DockPanel Margin="5,2"> <TextBlock VerticalAlignment="Center" DockPanel.Dock="Right" Text="." /> <controls:NumbericTextBox x:Name="IPPart1" MaxValue="255" MinValue="0" Precision="0" /> </DockPanel> <DockPanel Margin="0,2,5,2"> <TextBlock VerticalAlignment="Center" DockPanel.Dock="Right" Text="." /> <controls:NumbericTextBox x:Name="IPPart2" MaxValue="255" MinValue="0" Precision="0" /> </DockPanel> <DockPanel Margin="0,2,5,2"> <TextBlock VerticalAlignment="Center" DockPanel.Dock="Right" Text="." /> <controls:NumbericTextBox x:Name="IPPart3" MaxValue="255" MinValue="0" Precision="0" /> </DockPanel> <controls:NumbericTextBox x:Name="IPPart4" Margin="0,2,5,2" MaxValue="255" MinValue="0" Precision="0" /> </UniformGrid> </UserControl>
后台代码
namespace YiYan127.WPF.Controls { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; /// <summary> /// IP地址输入框 /// </summary> public partial class IpAddressControl { #region Fields /// <summary> /// IP地址的依赖属性 /// </summary> public static readonly DependencyProperty IPProperty = DependencyProperty.Register( "IP", typeof(string), typeof(IpAddressControl), new FrameworkPropertyMetadata(DefaultIP, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, IPChangedCallback)); /// <summary> /// IP地址的正则表达式 /// </summary> public static readonly Regex IpRegex = new Regex(@"^((2[0-4]\d|25[0-5]|(1\d{2})|([1-9]?[0-9]))\.){3}(2[0-4]\d|25[0-4]|(1\d{2})|([1-9][0-9])|([1-9]))$"); /// <summary> /// 默认IP地址 /// </summary> private const string DefaultIP = "127.0.0.1"; private static readonly Regex PartIprRegex = new Regex(@"^(\.?(2[0-4]\d|25[0-5]|(1\d{2})|([1-9]?[0-9]))\.?)+$"); /// <summary> /// 输入框的集合 /// </summary> private readonly List<NumbericTextBox> numbericTextBoxs = new List<NumbericTextBox>(); /// <summary> /// 当前活动的输入框 /// </summary> private NumbericTextBox currentNumbericTextBox; #endregion Fields #region Constructors public IpAddressControl() { InitializeComponent(); this.numbericTextBoxs.Add(this.IPPart1); this.numbericTextBoxs.Add(this.IPPart2); this.numbericTextBoxs.Add(this.IPPart3); this.numbericTextBoxs.Add(this.IPPart4); this.KeyUp += this.IpAddressControlKeyUp; this.UpdateParts(this); foreach (var numbericTextBox in this.numbericTextBoxs) { numbericTextBox.PreviewTextChanged += this.NumbericTextBox_OnPreviewTextChanged; } foreach (var numbericTextBox in this.numbericTextBoxs) { numbericTextBox.TextChanged += this.TextBoxBase_OnTextChanged; } } #endregion Constructors #region Properties public string IP { get { return (string)GetValue(IPProperty); } set { SetValue(IPProperty, value); } } #endregion Properties #region Private Methods /// <summary> /// IP值改变的响应 /// </summary> /// <param name="dependencyObject"></param> /// <param name="dependencyPropertyChangedEventArgs"></param> private static void IPChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { if (dependencyPropertyChangedEventArgs.NewValue == null) { throw new Exception("IP can not be null"); } var control = dependencyObject as IpAddressControl; if (control != null) { control.UpdateParts(control); } } private void UpdateParts(IpAddressControl control) { string[] parts = control.IP.Split(new[] { '.' }); control.IPPart1.Text = parts[0]; control.IPPart2.Text = parts[1]; control.IPPart3.Text = parts[2]; control.IPPart4.Text = parts[3]; } #endregion Private Methods #region Event Handling /// <summary> /// 按键松开的响应 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void IpAddressControlKeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.OemPeriod || e.Key == Key.Decimal) { if (this.currentNumbericTextBox != null) { int index = this.numbericTextBoxs.IndexOf(this.currentNumbericTextBox); int next = (index + 1) % this.numbericTextBoxs.Count; this.numbericTextBoxs[next].Focus(); this.numbericTextBoxs[next].SelectionStart = this.numbericTextBoxs[next].Text.Length; } } } /// <summary> /// 获得焦点的响应 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void TextBox_OnGotFocus(object sender, RoutedEventArgs e) { this.currentNumbericTextBox = e.OriginalSource as NumbericTextBox; } private void NumbericTextBox_OnPreviewTextChanged(object sender, TextChangedEventArgs e) { var numbericTextBox = sender as NumbericTextBox; Contract.Assert(numbericTextBox != null); if (PartIprRegex.IsMatch(numbericTextBox.Text)) { var ips = numbericTextBox.Text.Split('.'); if (ips.Length == 1) { return; } int index = this.numbericTextBoxs.IndexOf(numbericTextBox); int pointer2Ips = 0; for (int i = index; i < this.numbericTextBoxs.Count; i++) { while (pointer2Ips < ips.Length && string.IsNullOrEmpty(ips[pointer2Ips])) { pointer2Ips++; } if (pointer2Ips >= ips.Length) { return; } this.numbericTextBoxs[i].Text = ips[pointer2Ips]; pointer2Ips++; } } } private void TextBoxBase_OnTextChanged(object sender, TextChangedEventArgs e) { var ip = string.Format( "{0}.{1}.{2}.{3}", this.IPPart1.Text, this.IPPart2.Text, this.IPPart3.Text, this.IPPart4.Text); if (IpRegex.IsMatch(ip)) { this.IP = ip; } } #endregion Event Handling } }