# 新控件适配 Space 开发指南 本文档面向 AtomUI 控件开发者,说明如何让新控件正确工作在 `Space` 和 `CompactSpace` 中。 --- ## 1. Space(通用间距面板) ### 无需任何适配 `Space` 对子控件**没有接口要求**。任何 `Control` 都可以直接作为 `Space` 的子项: ```xml 按钮 文本 ``` Space 仅负责间距排列,不修改子控件的任何属性。 --- ## 2. CompactSpace(紧凑组合面板) ### 必须实现 ICompactSpaceAware 接口 `CompactSpace` 会在添加子控件时强制检查 `ICompactSpaceAware` 接口: ```csharp private static void EnsureCompactSpaceItem(object item) { if (item is not ICompactSpaceAware) { throw new ArgumentException($"{item.GetType().FullName} is not ICompactSpaceAware."); } } ``` 如果子控件未实现此接口,会在运行时抛出异常。 --- ## 3. 适配 CompactSpace 的完整步骤 ### 步骤 1:声明实现 ICompactSpaceAware 接口 ```csharp using AtomUI.Controls; using AtomUI.Desktop.Controls; using Avalonia; using Avalonia.Layout; public class MyControl : TemplatedControl, ISizeTypeAware, // 建议实现,以支持 SizeType 传递 ICompactSpaceAware // 必须实现 { // ... } ``` ### 步骤 2:注册 CompactSpace 相关的 StyledProperty 使用 `CompactSpaceAwareControlProperty` 的 `AddOwner` 模式注册三个内部属性: ```csharp #region CompactSpace 相关内部属性 internal static readonly StyledProperty CompactSpaceItemPositionProperty = CompactSpaceAwareControlProperty.CompactSpaceItemPositionProperty.AddOwner(); internal static readonly StyledProperty CompactSpaceOrientationProperty = CompactSpaceAwareControlProperty.CompactSpaceOrientationProperty.AddOwner(); internal static readonly StyledProperty IsUsedInCompactSpaceProperty = CompactSpaceAwareControlProperty.IsUsedInCompactSpaceProperty.AddOwner(); internal SpaceItemPosition? CompactSpaceItemPosition { get => GetValue(CompactSpaceItemPositionProperty); set => SetValue(CompactSpaceItemPositionProperty, value); } internal Orientation CompactSpaceOrientation { get => GetValue(CompactSpaceOrientationProperty); set => SetValue(CompactSpaceOrientationProperty, value); } internal bool IsUsedInCompactSpace { get => GetValue(IsUsedInCompactSpaceProperty); set => SetValue(IsUsedInCompactSpaceProperty, value); } #endregion ``` ### 步骤 3:声明 EffectiveCornerRadius 属性 这是 CompactSpace 圆角裁剪的关键——控件的主题 AXAML 中应绑定 `EffectiveCornerRadius` 而不是 `CornerRadius`: ```csharp internal static readonly DirectProperty EffectiveCornerRadiusProperty = AvaloniaProperty.RegisterDirect( nameof(EffectiveCornerRadius), o => o.EffectiveCornerRadius, (o, v) => o.EffectiveCornerRadius = v); private CornerRadius _effectiveCornerRadius; internal CornerRadius EffectiveCornerRadius { get => _effectiveCornerRadius; set => SetAndRaise(EffectiveCornerRadiusProperty, ref _effectiveCornerRadius, value); } ``` ### 步骤 4:监听属性变化并更新有效圆角 在 `OnPropertyChanged` 中监听圆角和 CompactSpace 位置/方向变化: ```csharp static MyControl() { // 确保 CompactSpace 属性变化触发重新测量/排列 AffectsMeasure(CompactSpaceItemPositionProperty, CompactSpaceOrientationProperty); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == CornerRadiusProperty || change.Property == CompactSpaceItemPositionProperty || change.Property == CompactSpaceOrientationProperty) { ConfigureEffectiveCornerRadius(); } } private void ConfigureEffectiveCornerRadius() { EffectiveCornerRadius = CompactSpace.CalculateEffectiveCornerRadius( CornerRadius, IsUsedInCompactSpace, CompactSpaceItemPosition, CompactSpaceOrientation); } ``` ### 步骤 5:实现 ICompactSpaceAware 接口方法 ```csharp void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { IsUsedInCompactSpace = position != null; CompactSpaceItemPosition = position; } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { CompactSpaceOrientation = orientation; } double ICompactSpaceAware.GetBorderThickness() { // 返回主轴方向的边框厚度,用于计算偏移量消除边框重叠 return CompactSpaceOrientation == Orientation.Horizontal ? BorderThickness.Left : BorderThickness.Top; } ``` #### 可选方法覆盖 ```csharp // 如果控件在 CompactSpace 中应始终保持高 ZIndex(如 Primary 样式) bool ICompactSpaceAware.IsAlwaysActiveZIndex() { return false; // 默认为 false;根据业务逻辑判断 } // 如果控件不应参与 Focus/Hover 的 ZIndex 管理(如纯装饰控件) bool ICompactSpaceAware.IgnoreZIndexChange() { return false; // 默认为 false } ``` ### 步骤 6:更新控件主题 AXAML 在控件的主题模板中,**使用 `EffectiveCornerRadius` 代替 `CornerRadius`**: ```xml ``` --- ## 4. 各方法的行为约定 ### 4.1 NotifyPositionChange CompactSpace 在以下时机调用此方法: - 子项集合变化(添加/删除/移动) - 方向改变触发重新计算 传入的 `SpaceItemPosition` 是 Flags 枚举: | position 值 | 含义 | 圆角策略 | |---|---|---| | `First` | 排列中的第一个 | 保留起始侧圆角 | | `Middle` | 中间位置 | 全部圆角清零 | | `Last` | 排列中的最后一个 | 保留结束侧圆角 | | `First \| Last` | 唯一的子项 | 保留全部圆角 | | `null` | 不在 CompactSpace 中 | 使用原始圆角 | **控件的响应**:将 `position` 值存入对应的 StyledProperty,后续在 `OnPropertyChanged` 中触发 `ConfigureEffectiveCornerRadius()`。 ### 4.2 NotifyOrientationChange 传入当前 CompactSpace 的排列方向。控件需要保存此值,因为圆角裁剪策略依赖方向: - 水平方向:裁剪左右两侧圆角 - 垂直方向:裁剪上下两侧圆角 ### 4.3 GetBorderThickness 返回控件在主轴方向上的边框厚度。CompactSpace 使用此值来计算偏移量,消除相邻控件的重复边框。 **注意事项**: - 如果控件无边框(如 Borderless 样式),应返回 `0.0` - 如果控件的边框厚度随样式变化,需要返回当前实际值 ```csharp double ICompactSpaceAware.GetBorderThickness() { // 如果控件有多种样式变体,按条件返回 if (StyleVariant == InputControlStyleVariant.Borderless) return 0.0; return CompactSpaceOrientation == Orientation.Horizontal ? BorderThickness.Left : BorderThickness.Top; } ``` ### 4.4 IsAlwaysActiveZIndex 当返回 `true` 时,控件在 CompactSpace 中始终保持高 ZIndex(1000)。典型场景: - `Button` 的 Primary 类型——其填充背景可能遮盖相邻控件的边框 - 有独立视觉层次的特殊控件 ### 4.5 IgnoreZIndexChange 当返回 `true` 时,CompactSpace 不会为此控件注册 Focus/Hover 事件来管理 ZIndex。典型场景: - `CompactSpaceAddOn`——纯装饰性标签,不需要交互反馈 --- ## 5. SizeType 传递 如果控件实现了 `ISizeTypeAware`,CompactSpace 会自动将自身的 `SizeType` 绑定到子控件: ```csharp // CompactSpace 中的自动绑定逻辑 if (target is ISizeTypeAware) { target[!SizeTypeProperty] = this[!SizeTypeProperty]; } ``` 因此建议同时实现 `ISizeTypeAware`,确保控件在 CompactSpace 中自动跟随尺寸档位变化。 --- ## 6. CompactSpaceSize 尺寸控制 开发者可以通过附加属性 `CompactSpace.ItemSize` 控制子项在布局中的空间分配: ```xml ``` 如果新控件需要动态改变自身在 CompactSpace 中的尺寸分配,可以在控件内部修改 `CompactSpace.ItemSize` 附加属性值。 --- ## 7. 使用 CompactSpaceFiller 占位 `CompactSpaceFiller` 用于在 CompactSpace 末尾填充剩余空间: ```xml Submit ``` **约束**: - CompactSpaceFiller 必须是最后一个子项 - 最多只能有一个 CompactSpaceFiller - 违反以上规则会抛出 `InvalidSpaceFillerUsageException` --- ## 8. 完整实现示例 以下是一个假设的 `MySearchBox` 控件适配 CompactSpace 的完整实现: ```csharp using AtomUI.Controls; using AtomUI.Desktop.Controls; using Avalonia; using Avalonia.Controls.Primitives; using Avalonia.Layout; public class MySearchBox : TemplatedControl, ISizeTypeAware, ICompactSpaceAware { #region 公共属性 public static readonly StyledProperty SizeTypeProperty = SizeTypeControlProperty.SizeTypeProperty.AddOwner(); public SizeType SizeType { get => GetValue(SizeTypeProperty); set => SetValue(SizeTypeProperty, value); } #endregion #region CompactSpace 内部属性 internal static readonly DirectProperty EffectiveCornerRadiusProperty = AvaloniaProperty.RegisterDirect( nameof(EffectiveCornerRadius), o => o.EffectiveCornerRadius, (o, v) => o.EffectiveCornerRadius = v); internal static readonly StyledProperty CompactSpaceItemPositionProperty = CompactSpaceAwareControlProperty.CompactSpaceItemPositionProperty.AddOwner(); internal static readonly StyledProperty CompactSpaceOrientationProperty = CompactSpaceAwareControlProperty.CompactSpaceOrientationProperty.AddOwner(); internal static readonly StyledProperty IsUsedInCompactSpaceProperty = CompactSpaceAwareControlProperty.IsUsedInCompactSpaceProperty.AddOwner(); private CornerRadius _effectiveCornerRadius; internal CornerRadius EffectiveCornerRadius { get => _effectiveCornerRadius; set => SetAndRaise(EffectiveCornerRadiusProperty, ref _effectiveCornerRadius, value); } internal SpaceItemPosition? CompactSpaceItemPosition { get => GetValue(CompactSpaceItemPositionProperty); set => SetValue(CompactSpaceItemPositionProperty, value); } internal Orientation CompactSpaceOrientation { get => GetValue(CompactSpaceOrientationProperty); set => SetValue(CompactSpaceOrientationProperty, value); } internal bool IsUsedInCompactSpace { get => GetValue(IsUsedInCompactSpaceProperty); set => SetValue(IsUsedInCompactSpaceProperty, value); } #endregion static MySearchBox() { AffectsMeasure( CompactSpaceItemPositionProperty, CompactSpaceOrientationProperty); } public MySearchBox() { this.RegisterTokenResourceScope(MySearchBoxToken.ScopeProvider); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == CornerRadiusProperty || change.Property == CompactSpaceItemPositionProperty || change.Property == CompactSpaceOrientationProperty) { ConfigureEffectiveCornerRadius(); } } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); ConfigureEffectiveCornerRadius(); } private void ConfigureEffectiveCornerRadius() { EffectiveCornerRadius = CompactSpace.CalculateEffectiveCornerRadius( CornerRadius, IsUsedInCompactSpace, CompactSpaceItemPosition, CompactSpaceOrientation); } #region ICompactSpaceAware 实现 void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { IsUsedInCompactSpace = position != null; CompactSpaceItemPosition = position; } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { CompactSpaceOrientation = orientation; } double ICompactSpaceAware.GetBorderThickness() { return CompactSpaceOrientation == Orientation.Horizontal ? BorderThickness.Left : BorderThickness.Top; } #endregion } ``` --- ## 9. 验证方法 ### 9.1 Gallery 可视化验证 1. 在 `controlgallery/AtomUIGallery/ShowCases/Views/Layout/SpaceShowCase.axaml` 中添加测试用例 2. 运行 Gallery 应用(`controlgallery/AtomUIGallery.Desktop/`) 3. 导航到 Layout → Space 页面 ### 9.2 CompactSpace 验证检查清单 | 检查项 | 验证方法 | |---|---| | 控件可以放入 CompactSpace | 无运行时异常 | | 圆角裁剪正确 | 首项保留起始圆角,末项保留结束圆角,中间项无圆角 | | 边框不重叠 | 相邻控件之间无双倍边框线 | | Focus/Hover ZIndex 正确 | 聚焦/悬停时控件边框完整可见,不被相邻控件遮盖 | | SizeType 传递 | CompactSpace 的 `SizeType` 变化时,子控件同步变化 | | 水平/垂直方向 | 分别测试两个方向,圆角裁剪方向正确 | | 与其他控件组合 | 与 Button、LineEdit 等已适配控件混合使用 | ### 9.3 示例 AXAML 测试模板 ```xml 提交 按钮 1 按钮 3 ``` --- ## 10. 常见问题 ### Q: 控件放入 CompactSpace 后圆角没有变化? 确认以下几点: 1. 控件的主题 AXAML 中使用了 `{TemplateBinding EffectiveCornerRadius}` 而非 `{TemplateBinding CornerRadius}` 2. `OnApplyTemplate` 中调用了 `ConfigureEffectiveCornerRadius()` 3. `OnPropertyChanged` 中正确监听了 `CompactSpaceItemPositionProperty` 和 `CompactSpaceOrientationProperty` ### Q: 控件在 CompactSpace 中边框出现双倍粗细? 检查 `GetBorderThickness()` 返回值是否正确: - 该方法应返回控件在主轴方向的实际边框厚度 - 如果控件无边框(Borderless 样式),应返回 `0.0` ### Q: 控件的 ZIndex 行为异常? - 如果控件不需要参与 ZIndex 管理,让 `IgnoreZIndexChange()` 返回 `true` - 如果控件需要始终保持高 ZIndex,让 `IsAlwaysActiveZIndex()` 返回 `true` - 否则保持默认返回 `false`