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

VB.NET 中的基本计算文本框

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (6投票s)

2014 年 11 月 27 日

CPOL

8分钟阅读

viewsIcon

51599

downloadIcon

1466

对原始 TextBox 的简单扩展,允许进行简单计算(+、/、*、-)

概述

这是对 System.Windows.Forms.TextBox 组件的一个简单扩展。只允许以下按键:数字、运算符(+,-,*,/)、Escape、Enter、Backspace、小数点、千位分隔符。这是对 MSDN 上的数字文本框的改编。

背景

我正在处理一个项目,其中我们需要为项目添加成本。起初,我想将输入限制为数字和其他所需的按键(退格、小数点、负号)。但有时,我们的成本涉及一些基本计算。我最不想做的就是让用户拿出计算器(就像我一样,大多数人根本没有计算器,或者懒得拿出来,直接使用 Windows 计算器)。我找了一圈,什么都没找到。所以我想,我不是唯一一个会从这段简单的代码中受益的人。

代码

首先,让我们创建我们的 <CalcTextBox>

Imports System.Globalization
Public Class CalcTextBox

    Inherits TextBox

    Private valDefault As String = "0.00"  ' Starting value, can be changed to ""
    Private SpaceOK As Boolean = False               ' Allow spaces or not

    Private Event TotalValidated()         ' See notes below

    Public Property DefaultValue As String
        Get
            Return valDefault
        End Get
        Set(value As String)
            valDefault = value
        End Set
    End Property

    Public ReadOnly Property IntValue() As Integer
        Get
            Return Int32.Parse(Me.Text)
        End Get
    End Property

    Public Property AllowSpace() As Boolean

        Get
            Return Me.SpaceOK
        End Get
        Set(ByVal value As Boolean)
            Me.SpaceOK = value
        End Set
    End Property
    
    Public Sub New()
        Me.Text= valDefault
        Me.TextAlign= HorizontalAlignment.Right
    End Sub

End Class

目前,这是一个简单的 TextBoxIntValue 属性返回 TextBox 的整数部分。DefaultValue 属性允许将默认的 TextBox 值更改为您决定的任何数字。设置默认值可以清楚地告诉用户他们应该输入什么。此外,当用户按下“Escape”键时,TextBox 将重置为此值。AllowSpace 属性取自原始 MSDN 文档,我没有费心去更改它。有些语言使用空格分隔千位,因此这允许这样做。

New() sub 确保我们在创建时将 DefaultValue 放入 TextBox (在设计模式下也可见)。它还将文本右对齐(这也可以作为一个属性来实现,但我认为,因为这是一个计算器类型的 TextBox,右对齐是最好的)。

控件获得焦点时选择内容

现在,我还希望当控件获得焦点时,textbox 的内容被选中。这通过两个方法完成:OnGotFocus (当选项卡停止时发生)和 OnMouseUp (当用户单击控件时)。在这样做时,我们不希望每一次鼠标单击都导致选中。因此,我们将使用一个标志,在控件已获得焦点时将其设置为(alreadyFocused)。我从 Tim Murphy 那里剪切了这段代码(参见 此页面上的第二个答案)。

    Private alreadyFocused As Boolean                       ' Self explanatory

    Protected Overrides Sub OnMouseUp(ByVal mevent As MouseEventArgs)
        MyBase.OnMouseUp(mevent)
        ' This event selects the whole text on mouseup if the control doesn't already have focus
        If Not Me.alreadyFocused AndAlso Me.SelectionLength = 0 Then
            Me.alreadyFocused = True
            Me.SelectAll()
        End If

    End Sub

    Protected Overrides Sub OnLeave(ByVal e As EventArgs)
        If Not calculated Then
            ' Calculation underway but not complete
            ' Reset to original value (or last calculated value)
            ' Reset valOperator and calculated, then Beep
            ' Raise TotalValidated event (v2)
            Me.Text = valOriginal
            calculated = True
            valOperator = Nothing
            Beep()
        End If
        RaiseEvent TotalValidated()
        MyBase.OnLeave(e)
        Me.alreadyFocused = False
    End Sub

    Protected Overrides Sub OnGotFocus(e As EventArgs)
        MyBase.OnGotFocus(e)
        ' This event selects the whole text on tab stop if the control doesn't already have focus
        If MouseButtons = MouseButtons.None Then
            Me.SelectAll()
            Me.alreadyFocused = True
        End If

    End Sub

有趣的部分!

