一个 C# WPF .NET 4.0 NumberBox 用户控件






4.72/5 (24投票s)
用于输入数字的控件
引言
本文介绍一个 C#/WPF .NET 4.0 数字输入控件“NumberBox
”,它允许用户输入数字。我力求使该控件尽可能灵活,并且没有过多假设其使用场景。我还竭力开发出尽可能高质量的控件。启用/禁用、调整大小、BorderThickness
以及所有其他标准控件属性都应按预期工作。“NumberBox
”允许以多种方式输入数字,以支持不同的文化。输入的数字可以使用系统格式,或者由程序员自行设置的格式覆盖。
通用概念
在这个简单的控件中,我添加了大量属性。出于实际考虑,这些属性应该可以设置为样式,这样您就可以一次性为应用程序中使用的所有控件配置所有这些属性。因此,这些都是依赖属性。
为了进一步阐明,我将这些属性分为 3 类
- 影响控件中值类型和格式的属性。我将其称为“值属性”。
- 影响用户输入方式的属性。底层值不受影响。我们将这些称为“输入属性”。
- 仅影响值显示的属性。将这些称为“显示属性”。
为了拥有一个真正国际化的控件,我使用了系统设置来处理数字。在我的 Windows 7 机器上,可以通过点击“控制面板”,然后“时钟、语言和区域”,再点击“更改日期、时间或数字格式”,最后按右下角的“其他设置…”按钮来配置这些设置。

