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

NumericalBox - WPF TextBox 的子类,只接受格式正确的十进制数

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2015 年 9 月 9 日

CPOL

5分钟阅读

viewsIcon

22047

downloadIcon

509

接受 [[-+]整数部分][.小数部分][{eE}[-+]指数] 格式数字的 TextBox

引言

存在大量允许只输入数字的 TextBox 的子类实现。通常,畸形数字的拒绝是在额外的验证阶段提供的,并且在输入阶段允许诸如 1+234 或 .+1e23.4 之类的怪异输入。我们将设置输入约束,以便只能输入格式正确的数字。

正则表达式方法

我们希望在输入时只允许 [[-+]整数部分][.小数部分][{eE}[-+]指数] 格式的数字。

也就是说,诸如 123、+123、123.45、12e+45 等数字允许输入,而 123.、12.34.5、1+2345e 等数字则被禁止。同时,我们不会关心数字转换为机器格式(例如,按值)的能力。我们只检查数字的语法格式。

1950 年代伟大的 Stephen Kleene 发明的经典正则表达式为我们铺平了通往目标之路。我们将使用 C# 方言的正则表达式。

我们将实现一个NumericalBox - System.Windows.Controls.TextBox 类的简易子类。实现仅包含 3 个方法重写和一个辅助方法。让我们逐步考虑这些重写。

OnKey 重写

OnKey 是一个方法,它只允许允许的按键通过。通过设置不允许按键的 KeyEventArgs.Handled=true,我们隐藏了事件路由中OnKey 事件对监听器的可见性。允许的按键将调用监听器。

protected override void OnKeyDown(KeyEventArgs e)
{
    switch ( e.Key )
    {
        case Key.E:
        case Key.OemPlus:
        case Key.OemMinus:
        case Key.OemPeriod:
        case Key.Subtract:
        case Key.Add:
        case Key.Decimal:
        case Key.D0:
        case Key.D1:
        case Key.D2:
        case Key.D3:
        case Key.D4:
        case Key.D5:
        case Key.D6:
        case Key.D7:
        case Key.D8:
        case Key.D9:
        case Key.NumPad0:
        case Key.NumPad1:
        case Key.NumPad2:
        case Key.NumPad3:
        case Key.NumPad4:
        case Key.NumPad5:
        case Key.NumPad6:
        case Key.NumPad7:
        case Key.NumPad8:
        case Key.NumPad9:
        case Key.Back:
            break;
        default:
            e.Handled = true;
            break;
    }
}
图. 1: OnKey 方法 - 文件 NumericalBox.cs, 项目 NumericalBoxCtrl

OnKey 将阻止无意义的按键,但不会阻止畸形的数字,例如 +..1e.e.1 等。

数字验证的正则表达式

在文本更改时(即在输入时)检查数字的语法似乎是个好主意。现在,第一个正则表达式出现了。它是

[-+]?\d*(\.?)(\d+)([eE][-+]?\d+)?

含义:
[-+]? - 可选符号部分:匹配 -+ 或无
\d* - 整数部分:匹配零个或多个十进制数字
(\.?) - 小数点部分:匹配零个或一个小数点的组
 (\d+)- 小数部分:匹配一个或多个十进制数字的组
 ([eE][-+]?\d+)?- 指数部分:匹配零个或一个指数基数(e 或 E)后跟可选符号,再后跟一个或多个十进制数字的组。

该正则表达式以 Regex 类型的变量形式提供;参数值 RegexOptions.ECMAScript 仅将可能的数字表示形式限制为英语。

Regex _rgxChangedValid = new Regex( @"^[-+]?\d*(\.?)(\d+)([eE][-+]?\d+)?$" );
图. 2: Regex 变量 - 文件 NumericalBox.cs, 项目 NumericalBoxCtrl

锚点 ^ 和 $ 表示匹配从字符串的开头开始,并且必须发生在字符串的末尾。

OnTextChanged 重写 - 第一次尝试 - 项目 NumericalBoxCtrlJobHalfDone

让我们在OnTextChanged中实现以上所有内容

protected override void OnTextChanged( TextChangedEventArgs e )
{
    base.OnTextChanged( e );

    if ( !IsInputTextValid( Text ) )
    {
        Text = _lastVerifiedText;
    }

    _lastVerifiedText = Text;
    CaretIndex = Text.Length;
}

private bool IsInputTextValid( string text )
{
    return !string.IsNullOrEmpty( text == null ? null : text.Trim( ) ) 
        ? ( _rgxChangedValid.IsMatch( text ) ? true : false ) 
            : false;
}

图. 3: OnChangeText 重写和 IsInputTextValid 方法 - 文件 NumericalBox.cs, 项目 NumericalBoxCtrlJobHalfDone

私有变量 _lastVerifiedText 存储最后一个通过正则表达式检查的已键入字符串,并用于恢复上一个有效版本。最终项目NumericalTextBox将展示更方便的恢复方式。

现在让我们尝试项目NumericalTextBoxCtrlJobHalfDone - 启动演示NumericalTextBoxCtrlJobHalfDone.exe 并开始在文本框中输入数字 123.45。

图. 1: 控件不允许输入 123. 或 123e

尝试失败!我们无法输入小数点。

失败原因

