一、前言
WPF没有内置IP地址输入控件,因此我们需要通过自己定义实现。
我们先看一下IP地址输入控件有什么特性:
- 输满三个数字焦点会往右移
- 键盘←→可以空光标移动
- 任意位置可复制整段IP地址,且支持x.x.x.x格式的粘贴赋值
- 删除字符会自动向左移动焦点
知道以上特性,我们就可以开始动手了。
二、构成
Grid+TextBox*4+TextBlock*3
通过这几个控件的组合,我们完成IP地址输入控件的功能。
界面代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具