WinUI NumberBox 控件实战指南:数值输入、表达式计算、步进与格式化
【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml
导读
本文基于本仓库的 NumberBox 设计规范 与 控件实现源码,系统讲解 WinUI 3 中NumberBox控件的完整用法:从基础的数值绑定、Header/PlaceholderText 标签,到增量步进(SpinButton)、内联表达式求值、输入验证与区域感知的数字格式化。读完本文,你将掌握 NumberBox 全部核心属性(Value、SmallChange、LargeChange、ValidationMode、AcceptsExpression、NumberFormatter等)的语义、默认值与底层实现行为,并能直接写出可运行的 XAML/C# 代码。
一、背景:为什么需要 NumberBox
XAML 自带的TextBox虽可承担文本输入,但纯数字输入场景往往需要更贴合的交互:上下按钮步进、鼠标滚轮调值、键盘方向键增减、甚至直接输入算式求值。本仓库中的NumberBox正是为此设计的专用数值控件。
按 规范 的说明,NumberBox作为 WinUI 包 的一部分随包发布,而不是作为 Windows 操作系统的一部分,因此它能够在所有支持 WinUI 3 的目标平台上提供一致体验。
它"表示一个可用于显示和编辑数字的控件",支持校验、增量步进,并能计算基本的数学表达式(乘、除、加、减)。
二、这是不是你要找的控件?
规范的 "Is this the right control?" 一节给出了清晰的选型指南:
| 场景 | 应使用的控件 |
|---|---|
| 捕获、展示数学/数值输入 | NumberBox |
| 需要接受非数字内容的可编辑文本框 | TextBox |
| 密码等敏感输入 | PasswordBox |
| 搜索词输入 | AutoSuggestBox |
| 格式化富文本输入/编辑 | RichEditBox |
三、快速开始:创建一个 NumberBox
最基本的用法是绑定Value属性。规范建议使用x:Bind(而非传统Binding)以保证界面与数据同步,这一点在源码中也有印证:
<NumberBox Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay}" />Value 与 Text 的语义
从 NumberBox.idl 可以看到Value与Text两个关键属性的默认值差异:
| 属性 | 默认值(IDL 标注) | 说明 |
|---|---|---|
Value | quiet_NaN() | 未设置数值时为 NaN |
Text | 空字符串 | 与 TextBox 一致的文本层 |
源码层面有几个值得注意的行为(NumberBox.cpp):
- 当 NumberBox 被清空时,
Value会被设置为NaN,以表示"当前没有数值"; Value的 setter 对NaN做了特殊处理:当新旧值都为 NaN 时不触发赋值,这是为了避免nan != nan在x:Bind双向绑定下造成栈溢出;- 在初始化阶段
Value会覆盖Text;初始化之后,两者任一变化都会传播到另一个(见OnTextPropertyChanged与UpdateTextToValue)。
规范建议:程序化修改走 Value
规范的 Recommendations 一节明确建议:通过Value属性进行程序化赋值。虽然 NumberBox 继承了 TextBox 的Text属性,但 NumberBox 本身只接受数字与算式,持续通过Value修改可以避免"NumberBox 能接受非数字字符"的误解。
四、给 NumberBox 加标签:Header 与 PlaceholderText
当 NumberBox 的用途不直观时,可以用Header或PlaceholderText说明:
<NumberBox Header="Enter a number:" Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay}" />Header无论 NumberBox 是否有值都可见。PlaceholderText则显示在控件内部,仅在Value为 NaN 或用户清空输入时出现:
<NumberBox PlaceholderText="1+2^2" Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay}" />源码中Header支持字符串与HeaderTemplate(UpdateHeaderPresenterState):只有设置了非空字符串 Header 或提供 HeaderTemplate 时才显示 Header 区域,这一"尽量晚加载"的策略同时服务于轻量化样式(lightweight styling)。当 Header 不是字符串时,NumberBox 自身的 UIA Name 会被转发给内部 TextBox 作为辅助功能名称。
五、增量步进:SmallChange / LargeChange 与 SpinButton
触发步进的方式
规范明确了SmallChange与LargeChange的触发场景:
- SmallChange:NumberBox 获得焦点时,通过滚动滚轮、按 ↑/↓ 方向键,每次增减
SmallChange; - LargeChange:NumberBox 获得焦点时,按PageUp/PageDown键,每次增减
LargeChange。
对应的键盘处理见 OnNumberBoxKeyDown:Up → StepValue(SmallChange)、Down → StepValue(-SmallChange)、PageUp → StepValue(LargeChange)、PageDown → StepValue(-LargeChange);滚轮处理见 OnNumberBoxScroll,且滚轮步进要求控件处于焦点状态,以免在可滚动表面(如 ScrollViewer)上降低使用体验。
默认值(IDL):SmallChange = 1,LargeChange = 10。
SpinButtonPlacementMode:三种呈现方式
SpinButtonPlacementMode决定上下步进按钮的呈现方式,枚举定义见 NumberBox.idl:
| 枚举值 | 行为 |
|---|---|
Hidden | 不显示按钮(默认值) |
Inline | 按钮显示在控件旁边 |
Compact | 按钮以 Flyout 形式在获得焦点时浮出 |
Inline 模式示例:
<NumberBox Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay}" SmallChange="10" LargeChange="100" SpinButtonPlacementMode="Inline" />Compact 模式示例:
<NumberBox Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay}" SmallChange="10" LargeChange="100" SpinButtonPlacementMode="Compact" />从源码看,UpdateSpinButtonPlacement 通过视觉状态切换三种形态:SpinButtonsCollapsed、SpinButtonsVisible、SpinButtonsPopup;Compact 模式的弹出由 OnNumberBoxGotFocus / OnNumberBoxLostFocus 控制Popup.IsOpen。
按钮禁用逻辑
规范指出:当再步进一步会越过Maximum/Minimum时,对应按钮会被禁用。源码 UpdateSpinButtonEnabled 实现了这一点:当value < Maximum()时启用加按钮,value > Minimum()时启用减按钮;若启用了IsWrapEnabled(环绕)或ValidationMode != InvalidInputOverwritten,则两个按钮始终启用。
IsWrapEnabled:环绕步进
IsWrapEnabled(默认false)改变步进到边界时的行为——不再停在 Minimum/Maximum,而是环绕。规范给出示例:Minimum=0, Maximum=100, SmallChange=5, Value=98, IsWrapEnabled=True时,向上步进一步得到Value=3。实现见 StepValue:newVal > max → newVal = min,newVal < min → newVal = max。
六、表达式计算:AcceptsExpression
将AcceptsExpression设为true(默认false),NumberBox 即可求值基本的内联表达式,如乘法、除法、加法、减法,遵循标准运算优先级:
<NumberBox Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay}" AcceptsExpression="True" />求值触发时机:失去焦点或按下 Enter 键。表达式求值完成后,原始表达式形式不会被保留(文本会被替换为计算结果)。
底层求值引擎
NumberBox 内建了一个独立的中缀表达式解析器 NumberBoxParser,其求值流程为"词法分析 → 中缀转后缀 → 后缀求值"三步:
- GetTokens:词法分析。跳过空白,识别数字、运算符
+ - * / ^与左右括号,非法字符或括号不匹配时返回空向量(解析失败); - ConvertInfixToPostfix:调度场算法(shunting-yard),中缀转后缀;
- ComputePostfixExpression:后缀求值。除法时除数为 0 返回 NaN(对应 V2 规划中的 "Division by 0 unsupported" 提示),幂运算使用
std::pow。
优先级由 GetPrecedenceValue 定义,与规范 Remark 完全一致:
| 优先级(高→低) | 运算符 |
|---|---|
| 3 | ^(幂) |
| 2 | *、/ |
| 1 | +、- |
括号可覆盖上述优先级。规范同时注明 NumberBox 使用中缀记法,合法字符集为[ 0-9()+-/* ]及^。
测试用例佐证
仓库的交互测试 BasicExpressionTest 覆盖了大量边界表达式,可作为能力清单直接参考:
"5 + 3" → 8 "9 - 2 * 6 / 4" → 6 "9 - -7" → 16 "9-3*2" → 3 // 无空格 " 10 * 6 " → 60 // 多余空格 "10 /( 2 + 3 )" → 2 // 括号 "5 * -40" → -200 // 一元负号 "3 * ((4 + 8) / 2)" → 18 // 嵌套括号 "2 - 2 ^ 3" → -6 // 幂优先于减 "2 ^ 2 ^ 2 / 2 + 9" → 17 // 结合性与优先级 "5 ^ -2" → 0.04 // 负指数 "(-9)" → -9 "0^0" → 1同一测试还验证了AcceptsExpression=false时输入5 + 3不会求值(结果保持原值)。
七、输入验证:ValidationMode
ValidationMode是一个两值枚举(NumberBox.idl):
| 枚举值 | 行为 |
|---|---|
InvalidInputOverwritten | 在失焦或按 Enter 触发求值时,将既非数字也非合法算式的非法输入覆盖为上一次合法值 |
Disabled | 不做自动验证,允许开发者自行实现自定义校验 |
<NumberBox Header="Quantity" Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay}" ValidationMode="InvalidInputOverwritten" />验证的底层实现
验证核心在 ValidateInput:
- 文本为空 →
Value设为 NaN; - 非空 → 若
AcceptsExpression为 true 则走NumberBoxParser::Compute,否则用NumberFormatter(其本身须是INumberParser)解析纯数字;解析失败且ValidationMode == InvalidInputOverwritten时回写上一次合法值(UpdateTextToValue)。
此外,Minimum/Maximum越界也属于验证范畴:CoerceValue 在InvalidInputOverwritten模式下会把越界值强制收敛到边界内;而 CoerceMinimum/CoerceMaximum 保证Minimum <= Maximum的约束始终成立。
关于小数点和逗号
规范特别说明:用户输入中使用的小数点/逗号格式,会被 NumberBox 配置的格式化规则替换,且不会触发输入验证错误(这正是 NumberFormatter 同时充当 INumberParser 的原因,见下节)。
八、格式化输入:NumberFormatter
数字格式化由Windows.Globalization.NumberFormatting命名空间提供,配置一个格式化类实例并赋给NumberFormatter属性即可。可用的格式化类包括:DecimalFormatter(小数)、CurrencyFormatter(货币)、PercentFormatter(百分比)、SignificantDigitsNumberRounder(有效数字)等。取整行为同样由格式化属性决定。
规范示例:使用DecimalFormatter让值显示为 1 位整数 + 2 位小数,并按 0.25 的增量向上取整:
<NumberBox x:Name="FormattedNumberBox" Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay}" />private void SetNumberBoxNumberFormatter() { IncrementNumberRounder rounder = new IncrementNumberRounder(); rounder.Increment = 0.25; rounder.RoundingAlgorithm = RoundingAlgorithm.RoundUp; DecimalFormatter formatter = new DecimalFormatter(); formatter.IntegerDigits = 1; formatter.FractionDigits = 2; formatter.NumberRounder = rounder; FormattedNumberBox.NumberFormatter = formatter; }两个关键实现约束
- 必须同时实现 INumberParser:
NumberFormatter的赋值回调 ValidateNumberFormatter 会校验该对象是否也能try_as<INumberParser>,否则抛出E_INVALIDARG。这是为了保证"格式化"与"解析"使用同一套区域规则,从而让用户输入与显示格式保持一致; - 区域感知默认值:构造函数 GetRegionalSettingsAwareDecimalFormatter 会基于当前用户区域设置创建默认的
DecimalFormatter(IntegerDigits=1, FractionDigits=0),并兼容处理区域名中的排序后缀(下划线后的字符被裁剪)。
显示舍入
UpdateTextToValue 在把Value渲染为文本时,先用m_displayRounder(SignificantDigits=10的显示用 rounder)做一次舍入,以避免浮点精度带来的尾数显示问题,然后再交给NumberFormatter().FormatDouble输出。
九、输入范围与辅助功能
InputScope
NumberBox 默认使用Number输入范围(面向 0-9 数字),由 SetDefaultInputScope 在构造时设置。规范注明:开发者可以覆盖该值,但其他 InputScope 类型不会被显式支持。IDL 中InputScope属性标注为[MUX_PREVIEW](预览性质)。
键盘导航
规范附录给出了完整的 Tab 停靠顺序行为:
| 状态 | 动作 |
|---|---|
| 焦点在 NumberBox 之前的 Tab 项 | Tab 将焦点移入 NumberBox 的可编辑文本框 |
| 焦点在可编辑文本框 | Tab 触发求值;若有校验错误则移焦到错误消息,否则移到减号 SpinButton(若可见)或移出控件到下一 Tab 项 |
| 焦点在校验错误消息 | Tab 移到减号 SpinButton |
| 焦点在减号 SpinButton | Tab 移到加号 SpinButton |
| 焦点在加号 SpinButton | Tab 移出 NumberBox 到下一 Tab 项 |
另外,Enter 触发求值、Escape 回写上一次合法文本(OnNumberBoxKeyUp)。
讲述人(Narrator)
- 焦点进入文本框:朗读
AutomationProperty.Name、Header、Text属性; - 触发求值:播报求值结果;
- 返回校验错误消息:播报错误消息;
- 焦点移到加减按钮:播报按钮属性名。
源码层面,ReevaluateForwardedUIAProperties 会把 NumberBox 的 Name/Header(以及非字符串 Header 时的 LabeledBy)转发给内部 TextBox;当设置了Minimum/Maximum时,还会将边界值拼接到 UIA Name 中,帮助辅助功能用户了解取值范围。ValueChanged也会同步通过 NumberBoxAutomationPeer 触发 UIA 的数值变化事件。
Gamepad
- 空间导航可在 SpinButton、文本框与控件外部之间移动焦点;
- 文本框内按 A 进入输入模式(退出输入模式时触发计算),按 B 触发求值并退出输入模式;
- 焦点在加减按钮时按 A 执行对应的步进动作。
十、API 速览
枚举与事件(NumberBox.idl)
enum NumberBoxSpinButtonPlacementMode { Hidden, Compact, Inline }; enum NumberBoxValidationMode { InvalidInputOverwritten, Disabled }; runtimeclass NumberBoxValueChangedEventArgs { Double OldValue{ get; }; Double NewValue{ get; }; };ValueChanged事件携带新旧值(NumberBoxValueChangedEventArgs),当Value变化(且新旧值不同、非"两个 NaN")时触发,见 OnValuePropertyChanged。
属性总表
| 属性 | 默认值 | 用途 |
|---|---|---|
Value | NaN | 当前数值(Double) |
Minimum/Maximum | -max double/max double | 取值范围边界,用于校验与按钮禁用 |
SmallChange | 1 | 方向键/滚轮/步进按钮的单次增减量 |
LargeChange | 10 | PageUp/PageDown 的单次增减量 |
Header/HeaderTemplate | 空 | 标签及其模板 |
Text | 空 | 文本表示,与 Value 互相传播 |
PlaceholderText | 空 | 无值时的占位提示 |
ValidationMode | InvalidInputOverwritten | 输入验证模式 |
AcceptsExpression | false | 是否启用算式求值 |
SpinButtonPlacementMode | Hidden | 步进按钮呈现方式 |
IsWrapEnabled | false | 步进到边界是否环绕 |
NumberFormatter | 区域感知 DecimalFormatter | 数值格式化/解析器 |
InputScope(预览) | Number | 软键盘输入范围 |
十一、最佳实践小结
- 绑定用
Value而非Text:Value是数值层,Text是字符串层,两者会自动同步;程序化改动统一走Value,可避免混淆; - 需要算式能力时显式开启
AcceptsExpression,并注意表达式求值后原始文本会被结果覆盖; - 越界控制依赖
Minimum/Maximum+InvalidInputOverwritten,若需自定义校验可将ValidationMode设为Disabled; - 自定义格式化时务必使用同时实现
INumberParser的格式化类(如DecimalFormatter系),否则赋值会抛异常; - 步进与滚轮交互仅在获得焦点时生效,在可滚动容器内嵌套 NumberBox 时这是预期行为,不会抢占父级滚动;
- 规范中列为V2 规划(当前尚未实现)的能力包括:
ValidationMode扩展出TextBlockMessage/IconMessage消息模式(依赖 WinUI Input Validation 工作)、以及拖拽(drag)步进交互。
十二、深入仓库
- 设计规范:specs/NumberBox/NumberBox.md
- 接口定义(默认值/预览属性):controls/dev/NumberBox/NumberBox.idl
- 控件实现:controls/dev/NumberBox/NumberBox.cpp、controls/dev/NumberBox/NumberBox.h
- 表达式引擎:controls/dev/NumberBox/NumberBoxParser.cpp、controls/dev/NumberBox/NumberBoxParser.h
- 默认模板与主题资源:controls/dev/NumberBox/NumberBox.xaml、controls/dev/NumberBox/NumberBox_themeresources.xaml
- 交互测试(含大量表达式用例):controls/dev/NumberBox/InteractionTests/NumberBoxTests.cs
- API 测试:controls/dev/NumberBox/APITests/NumberBoxTests.cs
- 手工验证页面:controls/dev/NumberBox/TestUI/NumberBoxPage.xaml
【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考