全局属性
CtrlFlags
public byte CtrlFlags { get; set; }
其中 byte 是一个标志,由以下一个或多个值组成
public enum CtrlFlag : byte
{
AllowMain = 0x0001,
AllowFractions = 0x0002,
AllowNegatives = 0x0004,
AllowScientific = 0x0008,
AllowNegSci = 0x0010,
LoadScientific = 0x0020
}
输入属性 - 默认值为“AllowMain | AllowNegatives
”。对于科学计数法,除了设置 AllowScientific
外,还必须设置 AllowMain
标志。AllowScientific
本身无效。
Apparently it is not possible to set the flag in XMAL. See Silverlight 4 can't set a flag enum type property in XAML. However, this is not a big issue, since it can easily compute the short value and pass this directly. This is OK if you don’t change value of flags (which shouldn’t change anyway). Example: to allow only positive fractions: set to 2, to set all flags, set to 15, to allow main, negatives, and scientific notation, set to 13.
当设置 AllowNegatives
时,输入负数的方式与在计算器中输入负数相同:只需按下负号按钮(通常是减号“-”)。插入符号(caret)的位置无关紧要,只要控件获得了键盘输入焦点即可。
当设置 AllowScientific
时,可以使用科学计数法。要输入“e”或“E”,插入符号必须位于开头,或者紧跟在主数字之后。如果插入符号位于空格之后,或者当设置了 AllowFractions
标志时插入符号位于分数元素上,则无法输入“e”或“E”。“e”或“E”后面的数字只能是正整数。默认情况下,使用 AllowScientific
输入的数字在退出时会展开,从而有效地提供了输入长数字的简写。您只需键入“1.23e12
”,而不是“1230000000000
”。如果用户输入了没有数字跟随的“e”或“E”,则假定输入为“e0
”或“E0”,即 10 的 0 次方,等于 1。因此,在退出时,字段中的值为 1(仅输入了“e”或“E”),或者“e”或“E”被忽略(如果它跟在前面的数字后面)。即使 ScientificDisplayType
未设置为 ExpandOnExit
,也会发生这种情况。如果科学计数法部分的值过大无法存储在十进制数中,则将被忽略。
当设置 AllowNegSci
标志时,可以输入负数科学计数法,例如“e-9”。要做到这一点,请在插入符号位于“e”或“E”之后(并且在空格之前,如果还设置了 AllowFractions
)按下负号(通常是“-”)。如果 ScientificDisplayType
设置为 ExpandOnExit
,建议将 DecimalPlaces
设置为 -1。否则可能会显示类似“0.00”的内容(有关更多信息,请参见下面的 DecimalPlaces
部分)。仅当也设置了 AllowScientific
时才有意义。
当设置 LoadScientific
时,数字将以科学计数法加载。此标志仅在设置了 AllowScientific
时才有效。对于小数,需要同时设置 AllowNegSci
。系数中显示的数字位数由 DecimalPlaces
决定(如果 DecimalPlaces
> 0)。否则,则使用 SignificantDigits
。
当设置 AllowFractions
时,可以输入分数。这些分数始终由整数组成,不允许浮点数。有两种情况:
- 同时设置了
AllowMain
。用户必须按空格键(“ ”)来分隔分数和主数部分。如果输入了“1234
”之类的内容,则会忽略空格,并在退出时将该值视为“1234
”。如果输入了“123 12
”或“123 12/
”,则将其视为“123 12/1
”。如果输入了“12 /16
”,则将其视为“12 1/16
”。本质上,斜杠(“/”)两侧为空的部分将被视为“1”。 - 未设置
AllowMain
。不允许键入空格。同样,如果斜杠“/”两侧为空,则将其视为“1”。如果您只输入一个数字“1234
”,在退出时它将变为“1234/1
”。
总而言之,数字的输入方式如图所示:
- 橙色部分是负号部分,根据文化或设置,它可以是前缀或后缀。
- 粉色部分是主数部分。
- 浅蓝色部分是科学计数法部分。
- 浅紫色部分是一个空格。
- 浅绿色部分是分数部分。
DecValue
public decimal? DecValue { get; set; }
输入属性 - 默认值为空。此属性允许我们设置和检索主数和科学计数法部分的值。值以 decimal 类型存储。如果需要设置/获取分数部分的值,请使用下面描述的 NumValue
属性。
NumValue
public NumberValue? NumValue { get; set; }
其中
public struct NumberValue
{
public decimal Main;
public long Numerator, Denominator;
}
输入属性 - 默认值为空。此属性允许我们设置和检索所有值。主数和科学计数法部分的值存储在 NumberValue
的 Main
字段中。分子和分母分别存储在 Numerator
和 Denominator
字段中。分子与主字段具有相同的符号,这是符合逻辑的。例如:-1 3/4 = -(1 + 3/4) = -1 -3/4
设置了 AllowMain 标志时适用的属性
以下属性仅在 CtrlFlags
属性中设置了 AllowMain
标志时适用。
DecimalPlaces
public short DecimalPlaces { get; set; }
值属性 - 默认值为 2。此属性设置/获取小数点分隔符后允许的位数。如果设置为 0,则用户将无法再输入小数点分隔符。如果设置为负数,则小数点分隔符后的小数位数不再受限制。
当 DecimalPlaces
设置为负数时,显示小数位数数的规则如下:
- 当
ScientificDisplayType
设置为ExpandOnExit
时,展开值的小数分隔符后的位数等于最初输入的小数符号后的位数减去指数。如果此值小于零,则不显示小数分隔符后的任何位数,并且不显示小数分隔符本身。示例:1.234e-5,小数分隔符后的位数是 3 - (-5) = 8。另一个示例:1.67e12,显示的位数:2 - 12 = -10 < 0,因此不显示位数。 - 使用
NumValue
或DecValue
设置初始值时- 如果此值为 0,则不显示小数符号后的任何位数。
- 否则,对于值 >= 1 或 <= -1,将显示最多四位非零小数。一旦遇到零,将不再显示后面的数字。示例:12345.6789 将显示为这样。12.309 将显示为 12.3。1.234567 将显示为 1.2346
- 否则,对于 |值| < 1,计算所需小数位数 + 额外四位,遵循上述相同规则。示例:0.00000001 将显示为这样,0.0012345 将显示为这样,0.000000012345678 将四舍五入为 0.000000012346。
DecimalSeparatorType
public DecimalSeparatorType DecimalSeparatorType { get; set; }
其中
public enum DecimalSeparatorType : byte
{
System_Defined,
Point,
Comma
}
输入属性 - 默认值为“System_Defined
”。在英国或美国,小数点分隔符在数字中使用点(.),例如“pi = 3.14”。在法国,则使用逗号,例如“pi = 3,14”。此属性允许 NumberBox
覆盖系统设置。
SignificantDigits
public byte SignificantDigits { get; set; }
输入属性。对于非常小的数字,此属性表示有效数字的位数。例如,如果 SignificantDigits
设置为 3,0.00000456789 将显示为 0.00000457。默认值为 5。
GroupSeparatorType
public GroupSeparatorType GroupSeparatorType { get; set; }
其中
public enum GroupSeparatorType : byte
{
System_Defined,
Comma,
Point,
Space,
HalfSpace
}
显示属性 - 默认值为“HalfSpace
”(而不是“System_Defined
”,因为我认为“HalfSpace
”的显示效果更好)。此属性获取/设置通常用于分隔数字中 3 位数字块的符号。在美国或英国,通常使用逗号,有时也使用空格。在法国,则使用点代替逗号。“HalfSpace
”选项不是标准的系统设置,这很遗憾,因为我认为空格的间隙太宽了。
GroupSize
public short GroupSize { get; set; }
显示属性 - 默认值为 3。大数字通常分组为 3 位数字块。此属性允许更改除 3 以外的值。例如,在法国,电话号码通常显示为 2 位数字分组。