问题在于我们的 _rgxChangedValid 描述了一个完整的、格式正确的数字;例如,它必须在小数点后至少包含一个数字。但是,在顺序输入过程中,用户无法同时输入小数点和它后面的第一个数字 - 小数点总是先出现并被拒绝。

_rgxChangedValid 期望一个完整的数字,而此时我们只有一个未完成的工作。“不要给傻瓜看未完成的工作!”

OnTextChanged 重写 - 第二次尝试 - 项目 NumericalBoxCtrl

因此,我们应该在OnTextChanged重写中使用不同的正则表达式来批准不完整的数字。就是这个

Regex _rgxChangedValid = new Regex( @"^[-+]?\d*(\.?)(\d*)([eE][-+]?\d*)?$", RegexOptions.ECMAScript );
图. 4: 用于批准不完整数字的 Regex 变量 - 文件 NumericalBox.cs, 项目 NumericalBoxCtrl

NumericalBoxCtrlJobHalfDone_rgxChangeValidNumericalBoxCtrl_rgxChangedValid之间只有一个区别。第二个正则表达式在第二个和第三个数字位置有\d*。量词*表示“匹配前一个元素零次或多次”。在我们的例子中,这意味着“零个或多个数字”,进而意味着字符串“123.”和“123e”是被允许的。现在这是新版本OnTextChanged的代码

protected override void OnTextChanged( TextChangedEventArgs e )
{
    base.OnTextChanged( e );
    string longestValidText;

    if ( !IsTextValid( _rgxChangedValid, _rgxSaveChanged, Text, out longestValidText ) )
    {
        this.Text = longestValidText;
    }

    CaretIndex = Text.Length;
}
图. 4: OnTextChanged - 文件 NumericalBox.cs, 项目 NumericalBoxCtrl

私有方法IsTextValid (稍后解释)使用 _rgxChangedValid 检查NumericalBox的内容,并尝试从无效内容中提取最长的有效子字符串。

OnLostFocus 重写

现在,完整的数字也应该被检查 - 当它完成时,当然。LostFocus 事件是完整性的标准:当NumericalBox失去焦点时,数字应被视为已完成。微软在为 TextBox 的绑定选择 Mode=LostFocus 时并非偶然!

我们第一个“数字验证的正则表达式”应该在这里使用。我们现在称之为 _rgxLostFocusValid

Regex _rgxLostFocusValid = new Regex( @"^[-+]?\d*(\.?)(\d+)([eE][-+]?\d+)?$", RegexOptions.ECMAScript );

这是OnLostFocus

protected override void OnLostFocus( RoutedEventArgs e )
{
    base.OnLostFocus( e );
    string longestValidText;

    if(!IsTextValid( _rgxLostFocusValid, _rgxSaveLost, Text, out longestValidText ))
    {
        this.Text = longestValidText;
    }
}
图. 5: OnLostFocus - 文件 NumericalBox.cs, 项目 NumericalBoxCtrl

它几乎与OnTextChanged相似,除了它使用 _rgxLostFocusValid 正则表达式,并且不将光标放置在字符串的末尾,因为光标被焦点丢失取消了。

顺便说一句,关于焦点丢失。它是由框外的鼠标单击执行的,我很抱歉它是在代码隐藏中实现的。对于如此短的项目,LostFocus 事件的 MVVM 实现看起来有点笨拙。

IsTextValid 方法和有效子字符串的提取

IsTextValid 方法提取最长的有效子字符串。在不完整数字的情况下,它使用 rgxSave=_rgxSaveChanged

Regex _rgxSaveChanged = new Regex( @"[-+]?\d*(\.?)(\d*)([eE][-+]?\d*)?", RegexOptions.ECMAScript );

正则表达式,在完整数字的情况下,它使用 rgxSave=_rgxSaveLost

Regex _rgxSaveLost = new Regex( @"[-+]?\d*(\.?)(\d+)([eE][-+]?\d+)?", RegexOptions.ECMAScript );

正则表达式

longestValidSubstr = rgxSave.Match( text ).Value;
图. 6: IsTextValid 方法的摘录 - 文件 NumericalBox.cs, 项目 NumericalBoxCtrl

_rgxSaveChanged_rgxLostChanged 都没有 ^ 和 $ 锚点,用于从文本中间提取有效子字符串。

IsTextValid 方法还通过 Regex.Match 方法验证文本,其中 rgxValid 在“已更改”验证和“焦点丢失”验证中分别等于 _rgxChangedValid_rgxLostValid

return !string.IsNullOrEmpty( text == null ? null : text.Trim( ) ) ? 
            rgxValid.IsMatch( text ) : 
                false;
图. 7: IsTextValid 方法的摘录 - 文件 NumericalBox.cs, 项目 NumericalBoxCtrl

使用代码

尝试演示NumericalBoxCtrl.exe

现在NumericalBox允许格式正确的数字

图. 2: 格式正确的数字

并在 LostFocus 事件后尝试提取畸形数字的有效部分

图. 3: 畸形数字

图. 4: 畸形数字得到纠正

 

NumericalBox.cs 可以在任何需要的地方使用,代替 TextBox。

历史

2015-09-09 修复了指向图片的错误引用

2015-09-13 修正了语法和拼写错误;修复了代码中的一些错误

 

© . All rights reserved.