好的,现在我们有了一个具有一些附加功能的 TextBox,但它并没有做太多事情。下一步是过滤按键。首先,我们处理允许的按键(数字、分隔符、退格键、回车键、Escape 键等),最后,我们在一个简单的“else”语句中处理所有我们不想要的按键。让我们看看它是如何工作的。这是 OnKeyPress 子例程的完整代码。

    Private valOriginal As Double = valDefault         ' First number in any operation
    Private valCalculated As Double = valDefault       ' Final calculated value in an operation
    Private valOperator As Char = Nothing              ' +, -, /, *
    Private calculated As Boolean = True               ' False if operation is in progress,
                                                       ' True if operation is calculated

    ' Restricts the entry of characters to digits (including hex),
    ' the negative sign, the e decimal point, and editing keystrokes (backspace)
    ' as well as standard operators (+,-,*,/).
    Protected Overrides Sub OnKeyPress(ByVal e As KeyPressEventArgs)
        MyBase.OnKeyPress(e)

        Dim numberFormatInfo As NumberFormatInfo = _
            System.Globalization.CultureInfo.CurrentCulture.NumberFormat
        Dim decimalSeparator As String = numberFormatInfo.NumberDecimalSeparator
        Dim groupSeparator As String = numberFormatInfo.NumberGroupSeparator
        Dim negativeSign As String = numberFormatInfo.NegativeSign

        Dim keyInput As String = e.KeyChar.ToString()

        If [Char].IsDigit(e.KeyChar) Then
            ' Digits are OK, nothing to do

        ElseIf keyInput.Equals(decimalSeparator) OrElse keyInput.Equals(groupSeparator) Then 
            ' Decimal separator is OK, make sure we don't have one already 
            If keyInput.Equals(decimalSeparator) And Me.Text.Contains(decimalSeparator) then
                e.Handled=True
                Beep()
            End If

        ElseIf e.KeyChar = vbBack Then
            ' Backspace key is OK, nothing to do

        ElseIf Me.SpaceOK AndAlso e.KeyChar = " "c Then
            ' If spaces are allowed, nothing to do

        ElseIf e.KeyChar = Microsoft.VisualBasic.ChrW(Keys.Escape) Then
            ' Escape = reset to default values
            Me.Text = valDefault
            Me.SelectAll()
            valOriginal = 0
            valCalculated = 0
            valOperator = Nothing
            calculated = True
            RaiseEvent TotalValidated()                ' See TotalValidated notes

        ElseIf e.KeyChar = Microsoft.VisualBasic.ChrW(Keys.Return) Then
            ' Enter (proceed with calculation)
            If Not calculated then
                If CalculateTotal(e)=True then
                    ' The operation was a success
                    valOperator = Nothing
                    Me.Text = valCalculated
                    calculated = True
                    Me.SelectAll()
                End If
                RaiseEvent TotalValidated()            ' See TotalValidated notes
             End If

             e.Handled = True

        ElseIf e.KeyChar = "/"c OrElse e.KeyChar = "*"c _
        OrElse e.KeyChar = "+"c OrElse e.KeyChar = "-"c Then
            ' Operation required
            If Me.Text <> "" Then
                ' Previous text was not an operator
                If calculated = False Then
                    ' This is the 2nd operator, so we have to get the result of the first 
                    ' operation before proceeding with this one
                    Dim tmpResult as Boolean = CalculateTotal(e)    ' Result stored in valOriginal
                Else
                    ' This is the first operator, store the first operand into valOriginal
                    valOriginal = CDbl(Me.Text)
                End If
                ' Indicate that an operation is active but the total has not been calculated
                ' (2nd operand to follow)
                calculated = False
                Me.Text = ""

            End If

            valOperator = e.KeyChar         ' Store the operator before we get the 2nd operand
            e.Handled = True                ' Swallow this key 

        Else
            ' Swallow this invalid key and beep
            e.Handled = True
            Beep()
        End If

    End Sub

让我们尝试解释一下这里发生的事情。

Protected Overrides Sub OnKeyPress(ByVal e As KeyPressEventArgs)
    MyBase.OnKeyPress(e)

当用户键入任何内容时,无论是有效的按键还是无效的按键,都会触发 OnKeyPress 事件。由于我们正在重写它,所以 Sub 中的第一行确保我们调用此事件的父版本。重要的是要知道 OnKeyPress 事件发生在任何内容添加到 TextBox 之前。这很重要,因为我们想要控制输入。一些按键将被忽略,另一些将被允许通过。

Dim numberFormatInfo As NumberFormatInfo = System.Globalization.CultureInfo.CurrentCulture.NumberFormat
Dim decimalSeparator As String = numberFormatInfo.NumberDecimalSeparator
Dim groupSeparator As String = numberFormatInfo.NumberGroupSeparator
Dim negativeSign As String = numberFormatInfo.NegativeSign

