用于格式化数字输入的 TextBoxTemplate






4.71/5 (3投票s)
用于纬度、经度、距离、方位角、时间和其他格式化数字输入的控件
摘要
这个带模板的
控件在您有特殊/格式化数字输入时非常有用。像使用普通的TextBox
一样使用它,它将通过单个“模板”字符串驱动进行必要的格式化和验证。TextBox
引言
一个海上导航应用程序要求用户输入纬度、经度、距离、方位角、速度等——所有这些都有各种数字输入约束。我需要一个带有输入模板的TextBox
,它可以处理输入格式,例如
数据类型 | 所需的格式示例 |
纬度 | 22º33.445'N |
距离\ | 12.5 NM |
已用时间 | 12:34:56.7 |
一天中的时间 | 10:34:56.7PM(或 22:34:56.7,取决于用户的偏好) |
在内部,所有这些值都存储为
(或时间值),我想用一个标准的控件来尽可能轻松地获取这些类型的用户输入。double
我们来看看纬度数据类型。内部的纬度
存储为浮点度,范围从 -90.0 到 +90.0 度,正值显示为“N”(北),负值显示为“S”(南)。这些通常显示为度数和分钟,带有小数部分。经度类似,但范围是从 -180 到 +180,值显示为“E”(东)和“W”(西)。小数部分可以是一位小数(约 0.1 英里),但也经常显示三位小数(例如 22º33.445'N),提供约 6 英尺的实际分辨率。double
方位角是对物理对象的角度,以度为单位,范围从 0 到 360。通常不包含小数部分,因为在移动的船上很难准确测量方位角。
通常的做法是将单位符号(英里、码等)放在输入
右侧的TextBox
Label
中。然而,纬度、时间等的单位(度数和分钟符号)会穿插在值的显示中。为了保持一致性,我选择将所有单位符号包含在
显示中,适用于所有值类型,并保护单位不被用户输入。如果我将单位包含为TextBoxTemplate
,那么时间输入控件(例如)可能由三个Label
s 分别用于小时、分钟和秒,另一个(或TextBoxe
)用于 AM/PM 指定,以及用于冒号的ComboBox
s。这可以封装在一个用户控件中,但我需要为每种数据类型和输入格式创建一个单独的用户控件。我实际上是这样开始编写的,但在控件数量变得繁琐之后改变了主意。Label
一个对我有利的问题是,航海导航员(我的目标用户)习惯于固定格式的数据字段,并且不关心前导零。这意味着所有输入都可以始终处于“覆盖”模式。用户会看到一个有效的输入值(00º00.000'N),可以更改值,但永远不需要输入度数符号、小数点等。
控件的工作有三个方面:
以用户偏好的(或程序指定的)格式显示值
限制用户仅输入有效数据
允许使用相同的控件,而不管数据类型和用户的显示偏好
关于控件
解决方案是扩展TextBox
,使其包含一个InputTemplate
字符串,该字符串定义了格式和输入约束。通过嵌入的格式,控件可以看起来像这样(注意单个字符的突出显示)
这里显示的TextBoxTemplate
有三个输入“字段”,第一个是两位数的度数,第二个是 2.2 位小数的分钟,第三个是“N”(用户可以将其更改为“S”)。其他符号,如 º 符号、'(以及 .)被忽略,并且用户无法将光标移到它们上面或更改它们。输入数据时,光标会自动跳过这些字符。
由于控件自行格式化,因此不要设置TextBox
的“Text
”属性,而是设置新的“Value
”属性。设置此属性时,值将根据InputTemplate
字符串进行格式化。
控件的行为由“InputTemplate
”属性定义。以下是一些示例InputTemplate
字符串(包含在控件中)
public static string latTemplate = "90º60.00'N";
public static string lonTemplate = "180º60.00'E";
public static string speedTemplate = "00.0kts";
public static string rangeTemplate = "00.000nm";
public static string rangeTemplateYds = "00000yds";
public static string rangeTemplateMtrs = "00000m";
public static string bearingTemplate = "360º";
public static string inclinationTemplate = "00.0ºE";
public static string timeTemplate = "000.0min";
public static string minutesTemplate = "00min";
public static string timeOfDayTemplate = "13:60:60.0AM";
public static string shortTimeOfDayTemplate = "13:60:60AM";
public static string timeOfDayTemplate24 = "24:60:60.0";
public static string shortTimeOfDayTemplate24 = "24:60:60";
每个模板不仅定义了数据值的显示格式,还定义了值的输入约束。对于上面的latTemplate
,“90”表示度数字段必须是两位数且严格小于 90;“60.00”表示分钟部分必须是 2.2 位小数,严格小于 60,并且任何度的分数都应以 60 为基数进行转换;“N”是一个特殊字段,可以是“N”或“S”。InputTemplate
中其他可能的特殊字符是“E”(表示“E”或“W”)和“A”(表示“A”或“P”(用于 AM/PM))。InputTemplate 中所有其他字符都被视为“单位符号”并被忽略/跳过。
最终控件显示在窗口中时看起来像这样(显示了纬度和一天中的时间模板)
想要两位数小数的分钟而不是两位数小数?只需将InputTemplate
中的“60.00”更改为“60.000”。控件将处理其他所有事情。
这是错误消息弹出窗口在发生验证错误的位置字符下方显示的样子:(我刚刚按下了键盘上的“9”)
使用控件
该控件继承自 WPF TextBox
控件,因此可以将其添加到Window
中,并且可以访问所有常用参数,但您必须设置InputTemplate
属性和Value
属性,并且绝对不能设置Text
属性。创建TextBoxTemplate
控件的实例(无论是通过代码还是 XAML),并为其传递一个InputTemplate
,该InputTemplate
是一个定义控件行为的字符串(参见上文)。有用于经过测试的模板的静态字符串,例如 Latitude、Longitude、time、range、bearing 等。然后,不要通过访问TextBox.Text
属性,而是通过Value
属性进行访问,该属性是一个double
,表示输入的值(例如,度数、小时等)。
这是声明控件并使用预定义模板之一的方法——有关如何设置模板的详细信息包含在代码中
<my:TextBoxTemplate InputTemplate="{x:Static my:TextBoxTemplate.timeOfDayTemplate}" HorizontalAlignment="Left" Margin="150,50,0,0" x:Name="textBoxTemplate3" VerticalAlignment="Top" LostFocus="textBoxTemplate1_LostFocus" />
这是您如何处理控件的Value
属性的进出——我喜欢在此使用LostFocus
事件,因为它类似于 Excel,并且不必在用户输入的每个按键都被接收时处理值
private void textBoxTemplate1_LostFocus(object sender, RoutedEventArgs e)
{
if (sender == textBoxTemplate1)
{
textBoxTemplate2.Value = textBoxTemplate1.Value;
}
}
附加代码包含控件及其使用演示。它只是将值从一个控件复制到另一个控件,以显示如何获取和设置值。要查看它在为其开发的免费应用程序中的样子,请点击这里。
幕后原理/值得关注之处
设置 InputTemplate
:当设置InputTemplate
时,它会被解析并创建一个字段List
。每个字段都有一个起始位置、长度、最大值和截断标志。此列表由格式化和验证函数使用。截断标志用于整数字段(如度数),因此如果一个值是,例如 30.9,第一个字段将是 30,而 .9 可以转换为分钟。最大值也用作分数转换的数值基数,因此分钟以 60 为基数进行转换。
设置或获取值:有两个函数
protected double GetValueFromText(string text)
protected string GetTextFromValue(double theVal)
用于处理将数据格式化到TextBox
和返回值的逻辑。这些依赖于在设置InputTemplate
时构建的字段列表。请注意,由于double
比文本字符串携带的精度更高,因此会出现一些舍入。由于直接设置TextBox
的 Text 属性允许程序将不与InputTemplate
兼容的字符串放入TextBox
中,因此直接设置 Text 会引发异常。否则,GetValueFromText 和输入验证将不得不扩展以处理字符串中已存在的无效值。
事件:控件处理TextBox
的三个事件:
PreviewKeyDown
SelectionChanged
GotFocus
在PreviewKeyDown
事件处理程序中,控件执行了大部分“繁重的工作”。它确定输入的按键对于光标位置是否有效,然后检查是否接受按键,这样会产生一个有效值。如果值有效,则将按键传递给底层TextBox
。否则,控件使用弹出窗口在光标位置显示合理的错误消息。这里有一些有趣的问题:
如果光标位置在
TextBoxTemplate
的开头或结尾,控件会MoveFocus
以响应箭头键移动到上一个/下一个控件。如果用户按下 BACK,它将被视为左箭头。
有趣的情况:如果一个值为 180 的字段当前包含 090,光标位于第一个字符位置,用户按下“1”。如果接受,
TextBox
将包含 190,这是无效的,但显示错误消息并拒绝输入将阻止用户输入“120”,这是完全合理的。解决方案:接受“1”并将后面的数字设置为零,因此框中包含“100”。用户似乎喜欢这个解决方案。
在SelectionChanged
事件处理程序中,控件通过将SelectionLength
设置为 1 来强制TextBox
进入“覆盖”模式,因此始终有一个字符被选中/高亮显示。它还检查光标是否定位在有效输入字符上,并跳过非输入字符。这需要一个额外的变量“movingLeft
”(由PreviewKeyDown
设置),以便在光标位于无效字符上时确定光标移动的方向。
在GotFocus
事件处理程序中,控件处理进入控件时定位光标的问题。通常,光标会定位在最近一次使用控件时的字符位置,这可能会令人困惑。如果控件是通过鼠标单击获得焦点,则会选择鼠标光标下的字符;如果是通过 Shift-Tab,则会选择最后一个字符;否则,会选择第一个字符。
时间值通过TimeToDouble
和TimeFromDouble
函数将其转换为double
来处理。这样Value
始终是double
。在未来的版本中,我可能会选择不同的解决方案来将时间值作为double
处理。
历史
初次提交:2012 年 3 月 31 日
修订:2012 年 4 月 1 日,添加了大量详细信息