自定义 DecimalBox,只接受数字和小数点
如果您需要将用户输入限制为数字或小数数据,这是一种方法!
引言
在最近我为一个评估投资物业而创建的项目中,我发现我将需要多达大约20个文本框来接受货币金额,包括购买价格、首付、税费、租金收入、杂项收入以及除了抵押贷款支付之外的许多费用。我最初在网上搜索只接受数字的控件示例,但我对找到的结果感到失望;同样,当我考虑将用户输入格式化为货币时也如此。我遇到的解决方案并没有确保用户不会犯代码无法捕捉并立即拒绝的错误,而且我找到的格式化示例虽然具有启发性,但并不完全符合我想要实现的目标。因此,我决定利用我找到的一些片段来创建一个完全符合我要求的自定义控件。其标准如下:
- 除了数字或单个小数点之外的任何字符都将被简单地拒绝为用户输入,甚至不会显示在文本框中。这样,由于用户不可能犯任何输入错误,也就不需要错误处理,并且 mercifully,没有烦人的弹出错误消息。
- 进入控件时,控件的背景颜色应改变,以便用户清楚当前所在的字段;退出控件时,背景颜色恢复。
- 由于所有用户输入都应为“货币”,我希望在离开控件时将用户输入格式化为货币格式,其中会自动添加适当的货币符号和千位分隔符,精度设置为两位小数,并且所有数字在表单上整齐对齐。
正如我在测试中发现的(结果出奇地简单,一旦我打开并运行了两个 Visual Studio 2008 实例——一个用于开发我的“DecimalBox”,另一个用于测试“DecimalBox”)——任何可能出错的地方都会出错。你会发现一些代码乍一看似乎添加了不必要的代码,或者不必要地使事情复杂化,但实际上却排除了我在测试中意外引入的未处理错误。我们将逐步解释它们。
如果你过去倾向于避开自定义控件,认为它们要么不必要,要么开发起来太耗时,我希望这里的简单代码能让你看到,从长远来看,它们实际上会节省时间,并在下次你的表单上需要像“DecimalBox”这样的东西时,缩短开发时间。C# 鼓励重用、封装和代码效率,使用自定义控件可以帮助你在自己的代码中实现这一点。就像使用 Microsoft 提供的控件可以节省你的开发和支持时间一样,一次性创建自定义控件不仅会在你当前的项目中获得回报,而且在下次你需要接受(例如)只接受小数用户输入时,以及之后的时间里,都会受益。开发自定义控件是一项很棒的技能。我希望这个简单的控件能激励你考虑开发自己的自定义控件——并将你的成功贡献给这个论坛。确实,整个社区都受益于每一次贡献,我们都会成为更好的程序员。
背景
一位审阅者问了一个很好的问题:“为什么我要用你的控件而不是数字上下计数器?”这让我开始思考为什么要使用自定义控件的含义。我的回答如下:
对于任何控件,我认为有三个相互交织的问题:功能性、可维护性和用户感知或体验。请记住,我需要20个或更多能够实现以下功能的控件:
- 接受十进制值形式的用户输入,排除任何和所有无关字符
- 我希望在离开控件时将用户输入格式化为货币,添加适当的特定文化货币符号和适当的千位分隔符。
- 我希望在进入(将用户注意力集中到相应的控件)和离开时更改背景颜色。
虽然需求a)可以通过数字上下计数器来处理,但需求b)无法实现,因为该控件只显示数字或十进制值,而不显示文本(它是一个“数字”上下计数器)。
但更重要的是——解决需求 c)——是如何管理 20 个这样相同的控件。它们应该单独管理吗,即 20 个单独的“Focus - Leave”事件处理程序,每个都包含相同的代码,用于将用户的十进制输入格式化为货币,或者调用单独类中的方法来格式化用户的输入为货币?或者,我能否创建一个自定义控件,在一个地方创建和维护该货币格式代码,并在一个、20 个、100 个或 10,000 个控件(如果需要)中显示结果?从可维护性的角度来看,并且不要忘记创建此行为的开发周期,一个单一的位置,即一个单一的自定义控件,是最合乎逻辑的选择。
此外,在开发过程中,我最初的努力是简单地添加一个“$”字符(后来,为了防止格式化异常错误的发生而检测它的存在),在测试中我发现简单的方法并不能防止或解决问题。重点是,在一个(自定义)控件上进行开发,我就可以在
我希望很清楚,将开发工作集中在一个地方,使得开发和后续维护都变得简单得多。从哲学上讲,这带来了代码的可重用性,这是 C# 语言的标志之一。从另一个哲学角度来看,这与使用 Microsoft 提供的已内置某些功能的控件并没有太大区别,就像你提出的为什么不使用已经提供的数字上下控件的问题一样,答案应该很明显:尽管它有其优点,但它并不能完全实现我希望控件实现的功能。解决方案是一个自定义控件,正如我发现的,它并不像我最初想象的那么难开发,并且允许我向我的表单添加一个完全符合我要求的控件。
用户体验也不容忽视。用户一直都在使用文本框,并习惯于在其中输入值,无论是文本还是数字。当他们看到数字上下控件时,他们会看到一个带有箭头的控件,他们必须使用这些箭头来更改显示的值。除非他们得到视觉提示,例如背景颜色变化,否则他们可能不知道可以直接输入值来更改。在我的项目中——房地产评估——值可能从一个字段的几百美元到另一个字段的数千万美元不等。显然,应该鼓励用户输入他们希望的值,而不是使用箭头键来描述或选择它。
为了进一步鼓励使用自定义控件,我还提供了几十个文本框,用户可以在其中输入文本信息、姓名、地址、费用类别等。假设我希望当用户通过我的表单进行选项卡切换时,每个这些控件的背景颜色都会发生变化?我应该为这几十个文本框的每个实例添加一个“Focus - Enter”和“Focus - Leave”事件处理程序,在进入时更改背景颜色,在离开时将其改回,还是应该创建一个内置了适当背景颜色更改的自定义 TextBox 控件,以便将该控件的实例添加到我的表单——或任何其他项目——时,它会携带我希望该控件在用户通过表单进行选项卡切换时所显示的背景颜色更改?我希望答案是显而易见的,也就是说,创建自定义控件所需的少量努力从长远来看可以节省大量的开发和维护时间,尽管在前期开发它花费了一些时间。
Using the Code
- 打开 Visual Studio 2008,点击屏幕顶部的“文件”下拉菜单,然后点击“新建”和“项目…”
- 点击“Windows Forms 控件库”,然后将其命名为“DecimalBox”。
- 在屏幕右侧的“解决方案资源管理器”中,右键单击以“.cs”结尾的条目(默认情况下为“UserControl1”)并将其删除。
- 在 Visual Studio 顶部,单击“项目”下拉菜单,然后单击“添加新项”。
- 从项目列表中,点击“自定义控件”并将其命名为“DecimalBox”;这将成为项目的命名空间名称和将要创建的 DLL 文件名。
- 点击“添加”。
- 项目屏幕将显示“添加组件…”对话框。在该句末尾,点击高亮部分“单击此处切换到代码视图”。
- 在代码视图屏幕上,您会看到命名空间行“public partial class DecimalBox : Control”。将“Control”替换为“TextBox”,这是我们将要继承的控件。
- 将以下行添加为类代码的第一行
private string textData = "";
我们稍后会解释,使用局部变量来存储用户输入可以防止一些潜在的错误。省略“ = "" ”会需要在后面添加额外的代码,因为 null 和空字符串之间存在差异。
- 切换到设计屏幕(除了第 8 步中提到的对话框外,该屏幕仍为空白),然后点击屏幕右侧属性窗口上的闪电符号以调出控件的事件窗口。
- 我们将创建四个必要的事件处理程序,如下所示:双击“Enter”事件;切换回设计页面并双击“KeyPress”事件;切换回设计页面,并双击“TextChanged”事件;最后,切换回设计页面,并双击“Leave”事件。我们现在准备编写事件处理程序。
- 要更改我们自定义控件的背景颜色,请将以下代码添加到“DecimalBox_Enter”事件处理程序中
this. BackColor = System. Drawing. SystemColors. GradientInactiveCaption;
随意将此颜色更改为您想要的任何颜色。
- 接下来,我们希望过滤用户输入,只接受数字或单个小数点,但我们也希望他们能够使用退格键或删除键来纠正他们的数据,因此将以下代码添加到“
DecimalBox_KeyPress
”事件中if (this.Text.Contains('.')) { if ( !char. IsControl ( e. KeyChar ) && !char. IsDigit ( e. KeyChar ) ) e. Handled = true; } // If we don't already have a decimal point, // accept control keys, digits, OR a decimal point else if ( !char. IsControl ( e. KeyChar ) && !char. IsDigit ( e. KeyChar ) && !char. Equals ( e. KeyChar, '.' ) ) e. Handled = true;
第一个 '
if
' 语句检查DecimalBox
是否已经包含小数点;如果包含,则嵌入的 'if' 排除除数字或控制字符(即退格键或删除键)之外的任何字符。如果我们还没有小数点,'else' 语句将接受一个。由于“KeyPress
”事件在文本框控件接受任何字符或将其字符添加到其文本属性之前发生,我们可以使用“e.Handled = true
”语句拒绝我们不接受的任何字符。 - 由于我们接受的每个字符都会添加到“
this.text
”中,我们希望确保它也添加到我们的局部变量中,因此我们将以下代码行添加到“DecimalBox_TextChanged
”事件处理程序中textData = this. Text;
将此代码添加到“
KeyPress
”事件中似乎也能达到同样的效果,但是,由于字符在“KeyPress
”事件处理之前不会添加到控件的文本属性(“this.text
”)中,因此您总会比用户输入的字符少一个。 - 最后,我们格式化用户的输入为货币,并通过将以下代码添加到“
DecimalBox_Leave
”事件处理程序中,明确地将背景颜色重置为原始颜色if ( textData != "" ) { // we'll need to flag unexpected characters bool formattedAlready = false; // Any form using this control might // programmatically alter the string data by // formatting it or adding characters that // would cause an unexpected error to occur // when the currency format code attempted to // format it. So, check the string for any // chars other than digits or a decimal point... foreach ( char item in textData ) { if ( !char. IsDigit ( item ) && !char. Equals ( item, '.' ) ) formattedAlready = true; } // setting the bool var to true if true // if no unexpected characters were found, if ( formattedAlready == false ) { // format the string as currency... this. Text = ( Convert. ToDecimal ( textData ) ). ToString ( "C" ); } // otherwise this code is skipped } this.BackColor = System. Drawing. Color. White;
由于变量被定义为"",因此无需检查 null。正如您在注释中看到的,如果尝试将字符串格式化为货币,并且字符串包含除数字或小数点之外的任何字符,则会发生意外错误。当您以编程方式更改 DecimalBox 的文本然后对其进行格式化,并且用户随后通过选项卡进入并离开该 DecimalBox 时,这种情况随时都可能发生。
在自定义控件级别一次性格式化文本,简化了使用此控件的任何表单的代码,但需要在表单级别将数据解析为十进制数据,以便在计算中使用。同样,通用例程通过不将其绑定到任何特定文化来简化此过程,因此 DecimalBox 控件具有普遍适用性。您首先需要添加一个“using”语句来访问必要的解析方法,如下所示:
using System. Globalization;
然后,以下解析方法将为您完成繁重的工作,避免了手动解析字符串以删除货币符号和逗号或其他特定于任何货币或文化的千位分隔符的需求,purchasePrice 是我在类级别声明的十进制变量。
purchasePrice = decimal. Parse ( purchasePriceDecimalBox. Text, NumberStyles. Currency );
显然,该控件名为
purchasePriceDecimalBox
。
using System;
using System. Linq;
using System. Windows. Forms;
namespace DecimalBox
{
public partial class DecimalBox : TextBox
{
private string textData = "";
public DecimalBox ( )
{
InitializeComponent ( );
}
protected override void OnPaint ( PaintEventArgs pe )
{
base. OnPaint ( pe );
}
// On getting the focus change the back-color
private void DecimalBox_Enter ( object sender, EventArgs e )
{
this. BackColor = System. Drawing. SystemColors. GradientInactiveCaption;
}
// reject all but digits and the decimal point keys
private void DecimalBox_KeyPress ( object sender, KeyPressEventArgs e )
{
// If we already have a decimal point in the string,
// only accept control keys or digits
if (this.Text.Contains('.'))
{
if ( !char. IsControl ( e. KeyChar ) &&
!char. IsDigit ( e. KeyChar ) )
e. Handled = true;
}
// If we don't already have a decimal point,
// accept control keys, digits, OR a decimal point
else
if ( !char. IsControl ( e. KeyChar ) &&
!char. IsDigit ( e. KeyChar ) &&
!char. Equals ( e. KeyChar, '.' ) )
e. Handled = true;
}
// copy the amount to the local variable
private void DecimalBox_TextChanged ( object sender, EventArgs e )
{
textData = this. Text;
}
private void DecimalBox_Leave ( object sender, EventArgs e )
{
if ( textData != "" )
{ // we'll need to flag unexpected characters
bool formattedAlready = false;
// Any form using this control might
// programmatically alter the string data by
// formatting it or adding characters that
// would cause an unexpected error to occur
// when the currency format code attempted to
// format it. So, check the string for any
// chars other than digits or a decimal point...
foreach ( char item in textData )
{
if ( !char. IsDigit ( item ) &&
!char. Equals ( item, '.' ) )
formattedAlready = true;
} // setting the bool var to true if true
// if no unexpected characters were found,
if ( formattedAlready == false )
{ // format the string as currency...
this. Text = ( Convert. ToDecimal ( textData ) ).
ToString ( "C" );
} // otherwise skip this code
}
this.BackColor = System. Drawing. Color. White;
}
}
}