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
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。