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

仅数字的TextBox(使用C#中的正则表达式)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (12投票s)

2015 年 5 月 24 日

CPOL

11分钟阅读

viewsIcon

68716

downloadIcon

671

本文将介绍如何使用正则表达式(Regex)将文本框的内容限制为数字。

引言

本文源于我在遇到一篇由 Jan Martin 撰写的早期版本(现已删除)的文章时产生的想法。

https://codeproject.org.cn/Articles/887085/Simple-and-Reusable-Numbers-only-Textbox-in-Csha

我认为这是一篇很棒的文章,涵盖了字符串的字符处理。所有程序员都需要这项技能。

为此,我给他的文章打了五星,并“提高了他的正则表达式水平”。我觉得有些东西可能在翻译中丢失了。我怀疑他认为我在批评他。抱歉,Jan,那不是我的本意。我只是在为讨论增加一些想法。

我的想法是,可以使用正则表达式(Regex)来通过减少原始字符处理来实现类似的结果,让 .NET 的 System.Text.RegularExpressions 来处理所有繁重的工作。还可以推导出 Regex 模式来对 TextBox 控件或任何其他文本输入控件应用其他限制。

多年来,我处理过大量格式化的文本数据文件。Regex 一直是完成这项工作的关键。

总之……我提到过我会写一篇文章,所以(终于)它在这里了。

背景

与 Jan 的文章一样,目的是确保在 TextBox 控件中输入的字符始终符合捕获数据所需的格式(例如,整数、仅正数、货币等)。我习惯称之为“条件化”数据。

一个不同之处在于,我不会尝试容忍粘贴的字符串。粘贴的字符串有点麻烦,取决于你想提取什么。并非不可能,只是有点麻烦。这完全是关于在文本框中输入。因此,值会在您输入时不断地进行条件化。

如果 Jan 的解决方案更适合您的需求,那么这才是“适合您的选择”,我不会否定您的选择。我只是提供另一种方法。

正则表达式 (Regex)

这篇 维基百科文章(我知道,我知道……维基百科,哼。大多数领域都已随着时间推移而改进,而且这种愿望是好的。即使有些贡献可能是可疑的)对正则表达式的起源进行了合理的概述,所以我不会在这里尝试涵盖这个话题。

在最强大的时候,Regex 可能会让人望而生畏。语法可能很奇怪,难以记忆。我坦诚地承认,有时会因为弄不清楚为什么某些东西不起作用而感到困惑。Regex 绝对适合绝对的初学者。像 这个 这样的备忘单是必须的,但您总是需要弄清楚适合您所处理的 Regex 解析器的模式。 .NET 的解析器某些其他解析器不同。

在过去的几年里,我从 Google 和 Bing 搜索中获得了许多令人惊叹的 Regex 模式,它们为我处理大型、格式化的文本文件节省了无尽的麻烦,包括嵌套的大括号( {data {more data}} )或提取被引号包围的字符串( “some string” )等。对于本文,我将使模式相对简单(但有用)并逐一解释。

有三种主要的方法可以应用或操作正则表达式。这在很大程度上取决于您需要对结果做什么。

最透明(如果代码更长)的方法是

string p = "(?<Number>[0-9])"; 	// Set the pattern
Regex r = new Regex(p); 	// Initialise a Regex object with the pattern 'p'
Match m = r.Match("123hfj76l098"); // Match the Regex pattern 'p' in the string '123hfj76l098'
string firstInteger = m.Groups["Number"].Value;  // Get the found value

另一种方法

string p = "(?<Number>[0-9])"; 	 // Set the pattern
Match m = Regex.Match("123hfj76l098", p); // Initialise a Match Object passing in the string to be parsed and the pattern.
string firstInteger = m.Groups["Number"].Value; // Get the found value

还有一种方法

string firstInteger = Regex.Match("123hfj76l098", "(?<Number>[0-9])").Groups["Number"].Value;

第三种选择对于从字符串中获取单个匹配值是最有效的。但是,我需要一直处理字符串,直到找到所有匹配的字符。

因此,我可以选择第一种或第二种方法,但我将选择第一种,因为它更透明,并且我可以使用 Match 对象(m)继续进行处理。

您会注意到我没有使用“var”。这并不是说我反对使用它,我只是选择在这篇文章中非常明确。

让我们开始吧。

使用代码

TextFilter 类

下面是一个“文本过滤器”类的开头,带有一个入门方法“GetInteger”。

using System.Text.RegularExpressions;
 
namespace Filters
{
    class TextFilters
    {
        public string intPattern = "(?<Number>[0-9])";
 
        public string GetInteger(string SourceString, bool PosOnly = false)
        {
            string newNumber = string.Empty;
            if (!PosOnly)
                if (SourceString.StartsWith("-"))
                    newNumber += "-";
 
            Regex r = new Regex(intPattern);
            Match m = r.Match(SourceString);
            while (m.Success)
            {
                newNumber += m.Groups["Number"].Value;
                m = m.NextMatch();
            }
            return newNumber;
        }
    }
}

这是它的详细(但大多数情况下简洁)的英文解释:

  • 调用“System.Text.RegularExpressions”命名空间引用。
  • 定义一个新的命名空间“Filters”。
  • 定义一个新的类“TextFilters”。
  • 为整数 Regex 模式建立一个字符串变量。
    • 我在这里这样做,因为它是一个方便的地方来收集 Regex 模式字符串。
    • 我将在稍后解释模式本身。
  • 创建一个返回类型为“string”的公共方法“GetInteger”。
    • 我们将从 TextBox 传递一个“SourceString”参数。
    • 我们将传递一个“PosOnly”参数来确定我们是否接受负数。
      • 默认设置为“false”,因此我们不必始终提供该参数。
    • 初始化返回字符串:newNumber。
    • 如果不需要“仅正数”
      • 检查源字符串是否以负号开头。我们实际上不接受字符串中的其他任何“ - ”字符。这是有道理的。在 TextBox 中进行数学运算是另一个话题。
        • 如果确实如此,则将“-”添加到“newNumber”。
    • 初始化一个新的 Regex 对象(r),传入“intPattern”。
    • 初始化一个 Match 对象(m),使用先前创建的 Regex 对象(r),传入“SourceString”。
      • 这将获取源字符串中模式的第一个匹配项。
    • 当在源字符串中成功找到模式的匹配项时
      • 将名为“Number”的“捕获组”添加到“newNumber”。
      • 获取下一个匹配项。
    • 返回“newNumber”。

将该解释与代码进行比较,证明 C# 确实非常优雅。

那么……Regex 模式。

字符串“(?<Number>[0-9])”可以解读为:

  • (                          -  打开捕获组
  • ?<Number>       - 将捕获组命名为“Number”。
  • [0-9]                   - 定义一个范围,查找零到九之间的任何“单个”字符。
  • )                          - 关闭捕获组。

替代模式

其他数字类型呢?例如十进制数。

Decimal Pattern

在整数模式下方添加另一个模式字符串。

public static string
            intPattern = "(?<Number>[0-9])",
            decPattern = @"(?<Number>^[0-9]*\.?[0-9]*)",

此模式的含义是:

  • @                                      - 定义一个字面量字符串。对于转义的点“\。”很重要。点在 Regex 中具有特殊含义。
  • (                                      - 打开捕获组。
  • ?<Number>                  - 将捕获组命名为“Number”。
  • ^                                      - “以下模式必须出现在源字符串的开头”。允许十进制数模式出现在其他任何地方意义不大。
  • [0-9]*                          - 定义一个范围,查找任何数字(0-9)出现任意次数“0…n”(*)。
  • \.?                                 - 查找小数点(\.),允许它可选(?)。
  • [0-9]*                          - 查找(可选)小数点后的任何数字。当遇到不匹配的字符时会停止。
  • )                                     - 关闭捕获组。

因此,这应该至少允许以下内容:

  • 1234
  • 1234.5678
  • .1

我们可以捕获一个人只输入一个点(.)的情况,但这真的不是 Regex 模式的任务。它只在值被处理时才无效,而不是在输入时。

请注意,如果您移动光标并输入无效字符,您将丢失该无效字符之后的所有字符。这是模式的工作方式。请参阅下面的“SetControlText”方法来解决此问题。

让我们稍微修改一下“GetInteger”方法。

  • 将其重命名为“GetNumber”。
  • 添加另一个字符串参数(RegexPattern),它将接受 Regex 模式字符串。
  • 添加一行删除字符串中的“-”字符。
  • 这对于十进制模式比整数模式更重要,因为十进制模式强制从字符串开头进行搜索。
public string GetNumber(string RegexPattern, string SourceString, bool PosOnly = false)
{
    string newNumber = string.Empty;
    if (!PosOnly)
        if (SourceString.StartsWith("-"))
            newNumber += "-";

    SourceString = SourceString.Replace("-", string.Empty);

    Regex r = new Regex(RegexPattern);
    Match m = r.Match(SourceString);
    while (m.Success)
    {
        newNumber += m.Groups["Number"].Value;
        m = m.NextMatch();
    }

    return newNumber;
}

此代码将容忍文本开头的“-”字符,并在您键入文本框时删除任何后续不符合要求的字符。

Currency Pattern

那么,您会为货币构建什么样的模式呢?

假设我们有以下限制:

  • 我们希望删除字符串中的所有“$”符号。它们对计算没有用,文本框标签应清楚说明没有必要。
  • 我们将允许小数点前有任意数量的数字。
  • 我们只希望小数点后最多有两个数字。我们确实希望容忍整数输入(不像某些 ATM。为什么是这样?您又不能从它们那里取出硬币)。

模式将是:

@"^\$?(?<Number>[0-9]*\.?[0-9]{0,2})"

  • @                                      - 定义一个字面量字符串。再次,因为我们有一些不寻常的转义序列,对于 Regex 是必需的。
  • ^                                      - 以下内容必须出现在字符串的开头。
  • \$?                                 - 允许美元符号(\$),允许它可选(?)。“$”符号在 Regex 中有意义,如果您实际上要查找“$”字符,则必须进行转义。
  • (                  - 打开捕获组。
  • ?<Number >                  - 将捕获组命名为“Number”。
  • [0-9*                          - 定义一个范围,查找任何数字(0-9)出现任意次数“0…n”(*)。
  • \.?                                - 查找小数点(\.),允许它可选(?)。
  • [0-9]{0,2}                 - 查找(可选)小数点后的任何数字,数量在 0 到 2 之间。
  • )                                     - 关闭捕获组。

与十进制模式一样,如果您移动光标并输入无效字符,您将丢失该无效字符之后的所有字符。同样,“SetControlText”方法可以解决这个问题。

现在我们有一个类,可以通过一个方法处理至少三种不同的数字格式。这就是我坚持将我的捕获组命名为“Number”而不是“Integer”、“Double”和“Currency”的原因。

using System.Text.RegularExpressions;
 
namespace Filters
{
    public class TextFilters
    {
        public string
            intPattern = "(?<Number>[0-9])",
            decPattern = @"(?<Number>^[0-9]*\.?[0-9]*)",
            currPattern = @"^\$?(?<Number>[0-9]*\.?[0-9]{0,2})";
 
        public string GetNumber(string RegexPattern, string SourceString, bool PosOnly = false)
        {
            string newNumber = string.Empty;
            if (!PosOnly)
                if (SourceString.StartsWith("-"))
                    newNumber += "-";
 
            SourceString = SourceString.Replace("-", string.Empty);
 
            Regex r = new Regex(RegexPattern);
            Match m = r.Match(SourceString);
            while (m.Success)
            {
                newNumber += m.Groups["Number"].Value;
                m = m.NextMatch();
            }
 
            return newNumber;
        }
    }
}

一旦获得数字,您就可以对其运行其他检查,以查看它是否符合您想要放置的任何其他限制。

  • 它是否适合有符号 32 位或 64 位整数(Int32、Int64)的范围?
  • 它是否适合无符号 32 位或 64 位整数(Uint32、UInt64)的范围?
  • 它是否适合“float”或“double”的范围?
  • 它是否适合财务交易限制?

TextBox

让我们看看 TextChanged 事件的代码。

最基本地,确保整数的代码是:

private void textBox1_TextChanged(object sender, EventArgs e)
{
    TextFilters tF = new TextFilters();
    textBox1.Text = tF.GetNumber(tF.intPattern, textBox1.Text);
    textBox1.SelectionStart = textBox1.Text.Length;
}

英文

  • 初始化“TextFilters”类的实例(tF)。
  • 将 TextBox 的“Text”属性设置为通过“GetNumber”方法检索到的值。
  • 确保光标位于 TextBox 内容的末尾。

使其“静态”

如果我们的“TextFilters”类是静态的,那会更好,这样我们就可以在不一直创建实例的情况下调用它。它更像是一个“功能”类(即,“做这个”),而不是一个“事物”类(即,“创建这个”)。我们不应该需要多个“对象”实例。

该类现在看起来像这样(注意高亮的“static”修饰符以及类中添加的“public”。)

using System.Text.RegularExpressions;
 
namespace Filters
{
    public static class TextFilters
    {
        public static string
            intPattern = "(?<Number>[0-9])",
            decPattern = @"(?<Number>^[0-9]*\.?[0-9]*)",
            currPattern = @"^\$?(?<Number>[0-9]*\.?[0-9]{0,2})";
 
        public static string GetNumber(string RegexPattern, string SourceString, bool PosOnly = false)
        {
            string newNumber = string.Empty;
            if (!PosOnly)
                if (SourceString.StartsWith("-"))
                    newNumber += "-";
 
            SourceString = SourceString.Replace("-", string.Empty);
 
            Regex r = new Regex(RegexPattern);
            Match m = r.Match(SourceString);
            while (m.Success)
            {
                newNumber += m.Groups["Number"].Value;
                m = m.NextMatch();
            }
 
            return newNumber;
        }
    }
}

“TextChanged”方法可以简化:

注意:

如果您像我一样是一个懒惰的打字员,可以将“using Filters;”语句更改为“using TF = Filters.TextFilters;”,用“TF”代替完整的“TextFilters”。

private void textBox1_TextChanged(object sender, EventArgs e)
{
    textBox1.Text = TF.GetNumber(TF.intPattern, textBox1.Text);
    textBox1.SelectionStart = textBox1.Text.Length;
}

使其更智能

这一切都很好。但我真的想创建一个方法,我可以将其应用于任何 TextBox 的“TextChanged”事件方法,并且能够处理用户将光标放在文本中间进行更改的情况。

我将把这视为我文本过滤过程的一部分,并将该方法添加到“TextFilters”类中。

这是新方法:

public static void SetControlText(System.Windows.Forms.TextBox TextBoxControl, string FilteredString)
{
    int cursorPos = TextBoxControl.SelectionStart; // Get the cursor position
    string textBoxContent = TextBoxControl.Text; // Extract the current content.  Important for the following comparisons

    if (FilteredString.Length < textBoxContent.Length) // This might indicate that an invalid character was entered part way through the string
    {
        cursorPos--; // Step back one character
        textBoxContent = textBoxContent.Remove(cursorPos, 1); // Remove the offending character
        TextBoxControl.Text = textBoxContent; // Set the Text property.  This triggers a ‘recurrent’ ‘TextChanged’ event.
    }
    else
        TextBoxControl.Text = FilteredString; // For some reason this ‘appears’ to not trigger a recurrent ‘TextChanged’ event.  Go figure.

    if (cursorPos >= TextBoxControl.Text.Length) // If the cursor was at the end of the text
        TextBoxControl.SelectionStart = TextBoxControl.Text.Length;
    Else // If the cursor was ‘inside’ the text
        TextBoxControl.SelectionStart = cursorPos;
}

我使用了完全限定的“System.Windows.Forms.TextBox”,因为这是我在该类中唯一会引用“System.Windows.Forms”的地方。五五开。

“TextChanged”方法变为:

void textBox1_TextChanged(object sender, EventArgs e)
{
    string filteredText = TF.GetNumber(TF.intPattern, intTextBox.Text);
    TF.SetControlText((TextBox)sender, filteredText);
}

Filter Restrictions

还有哪些其他限制是我们可能想要应用的?

这是场景:

  • 我们有一个名为“textBoxTransaction”的控件。
  • 我们想确保交易金额在 $0.00 到 $2000.00 之间。
  • 此限制可能源于:
    • 另一个控件中的财务委派值,
    • 基于应用程序用户的查找,
    • 账户的每日交易限额,
    • 可能性是无限的。

我们的“TextChanged”事件方法可以是:

private void textBox1_TextChanged(object sender, EventArgs e)
{            
    string filteredText = TBF.GetNumber(TBF.currPattern, textBox1.Text, true);
 
    double valueTest = Convert.ToDouble(filteredText);
    if (valueTest >= 0.00 && valueTest <= 2000.00)
        TBF.SetControlText((TextBox)sender, filteredText);
    Else
        MessageBox.Show("$2000.00 transaction limit exceeded.  Please re-enter.");
}

这将阻止输入更大的值,然后再输入小数点。'true' 参数用于阻止负数输入。

我们在执行测试时确实有一个“Text”属性可能为空(“”,“”)的问题。

所以……

private void textBox1_TextChanged(object sender, EventArgs e)
{
    if (textBox1.Text != string.Empty)
    {
        string filteredText = TF.GetNumber(TF.currPattern, textBox1.Text);
        if (Convert.ToDouble(filteredText) <= 2000.00)
            TF.SetControlText((TextBox)sender, filteredText);
        else
            MessageBox.Show("$2000.00 transaction limit exceeded.  Please re-enter.");
    }
}

整合

  • 我们现在有了一个不错的 TextFilter 类,我们可以根据需要进行扩展。
namespace Filters
{
    public static class TextFilters
    {
        public static string
            intPattern = "(?<Number>[0-9])",
            decPattern = @"(?<Number>^[0-9]*\.?[0-9]*)",
            currPattern = @"^\$?(?<Number>[0-9]*\.?[0-9]{0,2})";
 
        public static string GetNumber(string RegexPattern, string SourceString, bool PosOnly = false)
        {
            string newNumber = string.Empty;
            if (!PosOnly)
                if (SourceString.StartsWith("-"))
                    newNumber += "-";
 
            SourceString = SourceString.Replace("-", string.Empty);
 
            Regex r = new Regex(RegexPattern);
            Match m = r.Match(SourceString);
            while (m.Success)
            {
                newNumber += m.Groups["Number"].Value;
                m = m.NextMatch();
            }
 
            return newNumber;
        }
 
        public static void SetControlText(System.Windows.Forms.TextBox TextBoxControl, string FilteredString)
        {
            int cursorPos = TextBoxControl.SelectionStart; // Get the cursor position
            string textBoxContent = TextBoxControl.Text;
 
            if (FilteredString.Length < textBoxContent.Length)// this might mean that an invalid character was entered part way through the string
            {
                cursorPos--; // Step back one character
                textBoxContent = textBoxContent.Remove(cursorPos, 1); // Remove the offending character
                TextBoxControl.Text = textBoxContent; // Set the string
            }
            else
                TextBoxControl.Text = FilteredString;
 
            if (cursorPos >= TextBoxControl.Text.Length) // If the cursor was at the end of the text
                TextBoxControl.SelectionStart = TextBoxControl.Text.Length;
            else
                TextBoxControl.SelectionStart = cursorPos;
        }
 
    }
}
  • 我们创建了一个窗体并将其放在上面放了一个文本框。
    • 在这种情况下,我保留了默认名称“textBox1”。
  • 我们创建了一个“TextChanged”事件方法。
  • 我们告诉方法该怎么做:
private void textBox1_TextChanged(object sender, EventArgs e)
{
    if (textBox1.Text != string.Empty)
    {
        string filteredText = TF.GetNumber(TF.currPattern, textBox1.Text, true);
        double valueTest = Convert.ToDouble(filteredText);
        if (valueTest >= 0.00 && valueTest <= 2000.00)
            TF.SetControlText((TextBox)sender, filteredText);
        else
            MessageBox.Show("$2000.00 transaction limit exceeded.  Please re-enter.");
    }
}
  • 我们的窗体代码(不包括“Designer”内容)看起来像这样:

using System;
using System.Windows.Forms;
using System.Drawing;
 
using TF = Filters.TextFilters;
 
namespace NumberTextBox
{
    public partial class Form1 : Form
    {
        #region Form Stuff
        public Form1()
        {
            InitializeComponent();
        }
 
        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            if (textBox1.Text != string.Empty)
            {
                string filteredText = TF.GetNumber(TF.currPattern, textBox1.Text, true);
                double valueTest = Convert.ToDouble(filteredText);
                if (valueTest >= 0.00 && valueTest <= 2000.00)
                    TF.SetControlText((TextBox)sender, filteredText);
                else
                    MessageBox.Show("$2000.00 transaction limit exceeded.  Please re-enter.");
            }
        }
        # endregion
 
    }
}

结论

这是一个关于正则表达式可以提供的强大功能和灵活性的简单示例,它展示了实现相当健壮的功能只需要很少的代码。

这里还有更多可以做的事情,但我需要在这里结束这篇文章。我希望它能对某处、某人有所帮助。

历史

2015 年 3 月 24 日 - 初次发布。

© . All rights reserved.