11 KiB
NumericUpDown 桌面版实现原理
本文档描述 NumericUpDown 桌面版的内部数值同步、string mode、模板切换、ButtonSpinner 接入和 Form / CompactSpace 集成。共享输入分层见 输入控件共享架构设计,公共设计与 API 契约见 NumericUpDown 桌面版架构设计,Token 语义见 NumericUpDown Token 设计,变化记录见 NumericUpDown Changelog。
1. 实现定位
NumericUpDown 的实现基于 Avalonia NumericUpDown,AtomUI 通过 InputControlFrame / ButtonSpinner 负责输入表面、Addon、浮动 Handle、清除按钮、string mode、Form、CompactSpace 和 spinner 展示模式。实现文档聚焦状态同步和模板接入,不重新说明 Avalonia 基类的数值算法。
2. 源码文件结构
主要源码:
src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs:public API、数值同步、custom size、string mode、清除按钮、键盘处理、Form / CompactSpace / Motion 接口。src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDownSpinner.cs:internal Spinner 子控件,复用 ButtonSpinner 的数值步进语义并承载 NumericUpDown 专用 inline 模板。src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDownToken.cs:NumericUpDown Token scope。src/AtomUI.Desktop.Controls/NumericUpDown/Themes/NumericUpDownTheme.axaml:输入模式和 spinner 模式外层模板、状态传递。src/AtomUI.Desktop.Controls/NumericUpDown/Themes/NumericUpDownSpinnerTheme.axaml:NumericUpDownSpinner 的 inline 模板、分隔线、加减按钮和按钮状态视觉。src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinner.cs:输入壳体和 spin 入口。src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinnerDecoratedBox.cs:在 shared frame 上扩展浮动 Handle、Addon、CompactSpace 和 spinner 布局。src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinnerHandle.cs:Handle 按钮和上下箭头。src/AtomUI.Desktop.Controls/Input/TextBox.cs:文本编辑器。src/AtomUI.Desktop.Controls/Input/InputClearIconButton.cs:清除按钮。
3. 核心类职责
NumericUpDown 负责把 Avalonia 数值模型与 AtomUI 输入体系连接起来。它不复制 ButtonSpinner、TextBox 或 AddOnDecoratedBox 的视觉状态 selector。
ButtonSpinner 是 NumericUpDown 的输入组合边界,复用 InputControlFrame 的外观和 EffectiveStatus,负责外部 AddOn、内部内容、浮动 Handle 和 spin 入口;它不重新声明 variant/status/error/warning/disabled。
NumericUpDownSpinner 是 NumericUpDown 专用的 internal 子控件。它以 ButtonSpinner 为基础主题,在 IsButtonSpinnerFloatable=False 时提供 inline 三段式模板,并在自己的主题中维护 PART_DecreaseButton、PART_IncreaseButton 的视觉。NumericUpDown 父主题只传递属性和内容,不进入该子控件的模板内部。
TextBox#PART_TextBox 负责文本编辑、placeholder、只读、数据校验和 disabled 文本色。NumericUpDown 不应让内层 TextBox 绘制独立边框。
4. 状态与数据流
数值同步流:
TextBox text input
↓
NumericUpDown Text / TextConverter / ParsingNumberStyle
↓
Value
↓
FormatString / NumberFormat / TextConverter
↓
display Text
String mode 同步流:
IsStringMode=true
Text changed → StringValue = raw text
StringValue changed → Text = raw text, Value = parsed decimal? or null
Value changed → StringValue = raw decimal text
输入壳体状态:
SizeType类型为CustomizableSizeType,传递给ButtonSpinner、TextBox和内部 Handle;Custom由主题按Middle默认分支处理。IsCustomFontSize通过模板接入后的 relay binding 传递给内部TextBox,只参与字号覆盖判断,不参与数值状态计算。StyleVariant、Status、FormStatus和 native validation 由 NumericUpDown 的输入适配层归一后传递给InputControlFrame,再由ButtonSpinner和 Handle 消费;NumericUpDown 不另建一套与 shared frame 并行的输入表面状态源。ShowButtonSpinner传递给ButtonSpinner.IsButtonSpinnerVisible;默认输入模式由它控制浮动 Handle,spinner 模式由它控制左右 action 段。Mode只驱动模板选择和模式专用 part 接入,不参与数值状态计算。- CompactSpace 状态只作为输入壳体集成状态,不形成公开 API。
5. 生命周期与模板接入
OnApplyTemplate 获取跨模式稳定 part:PART_Spinner、PART_TextBox、PART_ClearButton、PART_InnerRightContentPresenter。Mode=Spinner 模板中的 PART_DecreaseButton 和 PART_IncreaseButton 属于 NumericUpDownSpinner 的模板,由继承的 ButtonSpinner spin 语义接入;NumericUpDown 本体不直接持有或订阅这两个按钮。
模板替换时必须解除旧 TextBox.KeyDown、清除按钮 Click 和内容区 relay binding。运行时切换 Mode 会触发模板重建,控件本体上的 Value、Text、StringValue、Status、SizeType 和 IsCustomFontSize 等状态通过绑定保留;旧 TextBox 的焦点、光标位置和选区属于旧模板实例,不作为跨模板强契约。
SetupTemplatePartBindings 统一接入清除按钮图标、清除按钮动效、清除按钮可见性、内部右侧内容、内部右侧模板和内部 TextBox.IsCustomFontSize。这些绑定在下一次模板接入前必须通过 CompositeDisposable 释放。清除按钮和内部 TextBox 的事件订阅由成对 part setter 负责,避免在 OnApplyTemplate 中散落重复解绑逻辑。
Mode=Input 默认模板不能创建 spinner 模式左右按钮。Mode=Spinner 的按钮、分隔线和专用布局节点只在启用 spinner 模式时实例化。两个模板分支都必须消费继承的 ShowButtonSpinner,不能用写死的模板值覆盖用户设置。
6. 交互与事件处理
步进行为沿 Avalonia 基类的 OnSpin / DoIncrement / DoDecrement 语义更新 Value,并遵守 Minimum、Maximum、Increment、IsReadOnly 与 AllowSpin。
IsKeyboardEnabled=false 只拦截 Up、Down、PageUp 和 PageDown 的步进行为,不改变普通文本输入、焦点和鼠标行为。
清除按钮只在 IsAllowClear=true、IsReadOnly=false 且 Text 非空时显示。清除动作将 Value 置为 null,并通过基类文本和值同步机制更新显示状态。
Mode=Spinner 下左右按钮点击继续进入统一 spin 语义,不在 NumericUpDown 内直接执行 Value += Increment 或 Value -= Increment。
7. 内部算法与关键流程
7.1 String Mode
String mode 保存 raw text,同时尝试同步可计算 Value。内部同步必须避免 Text、Value 和 StringValue 之间递归更新。
用户设置自定义 TextConverter 时,关闭 string mode 后必须恢复用户转换器。编辑焦点在 TextBox 内时显示文本应尽量保持 raw text;失焦或显示格式刷新时才应用格式化策略。
7.2 浮动 Handle
浮动 Handle 由 ButtonSpinnerDecoratedBox 控制:
- normal 状态默认隐藏。
- pointer hover 输入壳体时显示。
- 显示时通过
HandleOpacity和HandleOffset动画进入,不改变控件整体布局尺寸。 - 内部右侧内容可产生位移,避免与 Handle 重叠。
- disabled 清除 hover 状态,并保持
HandleOpacity=0。
7.3 Spinner Mode 按需模板
Spinner mode 使用独立模板创建三段式结构。左右 action 段按输入横向 padding、标准图标尺寸和分隔线自适应宽度,不使用浮动 Handle 的固定宽度和小号箭头规则。
spinner 模式的外层 content frame 必须保持零 padding,避免与左右 action 段和中间输入段自身 padding 叠加,导致控件高度超过输入控件标准高度。
左右按钮 enabled 状态由 AllowSpin、IsEnabled、IsReadOnly 和当前值是否达到 min/max 共同决定。
左右 action 段可见性由 ShowButtonSpinner 控制;隐藏时不应占用三段式布局宽度,也不改变中间输入段的数值和文本同步语义。
7.4 Form 集成
Form 集成流:
Form.SetValue(decimal?) → NumericUpDown.Value
NumericUpDown.ValueChanged → Form value changed
Form.ClearValue() → NumericUpDown.Value = null
DataValidationErrors → InputControlFrame.EffectiveStatus
Form.ValidateStatus → FormStatus → InputControlFrame + feedback
Form 不直接使用 StringValue。需要提交原始字符串时,由业务层绑定 StringValue。
8. 资源、性能与 AOT 边界
NumericUpDown 不通过反射访问 ButtonSpinner、TextBox 或 AddOnDecoratedBox 内部状态。跨控件协同通过公开属性、稳定 template part 和接口完成。
Mode=Input 是默认路径,必须避免 spinner 模式额外节点和额外按钮事件订阅。共享视觉问题应修在 InputControlFrameTheme 或 ButtonSpinner 布局主题层;NumericUpDown 专用的 inline 按钮视觉应修在 NumericUpDownSpinnerTheme 中,不能在 NumericUpDown 主题中复制跨模板 selector。
Token 通过动态资源进入主题。NumericUpDown 不把实例状态、当前值、文本、按钮 enabled 状态或模板切换状态写入 Token。
IsCustomFontSize 只通过局部绑定影响内部 TextBox 的字号样式选择,不应引入全局资源、反射访问或跨模板缓存。
9. 维护不变量
内部重构必须保持以下不变量:
Value、Text、StringValue同步不递归、不丢失 raw text。- string mode 内部同步标志必须通过成对 helper 设置和恢复,不能在多个调用点手写进入/退出逻辑。
SizeType=Custom以Middle作为未显式覆盖时的默认视觉基线。IsCustomFontSize=true不能被内部TextBox的SizeType字体样式覆盖。IsKeyboardEnabled=false只影响步进快捷键。Mode=Input不承担 spinner 模式成本。Mode=Spinner不复制数值增减算法。ShowButtonSpinner=false同时隐藏默认输入模式浮动 Handle 和 spinner 模式左右 action 段。- NumericUpDown 本体不直接订阅 spinner 模式左右按钮事件。
- 清除按钮和
InnerRightContent顺序不变。 - 禁用态隐藏浮动 Handle,并命中 disabled 文本色。
- Filled Handle 背景来自
FilledHandleBg。 - CompactSpace 状态传递给输入壳体,不在 NumericUpDown 中另写边框折叠规则。
10. 测试与验证
验证范围:
IsStringMode开关时TextConverter保存与恢复。- Large / Middle / Small / Custom 尺寸下
ButtonSpinner、内部TextBox、Handle 和 spinner action 对齐。 IsCustomFontSize=true时用户设置的FontSize不被内部TextBox的SizeType样式覆盖。StringValue、Text、Value的双向同步和高精度 raw text 保留。IsKeyboardEnabled=false对方向键和 PageUp / PageDown 的拦截。- 清除按钮可见性和清除行为。
- Form 设置、读取、清空、native validation error 投射和扩展状态映射。
- CompactSpace 中边框厚度、圆角和位置变化。
Mode=Input默认视觉树不创建 spinner 左右按钮。Mode=Spinner下左右按钮、min/max、disabled、read-only 和AllowSpin=false状态。ShowButtonSpinner=false在Mode=Input和Mode=Spinner两种模板中都能隐藏步进入口,并支持运行时切换。- 模板切换后新 part 重新接入,旧 part 事件解绑。
- 文档改动运行
git diff --check。