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中引用该控件

六、最终效果

————————————————————

代码地址:https://github.com/cmfGit/IpAddressControl.git

posted @ 2019-06-02 13:20  一叶知秋,知寒冬  阅读(5374)  评论(3编辑  收藏  举报