WPF自定义实现IP地址输入控件
作者:一叶知秋,知寒冬
一、前言
WPF没有内置IP地址输入控件,因此我们需要通过自己定义实现。
我们先看一下IP地址输入控件有什么特性:
- 输满三个数字焦点会往右移
- 键盘←→可以空光标移动
- 任意位置可复制整段IP地址,且支持x.x.x.x格式的粘贴赋值
- 删除字符会自动向左移动焦点
知道以上特性,我们就可以开始动手了。
二、构成
Grid+TextBox*4+TextBlock*3
通过这几个控件的组合,我们完成IP地址输入控件的功能。
界面代码如下:
<UserControl x:Class="IpAddressControl.IpAddressControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:IpAddressControl" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Margin="10,0" d:DesignHeight="50" d:DesignWidth="800" mc:Ignorable="d" Background="White"> <UserControl.Resources> <ControlTemplate x:Key="validationTemplate"> <DockPanel> <TextBlock Margin="1,2" DockPanel.Dock="Right" FontSize="{DynamicResource ResourceKey=Heading4}" FontWeight="Bold" Foreground="Red" Text="" /> <AdornedElementPlaceholder /> </DockPanel> </ControlTemplate> <Style x:Key="CustomTextBoxTextStyle" TargetType="TextBox"> <Setter Property="MaxLength" Value="3" /> <Setter Property="HorizontalAlignment" Value="Stretch" /> <Setter Property="VerticalAlignment" Value="Center" /> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Trigger.Setters> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" /> <Setter Property="BorderBrush" Value="Red" /> <Setter Property="Background" Value="Red" /> </Trigger.Setters> </Trigger> </Style.Triggers> </Style> </UserControl.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition MinWidth="30" /> <ColumnDefinition Width="10" /> <ColumnDefinition MinWidth="30" /> <ColumnDefinition Width="10" /> <ColumnDefinition MinWidth="30" /> <ColumnDefinition Width="10" /> <ColumnDefinition MinWidth="30" /> </Grid.ColumnDefinitions> <!-- Part 1 --> <TextBox Grid.Column="0" BorderThickness="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" x:Name="part1" PreviewKeyDown="Part1_PreviewKeyDown" local:FocusChangeExtension.IsFocused="{Binding IsPart1Focused, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}" Style="{StaticResource CustomTextBoxTextStyle}" Validation.ErrorTemplate="{StaticResource validationTemplate}"> <TextBox.Text> <Binding Path="Part1" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:IPRangeValidationRule Max="255" Min="0" /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> <TextBlock Grid.Column="1" HorizontalAlignment="Center" FontSize="15" Text="." VerticalAlignment="Center" /> <!-- Part 2 --> <TextBox Grid.Column="2" x:Name="part2" BorderThickness="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" PreviewKeyDown="Part2_KeyDown" local:FocusChangeExtension.IsFocused="{Binding IsPart2Focused}" Style="{StaticResource CustomTextBoxTextStyle}" Validation.ErrorTemplate="{StaticResource validationTemplate}"> <TextBox.Text> <Binding Path="Part2" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:IPRangeValidationRule Max="255" Min="0" /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> <TextBlock Grid.Column="3" HorizontalAlignment="Center" FontSize="15" Text="." VerticalAlignment="Center"/> <!-- Part 3 --> <TextBox Grid.Column="4" x:Name="part3" BorderThickness="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" PreviewKeyDown="Part3_KeyDown" local:FocusChangeExtension.IsFocused="{Binding IsPart3Focused}" Style="{StaticResource CustomTextBoxTextStyle}" Validation.ErrorTemplate="{StaticResource validationTemplate}"> <TextBox.Text> <Binding Path="Part3" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:IPRangeValidationRule Max="255" Min="0" /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> <TextBlock Grid.Column="5" HorizontalAlignment="Center" FontSize="15" Text="." VerticalAlignment="Center"/> <!-- Part 4 --> <TextBox Grid.Column="6" x:Name="part4" BorderThickness="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" PreviewKeyDown="Part4_KeyDown" local:FocusChangeExtension.IsFocused="{Binding IsPart4Focused}" Style="{StaticResource CustomTextBoxTextStyle}" Validation.ErrorTemplate="{StaticResource validationTemplate}"> <TextBox.Text> <Binding Path="Part4" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:IPRangeValidationRule Max="255" Min="0" /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </Grid> </UserControl>
三、验证输入格式
界面中为TextBox添加了CustomTextBoxTextStyle及validationTemplate样式,当输入格式不正确时,控件就会应用该样式。
通过自定义规则IPRangeValidationRule来验证输入的内容格式是否要求。
自定义规则代码如下:
public class IPRangeValidationRule : ValidationRule { private int _min; private int _max; public int Min { get { return _min; } set { _min = value; } } public int Max { get { return _max; } set { _max = value; } } public override ValidationResult Validate(object value, CultureInfo cultureInfo) { int val = 0; var strVal = (string)value; try { if (strVal.Length > 0) { if (strVal.EndsWith(".")) { return CheckRanges(strVal.Replace(".", "")); } // Allow dot character to move to next box return CheckRanges(strVal); } } catch (Exception e) { return new ValidationResult(false, "Illegal characters or " + e.Message); } if ((val < Min) || (val > Max)) { return new ValidationResult(false, "Please enter the value in the range: " + Min + " - " + Max + "."); } else { return ValidationResult.ValidResult; } } private ValidationResult CheckRanges(string strVal) { if (int.TryParse(strVal, out var res)) { if ((res < Min) || (res > Max)) { return new ValidationResult(false, "Please enter the value in the range: " + Min + " - " + Max + "."); } else { return ValidationResult.ValidResult; } } else { return new ValidationResult(false, "Illegal characters entered"); } } }
四、控制焦点变化
在界面代码中我通过local:FocusChangeExtension.IsFocused附加属性实现绑定属性控制焦点的变化。
附加属性的代码如下:
public static class FocusChangeExtension { public static bool GetIsFocused(DependencyObject obj) { return (bool)obj.GetValue(IsFocusedProperty); } public static void SetIsFocused(DependencyObject obj, bool value) { obj.SetValue(IsFocusedProperty, value); } public static readonly DependencyProperty IsFocusedProperty = DependencyProperty.RegisterAttached( "IsFocused", typeof(bool), typeof(FocusChangeExtension), new UIPropertyMetadata(false, OnIsFocusedPropertyChanged)); private static void OnIsFocusedPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (UIElement)d; if ((bool)e.NewValue) { control.Focus(); } } }
五、VM+后台代码混合实现焦点控制及内容复制粘贴
1、后台代码主要实现复制粘贴内容,另外←→移动光标也需要后台代码控制。通过PreviewKeyDown事件捕获键盘左移右移,复制,删除等事件,做出相应处理:
private void Part2_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) { if (e.Key == Key.Back && part2.Text == "") { part1.Focus(); } if (e.Key == Key.Right && part2.CaretIndex == part2.Text.Length) { part3.Focus(); e.Handled = true; } if (e.Key == Key.Left && part2.CaretIndex == 0) { part1.Focus(); e.Handled = true; } if (e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.C) { if (part2.SelectionLength == 0) { var vm = this.DataContext as IpAddressViewModel; Clipboard.SetText(vm.AddressText); } } }
通过DataObject.AddPastingHandler(part1, TextBox_Pasting)添加粘贴事件。使控件赋值。
2、通过ViewModel方式实现属性绑定通知,来控制焦点变化及内容赋值。
ViewModel类要实现绑定通知需要实现INotifyPropertyChanged接口中的方法。
我们新建一个IpAddressViewModel类继承INotifyPropertyChanged,代码如下:
public class IpAddressViewModel : INotifyPropertyChanged { public event EventHandler AddressChanged; public string AddressText { get { return $"{Part1??"0"}.{Part2??"0"}.{Part3??"0"}.{Part4??"0"}"; } } private bool isPart1Focused; public bool IsPart1Focused { get { return isPart1Focused; } set { isPart1Focused = value; OnPropertyChanged(); } } private string part1; public string Part1 { get { return part1; } set { part1 = value; SetFocus(true, false, false, false); var moveNext = CanMoveNext(ref part1); OnPropertyChanged(); OnPropertyChanged(nameof(AddressText)); AddressChanged?.Invoke(this, EventArgs.Empty); if (moveNext) { SetFocus(false, true, false, false); } } } private bool isPart2Focused; public bool IsPart2Focused { get { return isPart2Focused; } set { isPart2Focused = value; OnPropertyChanged(); } } private string part2; public string Part2 { get { return part2; } set { part2 = value; SetFocus(false, true, false, false); var moveNext = CanMoveNext(ref part2); OnPropertyChanged(); OnPropertyChanged(nameof(AddressText)); AddressChanged?.Invoke(this, EventArgs.Empty); if (moveNext) { SetFocus(false, false, true, false); } } } private bool isPart3Focused; public bool IsPart3Focused { get { return isPart3Focused; } set { isPart3Focused = value; OnPropertyChanged(); } } private string part3; public string Part3 { get { return part3; } set { part3 = value; SetFocus(false, false, true, false); var moveNext = CanMoveNext(ref part3); OnPropertyChanged(); OnPropertyChanged(nameof(AddressText)); AddressChanged?.Invoke(this, EventArgs.Empty); if (moveNext) { SetFocus(false, false, false, true); } } } private bool isPart4Focused; public bool IsPart4Focused { get { return isPart4Focused; } set { isPart4Focused = value; OnPropertyChanged(); } } private string part4; public string Part4 { get { return part4; } set { part4 = value; SetFocus(false, false, false, true); var moveNext = CanMoveNext(ref part4); OnPropertyChanged(); OnPropertyChanged(nameof(AddressText)); AddressChanged?.Invoke(this, EventArgs.Empty); } } public void SetAddress(string address) { if (string.IsNullOrWhiteSpace(address)) return; var parts = address.Split('.'); if (int.TryParse(parts[0], out var num0)) { Part1 = num0.ToString(); } if (int.TryParse(parts[1], out var num1)) { Part2 = parts[1]; } if (int.TryParse(parts[2], out var num2)) { Part3 = parts[2]; } if (int.TryParse(parts[3], out var num3)) { Part4 = parts[3]; } } private bool CanMoveNext(ref string part) { bool moveNext = false; if (!string.IsNullOrWhiteSpace(part)) { if (part.Length >= 3) { moveNext = true; } if (part.EndsWith(".")) { moveNext = true; part = part.Replace(".", ""); } } return moveNext; } private void SetFocus(bool part1, bool part2, bool part3, bool part4) { IsPart1Focused = part1; IsPart2Focused = part2; IsPart3Focused = part3; IsPart4Focused = part4; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
到这里基本就完成了,生成控件然后到MainWindow中引用该控件
六、最终效果
————————————————————
代码地址:https://github.com/cmfGit/IpAddressControl.git
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。