Files

11 KiB
Raw Permalink Blame History

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。