接下来,我们从用户的设置中获取小数点、负号和千位分隔符。这在我居住的地方尤其有用,因为有些地方使用不同的设置(逗号作为小数点,空格作为千位分隔符——尽管后者在输入数字时从不使用,但粘贴时可能很有用)。无论如何,这只是几行代码,并且几乎使其具有通用性。

If... elseif... andif... else... endif... 这么多 ifs!!!

当用户键入数字、分隔符或按下退格键时,无需执行任何操作。我们将让 TextBox 正常工作。这些代码行就是这样做的。

        If [Char].IsDigit(e.KeyChar) Then

        ElseIf keyInput.Equals(decimalSeparator) OrElse keyInput.Equals(groupSeparator) Then 
            ' Decimal separator is OK, make sure we don't have one already 
            If keyInput.Equals(decimalSeparator) And Me.Text.Contains(decimalSeparator) then
                e.Handled=True
                Beep()
            End If

        ElseIf e.KeyChar = vbBack Then

        ElseIf M.SpaceOK AndAlso e.KeyChar = " "c Then

第一行检查按键是否为数字。接下来的块基本上工作方式相同,但查找不同的按键。第二个块查找小数点或千位分隔符,但也检查小数点是否已存在,从而防止其被输入两次。第三个块查找退格键,最后一个块查找空格键(并且仅当 SpaceOK 变量允许时)。在所有这些情况下(如果小数点已存在,则第二个情况除外),按键是允许的,因此无需执行任何操作。我们只需让过程继续。

在最后一行,您可能会注意到“ ”后面的“c”。这不是拼写错误。它只是将“ ”(一个仅包含空格的字符串)转换为 KeyChar。稍后您将在代码中看到它用于其他比较。

让我们继续处理“Escape”键。基本上,此键应将所有内容重置为默认值,包括 .Text 属性。它还应该选择所有内容,以便客户可以轻松地进行下一项操作。这是使用以下代码完成的(我们仍在同一个 if...then 子句中)。

        ElseIf e.KeyChar = Microsoft.VisualBasic.ChrW(Keys.Escape) Then
            ' Escape = reset to default values
            Me.Text = valDefault
            Me.SelectAll()
            valOriginal = 0
            valCalculated = 0
            valOperator = Nothing
            calculated = True

如果您不确定为什么将某些变量分配给这些值,请不用担心,稍后您就会明白。

让我们跳到我们巨大的 if..then 子句的最后一个“else”语句。基本上,每当按下无效按键时,逻辑就会带我们到这里。如果按键与上面提到的不匹配,那么我们就需要“吞噬”该按键,换句话说,阻止它被添加到 TextBox。您可能已经注意到,子例程在触发时会传递 <KeyPressEventArg>e”。这不仅仅是一个变量,而是一个包含函数和属性的类。那么,我们如何告诉它跳过那些不需要的按键呢?我们使用 e.Handled=True 来完成。这基本上表示我们已经处理了该按键,无需再对其进行任何操作。链中的下一个事件将不会处理它(即,负责在 TextBox 中绘制或显示字符的事件将不会绘制任何内容)。我们还将添加一个哔哔声来提醒用户犯错。这是代码。

        Else
            ' Swallow this invalid key and beep
            e.Handled = True
            Beep()
        End If

下一步

到目前为止,我们已经修改了 TextBox,使其仅响应数字(以及其他几个按键)、在获得焦点时自动选择(通过单击或选项卡停止)以及在用户按下“Escape”键时重置。

在我们进一步深入代码之前,让我们定义一下我们希望 TextBox 如何工作。用户可以键入任何数字,一旦键入其中一个运算符,我们就必须将第一个操作数存储在一个变量(valOriginal)中,并将运算符存储在另一个变量(valOperator)中。然后 TextBox 会清空,准备好让用户输入第二个数字,或操作数,来进行我们的操作。通常,当用户完成时,他会按下“Enter”键。发生这种情况时,我们将使用第一个操作数(valOriginal)、运算符(valOperator,告诉我们该怎么做)以及我们 TextBox 的当前值(非空)作为最后一个操作数来进行计算。

