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





4.00/5 (6投票s)
对原始 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
目前,这是一个简单的 TextBox
。IntValue
属性返回 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
的内容来计算其他项目,这将非常有用。在我的案例中,一旦在我的应用程序中触发了该事件,我就会使用内容来计算总成本并更新表单上的标签。
结论
我发现的唯一问题是,您无法进行负数运算,因为负号是一个运算符。但就我的目的而言,我们无需进行负数运算,也无需使用负操作数进行运算。我们的成本只会是正数,如果我们进行任何运算,我们都会从其他金额中减去数字。所有这些都可以使用此代码实现。
这就是您所拥有的。简单易于改进。如果您想使用提供的类文件,只需将其包含在您的项目中并编译您的项目。然后,新控件将可在工具箱中使用。
未来添加项
- 粘贴(验证粘贴文本的内容)
防止重复小数点(11 月 27 日更新)- 设置小数位数
- 验证
DefaultValue
属性(确保它是数字!) - 确保第二个操作数不仅仅是小数点、千位分隔符或空格,然后再进行计算
历史
- 2014 年 11 月 27 日:初始版本
- 2014 年 11 月 27 日:添加了重复小数点验证
- 2014 年 11 月 28 日:添加了
TotalValidated()
事件,创建时将 multiline 设置为false
,处理用户在未完成操作时切换到其他控件的情况(版本 2,已上传)