一、前言
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中引用该控件
六、最终效果

————————————————————
代码地址:https://github.com/cmfGit/IpAddressControl.git
浙公网安备 33010602011771号