Silverlight开发中的疑难杂症-控件设计篇-如何实现一个NumericBox(上)
在最近的Silverlight开发中,发现一个需求,需要一个只能够输入数字,并且能够对小数点后的位数进行控制并填充的控件,原有的TextBox并不能实现这一个功能,于是就决定自己实现一个,要包含的具体的功能如下:
l 只能输入0-9的数字和至多一个小数点;
l 能够屏蔽通过非正常途径的不正确输入(输入法,粘贴等);
l 能够控制小数点后的最大位数,超出位数则无法继续输入;
l 能够选择当小数点数位数不足时是否补0;
l 去除开头部分多余的0(为方便处理,当在开头部分输入0时,自动在其后添加一个小数点);
l 由于只能输入一个小数点,当在已有的小数点前再次按下小数点,能够跳过小数点;
在实现前参考了许多网上的相关资料,虽然没有找到具体的实现代码,但是还是得到了很多有用的思路,最终决定将上述功能分成两个部分来实现:其中控制只能输入数字的部分作为一个通用的TextBoxFilterBehavior进行设计;而格式化的部分则在NumericBox内部实现,下面首先介绍TextBoxFilterBehavior的实现。
在实现之前,首先查看了TextBox的相关事件,发现Silverlight中的事件比WPF来说有了很大的减少,没得选择,可用的事件只有KeyDown跟TextChanged。这里使用KeyDown事件来处理按键产生的输入,TextChanged事件来处理通过其它方式进行的输入(其实完全可以都在TextChanged事件中进行处理,我也想不通为什么自己选择分开进行~)。
由于要做成一个通用的TextBox筛选行为,所以这里添加了一个筛选类型的枚举选项TextBoxFilterOptions,里面暂时只有数字、小数点跟字母,留待以后扩充,实现代码如下:
2 /// TextBox筛选选项
3 /// </summary>
4 [Flags]
5 public enum TextBoxFilterOptions
6 {
7 /// <summary>
8 /// 不采用任何筛选
9 /// </summary>
10 None = 0,
11 /// <summary>
12 /// 数字类型不参与筛选
13 /// </summary>
14 Numeric = 1,
15 /// <summary>
16 /// 字母类型不参与筛选
17 /// </summary>
18 Character = 2,
19 /// <summary>
20 /// 小数点不参与筛选
21 /// </summary>
22 Dot = 4,
23 /// <summary>
24 /// 其它类型不参与筛选
25 /// </summary>
26 Other = 8
27 }
28
29 /// <summary>
30 /// TextBox筛选选项枚举扩展方法
31 /// </summary>
32 public static class TextBoxFilterOptionsExtension
33 {
34 /// <summary>
35 /// 在全部的选项中是否包含指定的选项
36 /// </summary>
37 /// <param name="allOptions">所有的选项</param>
38 /// <param name="option">指定的选项</param>
39 /// <returns></returns>
40 public static bool ContainsOption(this TextBoxFilterOptions allOptions, TextBoxFilterOptions option)
41 {
42 return (allOptions & option) == option;
43 }
44 }
45
其中ContainsOption方法是一个提供了方便的枚举运算的扩展方法。
另外还增加了一个键盘操作的帮助类KeyboardHelper,在实现时发现Silverlight中的按键比WPF中少了很多,甚至连主键盘上的小数点键(句号)都无法识别,这里使用了一个平台相关的keycode进行判别,如果有更好的方法请给我留言。代码如下:
2 /// 键盘操作帮助类
3 /// </summary>
4 public class KeyboardHelper
5 {
6 /// <summary>
7 /// 键盘上的句号键
8 /// </summary>
9 public const int OemPeriod = 190;
10
11 #region Fileds
12
13 /// <summary>
14 /// 控制键
15 /// </summary>
16 private static readonly List<Key> _controlKeys = new List<Key>
17 {
18 Key.Back,
19 Key.CapsLock,
20 Key.Ctrl,
21 Key.Down,
22 Key.End,
23 Key.Enter,
24 Key.Escape,
25 Key.Home,
26 Key.Insert,
27 Key.Left,
28 Key.PageDown,
29 Key.PageUp,
30 Key.Right,
31 Key.Shift,
32 Key.Tab,
33 Key.Up
34 };
35
36 #endregion
37
38 /// <summary>
39 /// 是否是数字键
40 /// </summary>
41 /// <param name="key">按键</param>
42 /// <returns></returns>
43 public static bool IsDigit(Key key)
44 {
45 bool shiftKey = (Keyboard.Modifiers & ModifierKeys.Shift) != 0;
46 bool retVal;
47 //按住shift键后,数字键并不是数字键
48 if (key >= Key.D0 && key <= Key.D9 && !shiftKey)
49 {
50 retVal = true;
51 }
52 else
53 {
54 retVal = key >= Key.NumPad0 && key <= Key.NumPad9;
55 }
56 return retVal;
57 }
58
59 /// <summary>
60 /// 是否是控制键
61 /// </summary>
62 /// <param name="key">按键</param>
63 /// <returns></returns>
64 public static bool IsControlKeys(Key key)
65 {
66 return _controlKeys.Contains(key);
67 }
68
69 /// <summary>
70 /// 是否是小数点
71 /// Silverlight中无法识别问号左边的那个小数点键
72 /// 只能识别小键盘中的小数点
73 /// </summary>
74 /// <param name="key">按键</param>
75 /// <returns></returns>
76 public static bool IsDot(Key key)
77 {
78 return key == Key.Decimal;
79 }
80
81 /// <summary>
82 /// 是否是小数点
83 /// </summary>
84 /// <param name="key">按键</param>
85 /// <param name="keyCode">平台相关的按键代码</param>
86 /// <returns></returns>
87 public static bool IsDot(Key key, int keyCode)
88 {
89 return IsDot(key) || (key == Key.Unknown && keyCode == OemPeriod);
90 }
91
92 /// <summary>
93 /// 是否是字母键
94 /// </summary>
95 /// <param name="key">按键</param>
96 /// <returns></returns>
97 public static bool IsCharacter(Key key)
98 {
99 return key >= Key.A && key <= Key.Z;
100 }
101 }
102
最后是TextBoxFilterBehavior的实现,关于Behavior的相关概念,大家可以参见Blend的帮助文档以及MSDN的相关文章,园子里也有很多大牛对它进行了详细的阐述,我就不深入讲解了。在这里,我们首先添加了一个TextBoxFilterOptions类型的依赖属性,用来设置过滤的选项,如下:
2
3 /// <summary>
4 /// TextBox筛选选项,这里选择的为过滤后剩下的按键
5 /// 控制键不参与筛选,可以多选组合
6 /// </summary>
7 public TextBoxFilterOptions TextBoxFilterOptions
8 {
9 get { return (TextBoxFilterOptions)GetValue(TextBoxFilterOptionsProperty); }
10 set { SetValue(TextBoxFilterOptionsProperty, value); }
11 }
12
13 // Using a DependencyProperty as the backing store for TextBoxFilterOptions. This enables animation, styling, binding, etc...
14 public static readonly DependencyProperty TextBoxFilterOptionsProperty =
15 DependencyProperty.Register("TextBoxFilterOptions", typeof(TextBoxFilterOptions), typeof(TextBoxFilterBehavior), new PropertyMetadata(TextBoxFilterOptions.None));
16
17 #endregion
18
然后注册了关联的TextBox控件的KeyDown和TextChanged事件,在里面对输入的文本进行了验证,如果没有通过验证,则将文本充值回上次正确的文本。由于在TextChanged事件中并没有WPF里面的TextChange数组,所以无法拿到本次变更的文本,只能选择处理完整的Text。完整的代码如下:
2 /// TextBox筛选行为,过滤不需要的按键
3 /// </summary>
4 public class TextBoxFilterBehavior : Behavior<TextBox>
5 {
6 private string _prevText = string.Empty;
7
8 public TextBoxFilterBehavior()
9 {
10 }
11
12 #region Dependency Properties
13
14 /// <summary>
15 /// TextBox筛选选项,这里选择的为过滤后剩下的按键
16 /// 控制键不参与筛选,可以多选组合
17 /// </summary>
18 public TextBoxFilterOptions TextBoxFilterOptions
19 {
20 get { return (TextBoxFilterOptions)GetValue(TextBoxFilterOptionsProperty); }
21 set { SetValue(TextBoxFilterOptionsProperty, value); }
22 }
23
24 // Using a DependencyProperty as the backing store for TextBoxFilterOptions. This enables animation, styling, binding, etc...
25 public static readonly DependencyProperty TextBoxFilterOptionsProperty =
26 DependencyProperty.Register("TextBoxFilterOptions", typeof(TextBoxFilterOptions), typeof(TextBoxFilterBehavior), new PropertyMetadata(TextBoxFilterOptions.None));
27
28 #endregion
29
30 protected override void OnAttached()
31 {
32 base.OnAttached();
33
34 this.AssociatedObject.KeyDown += new KeyEventHandler(AssociatedObject_KeyDown);
35 this.AssociatedObject.TextChanged += new TextChangedEventHandler(AssociatedObject_TextChanged);
36 }
37
38 protected override void OnDetaching()
39 {
40 base.OnDetaching();
41
42 this.AssociatedObject.KeyDown -= new KeyEventHandler(AssociatedObject_KeyDown);
43 this.AssociatedObject.TextChanged -= new TextChangedEventHandler(AssociatedObject_TextChanged);
44 }
45
46 #region Events
47
48 /// <summary>
49 /// 处理通过其它手段进行的输入
50 /// </summary>
51 /// <param name="sender"></param>
52 /// <param name="e"></param>
53 void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e)
54 {
55 //如果符合规则,就保存下来
56 if (IsValidText(this.AssociatedObject.Text))
57 {
58 _prevText = this.AssociatedObject.Text;
59 }
60 //如果不符合规则,就恢复为之前保存的值
61 else
62 {
63 int selectIndex = this.AssociatedObject.SelectionStart - (this.AssociatedObject.Text.Length - _prevText.Length);
64 this.AssociatedObject.Text = _prevText;
65 this.AssociatedObject.SelectionStart = selectIndex;
66 }
67
68 }
69
70 /// <summary>
71 /// 处理按键产生的输入
72 /// </summary>
73 /// <param name="sender"></param>
74 /// <param name="e"></param>
75 void AssociatedObject_KeyDown(object sender, KeyEventArgs e)
76 {
77 bool handled = true;
78 //不进行过滤
79 if (TextBoxFilterOptions == TextBoxFilterOptions.None ||
80 KeyboardHelper.IsControlKeys(e.Key))
81 {
82 handled = false;
83 }
84 //数字键
85 if (handled && TextBoxFilterOptions.ContainsOption(TextBoxFilterOptions.Numeric))
86 {
87 handled = !KeyboardHelper.IsDigit(e.Key);
88 }
89 //小数点
90 if (handled && TextBoxFilterOptions.ContainsOption(TextBoxFilterOptions.Dot))
91 {
92 handled = !(KeyboardHelper.IsDot(e.Key, e.PlatformKeyCode) && !_prevText.Contains("."));
93 if (KeyboardHelper.IsDot(e.Key, e.PlatformKeyCode) && _prevText.Contains("."))
94 {
95 //如果输入位置的下一个就是小数点,则将光标跳到小数点后面
96 if (this.AssociatedObject.SelectionStart< this.AssociatedObject.Text.Length && _prevText[this.AssociatedObject.SelectionStart] == '.')
97 {
98 this.AssociatedObject.SelectionStart++;
99 }
100 }
101 }
102 //字母
103 if (handled && TextBoxFilterOptions.ContainsOption(TextBoxFilterOptions.Character))
104 {
105 handled = !KeyboardHelper.IsDot(e.Key);
106 }
107 e.Handled = handled;
108 }
109
110 #endregion
111
112 #region Private Methods
113
114 /// <summary>
115 /// 判断是否符合规则
116 /// </summary>
117 /// <param name="c"></param>
118 /// <returns></returns>
119 private bool IsValidChar(char c)
120 {
121 if (TextBoxFilterOptions == TextBoxFilterOptions.None)
122 {
123 return true;
124 }
125 else if (TextBoxFilterOptions.ContainsOption(TextBoxFilterOptions.Numeric) &&
126 '0' <= c && c <= '9')
127 {
128 return true;
129 }
130 else if (TextBoxFilterOptions.ContainsOption(TextBoxFilterOptions.Dot) &&
131 c == '.')
132 {
133 return true;
134 }
135 else if (TextBoxFilterOptions.ContainsOption(TextBoxFilterOptions.Character))
136 {
137 if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'))
138 {
139 return true;
140 }
141 }
142 return false;
143 }
144
145 /// <summary>
146 /// 判断文本是否符合规则
147 /// </summary>
148 /// <param name="text"></param>
149 /// <returns></returns>
150 private bool IsValidText(string text)
151 {
152 //只能有一个小数点
153 if (text.IndexOf('.') != text.LastIndexOf('.'))
154 {
155 return false;
156 }
157
158 foreach (char c in text)
159 {
160 if (!IsValidChar(c))
161 {
162 return false;
163 }
164 }
165 return true;
166 }
167
168 #endregion
169 }
170
TextBoxFilterBehavior类主要的实现思路和代码就是这样,希望能给需要的朋友一定的帮助,NumericBox类的实现思路及相关代码我将在下篇文章中给出,敬请关注。
(未完待续)