太简单了。如果我们连续进行多个操作怎么办?然后,我们必须在第二个操作数之后但在第二个运算符之前开始计算。例如,如果用户键入 23*1.5+5,我们希望在添加 5 之前计算 23x1.5(我们不会使用运算符优先级,至少在此版本中不会)。为了做到这一点,我们将使用一个名为 'calculated' 的变量,该变量始终为 true,除非我们捕获到运算符。当为 false 时,它将告诉我们我们已经开始了一个新操作,并且下次用户按下“Enter”键或另一个运算符键(且 TextBox.Text 值不为空,这为我们提供了第二个操作数),我们不得将数字存储在 valOriginal 变量中,而是立即进行数学运算,然后将结果存储在同一个变量中,将其传递给下一个操作。这是带有注释以帮助理解的代码。这部分代码就在我们刚刚讨论的最后一个“else”语句之前。

        ElseIf e.KeyChar = Microsoft.VisualBasic.ChrW(Keys.Return) Then
            ' Enter (proceed with calculation)
            If Not calculated Then
                If CalculateTotal(e) = True Then
                    ' The operation was a success
                    valOperator = Nothing
                    Me.Text = valCalculated
                    calculated = True
                    Me.SelectAll()
                End If
                RaiseEvent TotalValidated()
            End If

            e.Handled = True

        ElseIf e.KeyChar = "/"c OrElse e.KeyChar = "*"c _
        OrElse e.KeyChar = "+"c OrElse e.KeyChar = "-"c Then
            ' Operation required
            If Me.Text <> "" Then
                ' Previous text was not an operator
                If calculated = False Then
                    ' This is the 2nd operator, so we have to get the result of the first 
                    ' operation before proceeding with this one
                    Dim tmpResult as Boolean = CalculateTotal(e)    ' Result stored in valOriginal
                Else
                    ' This is the first operator, store the first operand into valOriginal
                    valOriginal = CDbl(Me.Text)
                End If
                ' Indicate that an operation is active but the total has not been calculated
                ' (2nd operand to follow)
                calculated = False
                Me.Text = ""

            End If

            valOperator = e.KeyChar         ' Store the operator before we get the 2nd operand
            e.Handled = True                ' Swallow this key 

接下来是 CalculateTotal 函数。

    Private Function CalculateTotal(ByRef e As KeyPressEventArgs) As Boolean
        ' This function will return True if successful otherwise False (v2)

        If calculated = False And valOperator <> Nothing And Me.Text <> "" Then
            ' Make sure we have an operation to do (calculated=false),
            ' and operator (valOperator) and a 2nd operand (Me.Text)
            Select Case valOperator
                Case "*"c
                    valCalculated = valOriginal * [Double].Parse(Me.Text)
                Case "-"c
                    valCalculated = valOriginal - [Double].Parse(Me.Text)
                Case "+"c
                    valCalculated = valOriginal + [Double].Parse(Me.Text)
                Case "/"c
                    If [Double].Parse(Me.Text) = 0 Then
                        ' Division by 0, stop everything, reset and Beep
                        Me.Text = valDefault
                        valOperator = Nothing
                        valOriginal = 0.0
                        valCalculated = 0.0
                        calculated = True
                        e.Handled = True
                        Me.SelectAll()
                        Beep()
                        Return False            ' Unsuccessful, we had to reset
                    End If
                    valCalculated = valOriginal / [Double].Parse(Me.Text)
            End Select

            valOriginal = valCalculated
            e.Handled = True                    ' Swallow this key after operation
        End If

        Return True
    End Function

您会注意到,在我们刚刚添加的两个“elseif”情况中,我们最终都会吞噬按键。这很重要,我们不希望这些按键显示在 TextBox 中。

TotalCalculated 事件

这是 2.0 版本中的一项新功能。我只是想要一种方式来得到通知,无论是当用户按下“Enter”键、内容被重置还是控件失去焦点时。如果您想使用此 CalcTextBox 的内容来计算其他项目,这将非常有用。在我的案例中,一旦在我的应用程序中触发了该事件,我就会使用内容来计算总成本并更新表单上的标签。

结论

我发现的唯一问题是,您无法进行负数运算,因为负号是一个运算符。但就我的目的而言,我们无需进行负数运算,也无需使用负操作数进行运算。我们的成本只会是正数,如果我们进行任何运算,我们都会从其他金额中减去数字。所有这些都可以使用此代码实现。

这就是您所拥有的。简单易于改进。如果您想使用提供的类文件,只需将其包含在您的项目中并编译您的项目。然后,新控件将可在工具箱中使用。

未来添加项

  1. 粘贴(验证粘贴文本的内容)
  2. 防止重复小数点(11 月 27 日更新)
  3. 设置小数位数
  4. 验证 DefaultValue 属性(确保它是数字!)
  5. 确保第二个操作数不仅仅是小数点、千位分隔符或空格,然后再进行计算

历史

  • 2014 年 11 月 27 日:初始版本
  • 2014 年 11 月 27 日:添加了重复小数点验证
  • 2014 年 11 月 28 日:添加了 TotalValidated() 事件,创建时将 multiline 设置为 false,处理用户在未完成操作时切换到其他控件的情况(版本 2,已上传)
© . All rights reserved.