设置了 AllowNegatives 标志时适用的属性
以下属性仅在 CtrlFlags
属性中设置了 AllowNegatives
标志并且用户输入了负数时适用。
NegativeSignSide
public NegativeSignSide NegativeSignSide { get; set; }
其中
public enum NegativeSignSide : byte
{
System_Defined,
Prefix,
Suffix
}
输入属性 - 默认值为“System_Defined
”。在某些文化中,负数以负号符号作为后缀,而不是前缀。此属性允许您使用系统设置(默认),或指定负号是数字的前缀还是后缀。如果您选择“System_Defined
”,则假定为前缀(如果系统格式设置为“(1.1)”、“-1.1”或“- 1.1”)。否则,为后缀。
NegativePatternType
public NegativePatternType NegativePatternType { get; set; }
其中
public enum NegativePatternType : byte
{
System_Defined,
Symbol_NoSpace,
Symbol_Space,
Parentheses
}
显示属性 - 默认值为“System_Defined
”。负数模式可能显示不同:系统允许:“(1.1)”、“-1.1”、“- 1.1”、“1.1-”、“1.1 -”。如果您选择“Symbol_NoSpace
”,格式为“-1.1”或“1.1-”;如果您选择“Symbol_Space
”,格式为“- 1.1”或“1.1 -”;如果您选择“Parentheses
”,格式为“(1.1)”。如果选择“System_Defined
”,格式将与系统设置相同,但此属性会被 NegativeSignSide
覆盖。因此,例如,如果系统设置为“1.1 -”,并且 NegativeSignSide
设置为 Prefix
,则最终显示将是:“- 1.1”,而不是“1.1 -”。系统设置的空格将被保留。
NegativeTextBrush
public Brush NegativeTextBrush { get; set; }
显示属性 - 默认值为 null
(未设置)。此属性允许您以不同于正数的颜色显示负数。常见的做法是用红色显示负数。
NegativeSignType
public NegativeSignType NegativeSignType { get; set; }
其中
public enum NegativeSignType : byte
{
System_Defined,
Minus
}
输入属性 - 默认值为“System_Defined
”。据我所知,负数用减号(“-”)表示。有一个默认设置允许您用除“-”以外的符号表示负数。此属性允许您用减号覆盖默认设置。
NegativeSignSide = Prefix 或 System_Defined 类型为“(1.1)”、“-1.1”或“- 1.1”。 | NegativeSignSide = Suffix 或 System_Defined 类型为“1.1-”或“1.1 -”。 |
![]() |
![]() |
设置了 AllowScientific 标志时适用的属性
以下属性仅在 CtrlFlags
属性中设置了 AllowScientific
和 AllowMain
标志,并且用户在“e”或“E”后输入了数字时适用。
ScientificDisplayType
public ScientificDisplayType ScientificDisplayType { get; set; }
其中
public enum ScientificDisplayType : byte
{
CapitalE,
SmallE,
Pow10,
ExpandOnExit
}
显示属性,除了“ExpandOnExit
”选项外,后者会影响输入。默认值为“ExpandOnExit
”。
Using the Code
使用该代码应该非常简单,并且符合 XAML 标准,除了用于设置/获取控件值的属性。我在示例中提供了示例。
原型样式
<Style x:Key="NumberBoxStyle" TargetType="{x:Type local:NumberBox}">
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="BorderBrush" Value="Blue"/>
<Setter Property="Foreground" Value="Green"/>
<Setter Property="TextAlignment" Value="Left"/>
<Setter Property="CtrlFlags" Value="15"/>
<Setter Property="DecimalPlaces" Value="3"/>
<Setter Property="DecimalSeparatorType" Value="Point"/>
<Setter Property="GroupSeparatorType" Value="Space"/>
<Setter Property="GroupSize" Value="4"/>
<Setter Property="NegativeSignType" Value="Minus"/>
<Setter Property="NegativePatternType" Value="Parentheses"/>
<Setter Property="NegativeTextBrush" Value="Red"/>
<Setter Property="ScientificDisplayType" Value="Pow10"/>
</Style>
使用示例
<local:NumberBox x:Name="NumBoxSty1"
Style="{StaticResource NumberBoxStyle}"
HorizontalAlignment="Stretch"
Margin="15,30,15,0" VerticalAlignment="Top"/>
Silverlight 4 版本
我添加了一个适用于 Silverlight 4 的控件版本,我不得不单独下载,因为它没有随我的 Visual Studio 2010 版本一起提供。Silverlight 3 有太多问题,像 Foreground="{Binding Foreground, ElementName=Root}"
这样的简单 XAML 语句例如就无法正常工作。
与 WPF 相比最大的改动是用 TextBox
实例替换了所有 FormattedText
实例,因为 Silverlight 中没有 FormattedText
。
CoerceValueCallback
也不可用,并且无法重置已定义的 DependencyProperties
的默认值。这意味着您必须在 NumberBox
的客户端实例中显式设置 BorderThickness
为 1
和 Background
为 White
,否则将得到一个透明的、没有边框的 NumberBox
。例如:
<local:NumberBox x:Name="FirstNumBoxBox" BorderThickness="1" Background="White"/>
没有 PreviewKeyDown
事件,我不得不使用 KeyDown
代替。此事件并非总能在按下按钮时收到消息。例如,首次按下“Home”按钮时,没有收到消息。这意味着您无法像 WPF 版本那样系统地将插入符号设置为负号之后。我希望未来版本的 Silverlight 能够具备此功能,并保留了处理事件的函数与 WPF 版本相同。
如果 NumberBox
的高度不足以完整显示其中的字符,则在非活动状态下会显示为空白。这是因为 DisplayNumEdit
的“Text
”字段因某种原因发生了变化。
否则,所有重要功能都运行正常,WFP 版本的所有新依赖属性在 Silverlight 中也可用。行为也相同。
我认为随着互联网日益重要,拥有该控件的 Web 版本至关重要。
关注点
该控件支持复制/粘贴:无法粘贴格式不正确的字符串,并且在右键单击“NumberBox
”时,“粘贴”选项将禁用。
拖放操作也类似:无法删除格式无效的字符串。删除时,插入点可能不是用户期望的。但是,我认为对于此类控件来说,通过这种方式输入值的情况非常罕见。在任何情况下,都不应该允许输入格式错误的字符串。
该控件仅使用标准的“TextBox
”控件,因此应该支持所有主题,并且此处应该没有问题。我一直无法获取控件的标准禁用文本颜色。我使用了一个标准值 #FF6D6D6D
。不幸的是,SystemColors
似乎没有返回任何对应于我当前计算机设置的那些主题的值。
我对使用包含 Grid
、两个 TextBoxes
和 DisplayNumEdit
的用户控件感到不是特别满意,所有这些都只是为了一个控件。可能存在更轻量级的解决方案。然而,我也可能遇到重大障碍。
没有处理最小值或最大值,也没有相应的属性。如果数字太大,不会抛出异常。修改此类处理所需的代码并不难实现,但我将此更改留给使用该代码的开发人员。最佳解决方案可能是让 DecValue
和 NumValue
方法抛出错误。我不愿让任何属性抛出未捕获的异常,因为如果使用此示例的开发人员忘记了 try
…catch
块,那么很可能会因此代码受到指责。
最后,我确信您以前下载过代码并发现了很多问题。我已尽力修复了我所知的任何问题。但是,如果您碰巧发现了错误,请告知我。我也想知道潜在的改进之处。
致谢
编写此控件的一个困难之处在于猜测用户和开发者的需求。为此,我查阅了一个类似的控件的评论,该控件是旧的 MFC 版本:《Numeric Edit Control》。这给了我关于科学计数法和支持不同负数格式的想法。其他方面,分数显示、用红色显示负数、3 位数字分组之间的间距以及科学计数法中的“ExpandOnExit
”简写以输入大数字,都来自于我在一家金融软件公司工作期间进行用户界面开发时的经验。我设计了这个用户控件,以适应最大数量的需求。
历史
- 2011 年 6 月 1 日:初始版本
- 2011 年 6 月 11 日:新版本,包含以下错误修复和增强功能:
- 当未设置“
AllowNegatives
”标志时,不再可能输入负号。 - 如果未设置“
AllowFractions
”标志,则不再可能使用NumValue
属性显示分数。 - 当先前输入的负数变为正数时,
NegativeTextBrush
现在已正确禁用。 - 当
DecimalPlaces
< 0 时,“ExpandOnExit
”现在可以正常工作。 - 当
DecimalPlaces
< 0 时,“NumValue
”和“DecValue
”属性现在可以正常工作。 - 讨论了
DecimalPlaces
< 0 时的正确行为。 - 新增标志“
AllowNegSci
”,允许负指数。
- 当未设置“
- 2011 年 6 月 20 日:添加了 Silverlight 4 版本并进行了一次小错误修复:输入负数时(即具有两个负号的值)负号会被从科学计数法值中移除,导致失败。例如,“-1.234e-5”失败了。
- 2012 年 3 月 25 日:错误修复和增强。特别是,修复了“评论与讨论”部分中报告的所有问题。
DecValue
和NumValue
现在是依赖属性。这允许将它们用于DataGrid
内部。这些属性现在还接受/返回可空参数。这是为了允许设置/获取 null、空字段。NegativeTextBrush
现在也适用于NumberBox
获得焦点时(错误)。- 实现了新的
LoadScientific
标志。 - 如果在控件显示后调用
DecValue
或NumValue
,新值现在将显示(错误 - 仅 WPF)。 - 在 SilverLight 中,修复了一个问题:之前输入分数然后删除它时,会显示一个间隙(错误 - 仅 SilverLight)。
- 添加了新属性
SignificantDigits
。 - StrIsFilled 函数被 !string.IsNullOrEmpty 替换。