65.9K
CodeProject 正在变化。 阅读更多。
Home

用于格式化数字输入的 TextBoxTemplate

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (3投票s)

2012年4月2日

CPOL

9分钟阅读

viewsIcon

35552

downloadIcon

1337

用于纬度、经度、距离、方位角、时间和其他格式化数字输入的控件

摘要

这个带模板的TextBox 控件在您有特殊/格式化数字输入时非常有用。像使用普通的TextBox 一样使用它,它将通过单个“模板”字符串驱动进行必要的格式化和验证。

引言

一个海上导航应用程序要求用户输入纬度、经度、距离、方位角、速度等——所有这些都有各种数字输入约束。我需要一个带有输入模板的TextBox,它可以处理输入格式,例如

数据类型所需的格式示例
纬度22º33.445'N
距离\12.5 NM
已用时间 12:34:56.7
一天中的时间10:34:56.7PM(或 22:34:56.7,取决于用户的偏好)

在内部,所有这些值都存储为double(或时间值),我想用一个标准的控件来尽可能轻松地获取这些类型的用户输入。

我们来看看纬度数据类型。内部的纬度double存储为浮点度,范围从 -90.0 到 +90.0 度,正值显示为“N”(北),负值显示为“S”(南)。这些通常显示为度数和分钟,带有小数部分。经度类似,但范围是从 -180 到 +180,值显示为“E”(东)和“W”(西)。小数部分可以是一位小数(约 0.1 英里),但也经常显示三位小数(例如 22º33.445'N),提供约 6 英尺的实际分辨率。

方位角是对物理对象的角度,以度为单位,范围从 0 到 360。通常不包含小数部分,因为在移动的船上很难准确测量方位角。

通常的做法是将单位符号(英里、码等)放在输入TextBox右侧的Label中。然而,纬度、时间等的单位(度数和分钟符号)会穿插在值的显示中。为了保持一致性,我选择将所有单位符号包含在TextBoxTemplate 显示中,适用于所有值类型,并保护单位不被用户输入。如果我将单位包含为Label,那么时间输入控件(例如)可能由三个TextBoxes 分别用于小时、分钟和秒,另一个(或ComboBox)用于 AM/PM 指定,以及用于冒号的Labels。这可以封装在一个用户控件中,但我需要为每种数据类型和输入格式创建一个单独的用户控件。我实际上是这样开始编写的,但在控件数量变得繁琐之后改变了主意。

一个对我有利的问题是,航海导航员(我的目标用户)习惯于固定格式的数据字段,并且不关心前导零。这意味着所有输入都可以始终处于“覆盖”模式。用户会看到一个有效的输入值(00º00.000'N),可以更改值,但永远不需要输入度数符号、小数点等。

控件的工作有三个方面:

  1. 以用户偏好的(或程序指定的)格式显示值

  2. 限制用户仅输入有效数据

  3. 允许使用相同的控件,而不管数据类型和用户的显示偏好


关于控件

解决方案是扩展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的三个事件:

  1. PreviewKeyDown

  2. SelectionChanged

  3. GotFocus

PreviewKeyDown事件处理程序中,控件执行了大部分“繁重的工作”。它确定输入的按键对于光标位置是否有效,然后检查是否接受按键,这样会产生一个有效值。如果值有效,则将按键传递给底层TextBox。否则,控件使用弹出窗口在光标位置显示合理的错误消息。这里有一些有趣的问题:

  • 如果光标位置在TextBoxTemplate的开头或结尾,控件会MoveFocus以响应箭头键移动到上一个/下一个控件。

  • 如果用户按下 BACK,它将被视为左箭头。

  • 有趣的情况:如果一个值为 180 的字段当前包含 090,光标位于第一个字符位置,用户按下“1”。如果接受,TextBox将包含 190,这是无效的,但显示错误消息并拒绝输入将阻止用户输入“120”,这是完全合理的。解决方案:接受“1”并将后面的数字设置为零,因此框中包含“100”。用户似乎喜欢这个解决方案。

SelectionChanged事件处理程序中,控件通过将SelectionLength设置为 1 来强制TextBox进入“覆盖”模式,因此始终有一个字符被选中/高亮显示。它还检查光标是否定位在有效输入字符上,并跳过非输入字符。这需要一个额外的变量“movingLeft”(由PreviewKeyDown设置),以便在光标位于无效字符上时确定光标移动的方向。

GotFocus事件处理程序中,控件处理进入控件时定位光标的问题。通常,光标会定位在最近一次使用控件时的字符位置,这可能会令人困惑。如果控件是通过鼠标单击获得焦点,则会选择鼠标光标下的字符;如果是通过 Shift-Tab,则会选择最后一个字符;否则,会选择第一个字符。

时间值通过TimeToDoubleTimeFromDouble函数将其转换为double来处理。这样Value始终是double。在未来的版本中,我可能会选择不同的解决方案来将时间值作为double处理。

历史

初次提交:2012 年 3 月 31 日

修订:2012 年 4 月 1 日,添加了大量详细信息

© . All rights reserved.