WPF IP地址输入控件的实现
一、前言
WPF没有内置IP地址输入控件,因此我们需要通过自己定义实现。
我们先看一下IP地址输入控件有什么特性:
- 输满三个数字焦点会往右移
- 键盘←→可以空光标移动
- 任意位置可复制整段IP地址,且支持x.x.x.x格式的粘贴赋值
- 删除字符会自动向左移动焦点
知道以上特性,我们就可以开始动手了。
二、构成
Grid+TextBox*4+TextBlock*3
通过这几个控件的组合,我们完成IP地址输入控件的功能。
界面代码如下:
1 <UserControl 2 x:Class="IpAddressControl.IpAddressControl" 3 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 4 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 6 xmlns:local="clr-namespace:IpAddressControl" 7 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 8 Margin="10,0" 9 d:DesignHeight="50" 10 d:DesignWidth="800" 11 mc:Ignorable="d" Background="White"> 12 <UserControl.Resources> 13 <ControlTemplate x:Key="validationTemplate"> 14 <DockPanel> 15 <TextBlock 16 Margin="1,2" 17 DockPanel.Dock="Right" 18 FontSize="{DynamicResource ResourceKey=Heading4}" 19 FontWeight="Bold" 20 Foreground="Red" 21 Text="" /> 22 <AdornedElementPlaceholder /> 23 </DockPanel> 24 </ControlTemplate> 25 <Style x:Key="CustomTextBoxTextStyle" TargetType="TextBox"> 26 <Setter Property="MaxLength" Value="3" /> 27 <Setter Property="HorizontalAlignment" Value="Stretch" /> 28 <Setter Property="VerticalAlignment" Value="Center" /> 29 <Style.Triggers> 30 <Trigger Property="Validation.HasError" Value="True"> 31 <Trigger.Setters> 32 <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" /> 33 <Setter Property="BorderBrush" Value="Red" /> 34 <Setter Property="Background" Value="Red" /> 35 </Trigger.Setters> 36 </Trigger> 37 </Style.Triggers> 38 </Style> 39 </UserControl.Resources> 40 <Grid> 41 <Grid.ColumnDefinitions> 42 <ColumnDefinition MinWidth="30" /> 43 <ColumnDefinition Width="10" /> 44 <ColumnDefinition MinWidth="30" /> 45 <ColumnDefinition Width="10" /> 46 <ColumnDefinition MinWidth="30" /> 47 <ColumnDefinition Width="10" /> 48 <ColumnDefinition MinWidth="30" /> 49 </Grid.ColumnDefinitions> 50 51 <!-- Part 1 --> 52 <TextBox 53 Grid.Column="0" 54 BorderThickness="0" 55 HorizontalAlignment="Stretch" 56 VerticalAlignment="Stretch" 57 VerticalContentAlignment="Center" 58 HorizontalContentAlignment="Center" 59 x:Name="part1" 60 PreviewKeyDown="Part1_PreviewKeyDown" 61 local:FocusChangeExtension.IsFocused="{Binding IsPart1Focused, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}" 62 Style="{StaticResource CustomTextBoxTextStyle}" 63 Validation.ErrorTemplate="{StaticResource validationTemplate}"> 64 <TextBox.Text> 65 <Binding Path="Part1" UpdateSourceTrigger="PropertyChanged"> 66 <Binding.ValidationRules> 67 <local:IPRangeValidationRule Max="255" Min="0" /> 68 </Binding.ValidationRules> 69 </Binding> 70 </TextBox.Text> 71 </TextBox> 72 <TextBlock 73 Grid.Column="1" 74 HorizontalAlignment="Center" 75 FontSize="15" 76 Text="." 77 VerticalAlignment="Center" 78 /> 79 80 <!-- Part 2 --> 81 <TextBox 82 Grid.Column="2" 83 x:Name="part2" 84 BorderThickness="0" 85 VerticalAlignment="Stretch" 86 VerticalContentAlignment="Center" 87 HorizontalContentAlignment="Center" 88 PreviewKeyDown="Part2_KeyDown" 89 local:FocusChangeExtension.IsFocused="{Binding IsPart2Focused}" 90 Style="{StaticResource CustomTextBoxTextStyle}" 91 Validation.ErrorTemplate="{StaticResource validationTemplate}"> 92 <TextBox.Text> 93 <Binding Path="Part2" UpdateSourceTrigger="PropertyChanged"> 94 <Binding.ValidationRules> 95 <local:IPRangeValidationRule Max="255" Min="0" /> 96 </Binding.ValidationRules> 97 </Binding> 98 </TextBox.Text> 99 </TextBox> 100 <TextBlock 101 Grid.Column="3" 102 HorizontalAlignment="Center" 103 FontSize="15" 104 Text="." 105 VerticalAlignment="Center"/> 106 107 <!-- Part 3 --> 108 <TextBox 109 Grid.Column="4" 110 x:Name="part3" 111 BorderThickness="0" 112 VerticalAlignment="Stretch" 113 VerticalContentAlignment="Center" 114 HorizontalContentAlignment="Center" 115 PreviewKeyDown="Part3_KeyDown" 116 local:FocusChangeExtension.IsFocused="{Binding IsPart3Focused}" 117 Style="{StaticResource CustomTextBoxTextStyle}" 118 Validation.ErrorTemplate="{StaticResource validationTemplate}"> 119 <TextBox.Text> 120 <Binding Path="Part3" UpdateSourceTrigger="PropertyChanged"> 121 <Binding.ValidationRules> 122 <local:IPRangeValidationRule Max="255" Min="0" /> 123 </Binding.ValidationRules> 124 </Binding> 125 </TextBox.Text> 126 </TextBox> 127 <TextBlock 128 Grid.Column="5" 129 HorizontalAlignment="Center" 130 FontSize="15" 131 Text="." 132 VerticalAlignment="Center"/> 133 134 <!-- Part 4 --> 135 <TextBox 136 Grid.Column="6" 137 x:Name="part4" 138 BorderThickness="0" 139 VerticalAlignment="Stretch" 140 VerticalContentAlignment="Center" 141 HorizontalContentAlignment="Center" 142 PreviewKeyDown="Part4_KeyDown" 143 local:FocusChangeExtension.IsFocused="{Binding IsPart4Focused}" 144 Style="{StaticResource CustomTextBoxTextStyle}" 145 Validation.ErrorTemplate="{StaticResource validationTemplate}"> 146 <TextBox.Text> 147 <Binding Path="Part4" UpdateSourceTrigger="PropertyChanged"> 148 <Binding.ValidationRules> 149 <local:IPRangeValidationRule Max="255" Min="0" /> 150 </Binding.ValidationRules> 151 </Binding> 152 </TextBox.Text> 153 </TextBox> 154 </Grid> 155 </UserControl>
三、验证输入格式
界面中为TextBox添加了CustomTextBoxTextStyle及validationTemplate样式,当输入格式不正确时,控件就会应用该样式。
通过自定义规则IPRangeValidationRule来验证输入的内容格式是否要求。
自定义规则代码如下:
1 public class IPRangeValidationRule : ValidationRule 2 { 3 private int _min; 4 private int _max; 5 6 public int Min 7 { 8 get { return _min; } 9 set { _min = value; } 10 } 11 12 public int Max 13 { 14 get { return _max; } 15 set { _max = value; } 16 } 17 18 public override ValidationResult Validate(object value, CultureInfo cultureInfo) 19 { 20 int val = 0; 21 var strVal = (string)value; 22 try 23 { 24 if (strVal.Length > 0) 25 { 26 if (strVal.EndsWith(".")) 27 { 28 return CheckRanges(strVal.Replace(".", "")); 29 } 30 31 // Allow dot character to move to next box 32 return CheckRanges(strVal); 33 } 34 } 35 catch (Exception e) 36 { 37 return new ValidationResult(false, "Illegal characters or " + e.Message); 38 } 39 40 if ((val < Min) || (val > Max)) 41 { 42 return new ValidationResult(false, 43 "Please enter the value in the range: " + Min + " - " + Max + "."); 44 } 45 else 46 { 47 return ValidationResult.ValidResult; 48 } 49 } 50 51 private ValidationResult CheckRanges(string strVal) 52 { 53 if (int.TryParse(strVal, out var res)) 54 { 55 if ((res < Min) || (res > Max)) 56 { 57 return new ValidationResult(false, 58 "Please enter the value in the range: " + Min + " - " + Max + "."); 59 } 60 else 61 { 62 return ValidationResult.ValidResult; 63 } 64 } 65 else 66 { 67 return new ValidationResult(false, "Illegal characters entered"); 68 } 69 } 70 }
四、控制焦点变化
在界面代码中我通过local:FocusChangeExtension.IsFocused附加属性实现绑定属性控制焦点的变化。
附加属性的代码如下:
1 public static class FocusChangeExtension 2 { 3 public static bool GetIsFocused(DependencyObject obj) 4 { 5 return (bool)obj.GetValue(IsFocusedProperty); 6 } 7 8 public static void SetIsFocused(DependencyObject obj, bool value) 9 { 10 obj.SetValue(IsFocusedProperty, value); 11 } 12 13 public static readonly DependencyProperty IsFocusedProperty = 14 DependencyProperty.RegisterAttached( 15 "IsFocused", typeof(bool), typeof(FocusChangeExtension), 16 new UIPropertyMetadata(false, OnIsFocusedPropertyChanged)); 17 18 private static void OnIsFocusedPropertyChanged( 19 DependencyObject d, 20 DependencyPropertyChangedEventArgs e) 21 { 22 var control = (UIElement)d; 23 if ((bool)e.NewValue) 24 { 25 control.Focus(); 26 } 27 } 28 }
五、VM+后台代码混合实现焦点控制及内容复制粘贴
1、后台代码主要实现复制粘贴内容,另外←→移动光标也需要后台代码控制。通过PreviewKeyDown事件捕获键盘左移右移,复制,删除等事件,做出相应处理:
1 private void Part2_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) 2 { 3 if (e.Key == Key.Back && part2.Text == "") 4 { 5 part1.Focus(); 6 } 7 if (e.Key == Key.Right && part2.CaretIndex == part2.Text.Length) 8 { 9 part3.Focus(); 10 e.Handled = true; 11 } 12 if (e.Key == Key.Left && part2.CaretIndex == 0) 13 { 14 part1.Focus(); 15 e.Handled = true; 16 } 17 18 if (e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.C) 19 { 20 if (part2.SelectionLength == 0) 21 { 22 var vm = this.DataContext as IpAddressViewModel; 23 Clipboard.SetText(vm.AddressText); 24 } 25 } 26 }
通过DataObject.AddPastingHandler(part1, TextBox_Pasting)添加粘贴事件。使控件赋值。
2、通过ViewModel方式实现属性绑定通知,来控制焦点变化及内容赋值。
ViewModel类要实现绑定通知需要实现INotifyPropertyChanged接口中的方法。
我们新建一个IpAddressViewModel类继承INotifyPropertyChanged,代码如下:
1 public class IpAddressViewModel : INotifyPropertyChanged 2 { 3 public event EventHandler AddressChanged; 4 5 public string AddressText 6 { 7 get { return $"{Part1??"0"}.{Part2??"0"}.{Part3??"0"}.{Part4??"0"}"; } 8 } 9 10 private bool isPart1Focused; 11 12 public bool IsPart1Focused 13 { 14 get { return isPart1Focused; } 15 set { isPart1Focused = value; OnPropertyChanged(); } 16 } 17 18 private string part1; 19 20 public string Part1 21 { 22 get { return part1; } 23 set 24 { 25 part1 = value; 26 SetFocus(true, false, false, false); 27 28 var moveNext = CanMoveNext(ref part1); 29 30 OnPropertyChanged(); 31 OnPropertyChanged(nameof(AddressText)); 32 AddressChanged?.Invoke(this, EventArgs.Empty); 33 34 if (moveNext) 35 { 36 SetFocus(false, true, false, false); 37 } 38 } 39 } 40 41 private bool isPart2Focused; 42 43 public bool IsPart2Focused 44 { 45 get { return isPart2Focused; } 46 set { isPart2Focused = value; OnPropertyChanged(); } 47 } 48 49 50 private string part2; 51 52 public string Part2 53 { 54 get { return part2; } 55 set 56 { 57 part2 = value; 58 SetFocus(false, true, false, false); 59 60 var moveNext = CanMoveNext(ref part2); 61 62 OnPropertyChanged(); 63 OnPropertyChanged(nameof(AddressText)); 64 AddressChanged?.Invoke(this, EventArgs.Empty); 65 66 if (moveNext) 67 { 68 SetFocus(false, false, true, false); 69 } 70 } 71 } 72 73 private bool isPart3Focused; 74 75 public bool IsPart3Focused 76 { 77 get { return isPart3Focused; } 78 set { isPart3Focused = value; OnPropertyChanged(); } 79 } 80 81 private string part3; 82 83 public string Part3 84 { 85 get { return part3; } 86 set 87 { 88 part3 = value; 89 SetFocus(false, false, true, false); 90 var moveNext = CanMoveNext(ref part3); 91 92 OnPropertyChanged(); 93 OnPropertyChanged(nameof(AddressText)); 94 AddressChanged?.Invoke(this, EventArgs.Empty); 95 96 if (moveNext) 97 { 98 SetFocus(false, false, false, true); 99 } 100 } 101 } 102 103 private bool isPart4Focused; 104 105 public bool IsPart4Focused 106 { 107 get { return isPart4Focused; } 108 set { isPart4Focused = value; OnPropertyChanged(); } 109 } 110 111 private string part4; 112 113 public string Part4 114 { 115 get { return part4; } 116 set 117 { 118 part4 = value; 119 SetFocus(false, false, false, true); 120 var moveNext = CanMoveNext(ref part4); 121 122 OnPropertyChanged(); 123 OnPropertyChanged(nameof(AddressText)); 124 AddressChanged?.Invoke(this, EventArgs.Empty); 125 126 } 127 } 128 129 public void SetAddress(string address) 130 { 131 if (string.IsNullOrWhiteSpace(address)) 132 return; 133 134 var parts = address.Split('.'); 135 136 if (int.TryParse(parts[0], out var num0)) 137 { 138 Part1 = num0.ToString(); 139 } 140 141 if (int.TryParse(parts[1], out var num1)) 142 { 143 Part2 = parts[1]; 144 } 145 146 if (int.TryParse(parts[2], out var num2)) 147 { 148 Part3 = parts[2]; 149 } 150 151 if (int.TryParse(parts[3], out var num3)) 152 { 153 Part4 = parts[3]; 154 } 155 156 } 157 158 private bool CanMoveNext(ref string part) 159 { 160 bool moveNext = false; 161 162 if (!string.IsNullOrWhiteSpace(part)) 163 { 164 if (part.Length >= 3) 165 { 166 moveNext = true; 167 } 168 169 if (part.EndsWith(".")) 170 { 171 moveNext = true; 172 part = part.Replace(".", ""); 173 } 174 } 175 176 return moveNext; 177 } 178 179 private void SetFocus(bool part1, bool part2, bool part3, bool part4) 180 { 181 IsPart1Focused = part1; 182 IsPart2Focused = part2; 183 IsPart3Focused = part3; 184 IsPart4Focused = part4; 185 } 186 187 public event PropertyChangedEventHandler PropertyChanged; 188 189 190 protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 191 { 192 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 193 } 194 }
到这里基本就完成了,生成控件然后到MainWindow中引用该控件
六、最终效果
————————————————————