# AtomUI 媒体查询系统 — 架构设计
> 本文档描述 AtomUI 媒体查询系统的内部架构,包括断点检测管道、核心类型定义、事件流转机制以及各层组件的职责划分。
---
## 1. 架构总览
媒体查询系统由三层组件协作实现:
```
┌────────────────────────────────────────────────────────────────────────┐
│ Window 主题 (WindowTheme.axaml) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ContainerQuery 声明(6 组 min-width / max-width 查询) │ │
│ │ → 驱动 WindowMediaQueryIndicator.MediaBreakPoint 属性变更 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ 属性变更 │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ WindowMediaQueryIndicator (internal) │ │
│ │ → 调用 Window.NotifyMediaBreakPointChanged() │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ 事件通知 │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Window : IMediaBreakAwareControl │ │
│ │ → 更新 MediaBreakPoint 属性 │ │
│ │ → 触发 MediaBreakPointChanged 事件 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ 事件订阅 │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 消费控件:Row, FormItem, Descriptions 等 │ │
│ │ → 订阅 Window.MediaBreakPointChanged │ │
│ │ → 根据新断点重新计算布局 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
```
---
## 2. 断点检测管道
### 2.1 容器声明
在 `WindowTheme.axaml` 的控件模板中,Window 的内容区域 `Border#ContentFrame` 被声明为查询容器:
```xml
```
关键设置:
- **`Container.Name`**:值为 `IMediaBreakAwareControl.GlobalQueryContainerName`(常量 `"PART_GlobalQueryContainer"`),所有 ContainerQuery 通过此名称关联。
- **`Container.Sizing`**:设为 `WidthAndHeight`,使容器跟踪宽度和高度的实际尺寸。
### 2.2 ContainerQuery 断点规则
在 `WindowTheme.axaml` 的 ControlTheme 顶层声明了 6 组 ContainerQuery,覆盖所有断点:
```xml
```
**工作原理**:Avalonia 的 ContainerQuery 按声明顺序求值,后匹配的规则覆盖先匹配的。例如当容器宽度为 1024px 时:
1. `max-width:575` → 不匹配
2. `min-width:576` → 匹配,设为 `Small`
3. `min-width:768` → 匹配,**覆盖**为 `Medium`
4. `min-width:992` → 匹配,**覆盖**为 `Large`
5. `min-width:1200` → 不匹配
6. `min-width:1600` → 不匹配
最终结果:`MediaBreakPoint.Large`。
### 2.3 WindowMediaQueryIndicator(中继控件)
```csharp
// src/AtomUI.Desktop.Controls/Window/WindowMediaQueryIndicator.cs
internal class WindowMediaQueryIndicator : Control
{
public static readonly StyledProperty MediaBreakPointProperty =
MediaBreakAwareControlProperty.MediaBreakPointProperty.AddOwner();
public Window? OwnerWindow { get; set; }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == MediaBreakPointProperty)
{
OwnerWindow?.NotifyMediaBreakPointChanged(MediaBreakPoint);
}
}
}
```
**设计原因**:ContainerQuery 的 Style 不能直接影响容器本身或其祖先(Avalonia 约束,防止循环更新)。因此引入一个轻量的内部中继控件 `WindowMediaQueryIndicator`,它是容器的子孙控件,可以被 ContainerQuery 的 Style 正常设置属性。当其 `MediaBreakPoint` 属性被 ContainerQuery 更新时,它通知 `Window` 更新断点状态。
### 2.4 Window 断点分发
```csharp
// src/AtomUI.Desktop.Controls/Window/Window.cs
public class Window : AvaloniaWindow, IMediaBreakAwareControl, ...
{
public static readonly StyledProperty MediaBreakPointProperty =
MediaBreakAwareControlProperty.MediaBreakPointProperty.AddOwner();
public MediaBreakPoint MediaBreakPoint
{
get => GetValue(MediaBreakPointProperty);
internal set => SetValue(MediaBreakPointProperty, value);
}
public event EventHandler? MediaBreakPointChanged;
internal void NotifyMediaBreakPointChanged(MediaBreakPoint breakPoint)
{
SetCurrentValue(MediaBreakPointProperty, breakPoint);
MediaBreakPointChanged?.Invoke(this, new MediaBreakPointChangedEventArgs(breakPoint));
}
}
```
`Window` 在 `OnApplyTemplate` 中将自身引用注入 `WindowMediaQueryIndicator.OwnerWindow`,建立中继关系。
---
## 3. 核心类型定义
### 3.1 基础类型(`AtomUI.Controls.Shared/MediaQuery/`)
这些类型定义在基础共享层,所有控件项目均可引用。
#### `MediaBreakPoint` 枚举
```csharp
public enum MediaBreakPoint
{
ExtraSmall = 0, // xs
Small = 576, // sm
Medium = 768, // md
Large = 992, // lg (默认值)
ExtraLarge = 1200, // xl
ExtraExtraLarge = 1600, // xxl
}
```
枚举值即为该断点的最小宽度像素值,可直接用于比较运算。
#### `IMediaBreakAwareControl` 接口
```csharp
public interface IMediaBreakAwareControl
{
public const string GlobalQueryContainerName = "PART_GlobalQueryContainer";
MediaBreakPoint MediaBreakPoint { get; }
event EventHandler? MediaBreakPointChanged;
}
```
任何顶级容器(如 `Window`)实现此接口后,其子控件树中的响应式控件即可通过 `TopLevel.GetTopLevel(this) is IMediaBreakAwareControl` 获取断点信息。
#### `MediaBreakAwareControlProperty` 抽象属性持有者
```csharp
public abstract class MediaBreakAwareControlProperty
{
public const string MediaBreakPointPropertyName = "MediaBreakPoint";
public static readonly StyledProperty MediaBreakPointProperty =
AvaloniaProperty.Register(
MediaBreakPointPropertyName, MediaBreakPoint.Large);
}
```
遵循 AtomUI 的共享属性模式(`AddOwner` 模式),供 `Window`、`WindowMediaQueryIndicator` 等控件复用同一 `StyledProperty` 定义。
#### `MediaBreakPointChangedEventArgs`
```csharp
public class MediaBreakPointChangedEventArgs : EventArgs
{
public MediaBreakPoint MediaBreakPoint { get; }
}
```
#### `MediaBreakGridLength`
用于在不同断点下指定不同的 `GridLength` 值。典型用途:`Form` / `FormItem` 的 `LabelColInfo` 和 `WrapperColInfo` 属性。
```csharp
public record MediaBreakGridLength
{
public GridLength ExtraSmall { get; init; }
public GridLength Small { get; init; }
public GridLength Medium { get; init; }
public GridLength Large { get; init; }
public GridLength ExtraLarge { get; init; }
public GridLength ExtraExtraLarge { get; init; }
// 支持从字符串解析:
// 单一值: "2*" → 所有断点使用 2*
// 分断点: "xs:1*,lg:2*" → xs 断点 1*, lg 断点 2*
public static MediaBreakGridLength Parse(string input) { ... }
}
```
### 3.2 Grid 响应式类型(`AtomUI.Desktop.Controls/Grid/`)
#### `GridColSpanInfo`
用于 `Col.Span` 属性,支持在不同断点下指定不同的列跨度:
```csharp
[TypeConverter(typeof(GridColSpanInfoConverter))]
public readonly record struct GridColSpanInfo
{
public int ExtraSmall { get; init; }
public int Small { get; init; }
public int Medium { get; init; }
public int Large { get; init; }
public int ExtraLarge { get; init; }
public int ExtraExtraLarge { get; init; }
// 单一值: "8" → 所有断点 span=8
// 分断点: "xs:24,sm:12,lg:8" → 按断点指定
public static GridColSpanInfo Parse(string input) { ... }
public int GetValue(MediaBreakPoint breakPoint) { ... }
}
```
#### `GridGutterInfo`
用于 `Row.Gutter` 的水平/垂直分量,支持按断点设置不同间距值:
```csharp
public record GridGutterInfo
{
public double ExtraSmall { get; init; }
public double Small { get; init; }
// ... 6 个断点属性
// 单一值: "16" → 所有断点 16px
// 分断点: "xs:8,sm:16,lg:24" → 按断点指定
public static GridGutterInfo Parse(string input) { ... }
internal double GetValue(MediaBreakPoint breakPoint) { ... }
}
```
#### `GridGutter`
`Row.Gutter` 属性的完整类型,包含水平和垂直两个 `GridGutterInfo`:
```csharp
[TypeConverter(typeof(GridGutterConverter))]
public record GridGutter
{
public GridGutterInfo Horizontal { get; init; }
public GridGutterInfo Vertical { get; init; }
// "16" → 水平 16px, 垂直 0
// "16,24" → 水平 16px, 垂直 24px
// "xs:8,lg:16" → 水平按断点, 垂直 0
// "xs:8,lg:16;xs:4,lg:12" → 水平和垂直各按断点(用 ; 分隔)
public static GridGutter Parse(string input) { ... }
}
```
#### `GridColSize`
用于 `Col` 的响应式断点属性(`Xs`、`Sm`、`Md`、`Lg`、`Xl`、`Xxl`),可覆盖该断点下的 Span、Offset、Order、Push、Pull:
```csharp
[TypeConverter(typeof(GridColSizeConverter))]
public record GridColSize
{
public int? Span { get; init; }
public int? Offset { get; init; }
public int? Order { get; init; }
public int? Push { get; init; }
public int? Pull { get; init; }
// 纯数字: "12" → 仅设 Span=12
// 键值对: "span:12,offset:2,order:1" → 按名称设置
public static GridColSize Parse(string input) { ... }
}
```
#### `DescriptionsMediaBreakInfo`
用于 `Descriptions.ColumnInfo` 和 `DescriptionItem.Span`,按断点指定列数或跨度:
```csharp
public record DescriptionsMediaBreakInfo
{
public int ExtraSmall { get; init; }
public int Small { get; init; }
// ... 6 个断点属性
// 单一值: "3" → 所有断点 3 列
// 分断点: "xs:1,sm:2,lg:3" → 按断点指定列数
public static DescriptionsMediaBreakInfo Parse(string input) { ... }
}
```
---
## 4. 消费控件的订阅模式
所有需要响应断点变化的控件遵循统一的订阅模式:
```csharp
public class SomeResponsiveControl : Control
{
private MediaBreakPoint? _breakPoint;
private Window? _attachedWindow;
// 1. 附加到视觉树时,订阅 Window 的断点变更事件
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (TopLevel.GetTopLevel(this) is Window window)
{
_attachedWindow = window;
_breakPoint = window.MediaBreakPoint;
window.MediaBreakPointChanged += HandleMediaBreakChanged;
}
}
// 2. 从视觉树分离时,取消订阅
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (_attachedWindow != null)
{
_attachedWindow.MediaBreakPointChanged -= HandleMediaBreakChanged;
_attachedWindow = null;
}
}
// 3. 断点变更时,重新计算布局
private void HandleMediaBreakChanged(object? sender, MediaBreakPointChangedEventArgs args)
{
_breakPoint = args.MediaBreakPoint;
InvalidateMeasure();
InvalidateArrange();
}
}
```
**注意**:
- `Row` 使用 `TopLevel.GetTopLevel(this) is IMediaBreakAwareControl` 来获取断点(更通用的接口方式)。
- `FormItem` 和 `Descriptions` 直接转型为 `Window`(因为桌面平台的 TopLevel 就是 Window)。
- 默认断点为 `MediaBreakPoint.Large`(992px),当无法获取窗口引用时使用此值。
---
## 5. 文件索引
### 基础层(`AtomUI.Controls.Shared/MediaQuery/`)
| 文件 | 类型 | 职责 |
|---|---|---|
| `MediaBreakPoints.cs` | `MediaBreakPoint` 枚举 | 六级断点定义,枚举值即最小宽度像素 |
| `IMediaBreakAware.cs` | `IMediaBreakAwareControl` 接口 + `MediaBreakAwareControlProperty` | 断点感知接口和共享属性定义 |
| `MediaBreakPointChangedEventArgs.cs` | `MediaBreakPointChangedEventArgs` | 断点变更事件参数 |
| `MediaBreakGridLength.cs` | `MediaBreakGridLength` record | 按断点的 GridLength 映射,用于 Form 布局 |
### 平台层(`AtomUI.Desktop.Controls/`)
| 文件 | 类型 | 职责 |
|---|---|---|
| `Window/Window.cs` | `Window` | 实现 `IMediaBreakAwareControl`,集中管理断点状态 |
| `Window/WindowMediaQueryIndicator.cs` | `WindowMediaQueryIndicator` | ContainerQuery 的中继控件 |
| `Window/Themes/WindowTheme.axaml` | ControlTheme | 声明容器和 6 组 ContainerQuery |
| `Grid/Row.cs` | `Row` | 24 栏栅格行布局,消费断点驱动 Gutter 和列布局 |
| `Grid/Col.cs` | `Col` | 栅格列,按断点解析 Span/Offset/Order/Push/Pull |
| `Grid/GridGutter.cs` | `GridGutter` | 响应式间距值(水平 + 垂直) |
| `Grid/GridGutterInfo.cs` | `GridGutterInfo` | 按断点的间距值映射 |
| `Grid/GridColSpanInfo.cs` | `GridColSpanInfo` | 按断点的列跨度映射 |
| `Grid/GridColSize.cs` | `GridColSize` + `GridColLayout` | 响应式列配置(完整覆盖) |
| `Grid/ColInfoExtension.cs` | `ColInfoExtension` | AXAML 标记扩展 `{atom:ColInfo}` |
| `Form/Form.cs` | `Form` | 表单容器,传递 `LabelColInfo` / `WrapperColInfo` |
| `Form/FormItem.cs` | `FormItem` | 表单项,按断点计算标签/内容列宽 |
| `Descriptions/Descriptions.cs` | `Descriptions` | 描述列表,按断点计算列数 |
| `Descriptions/DescriptionsMediaBreakInfo.cs` | `DescriptionsMediaBreakInfo` | 按断点的整数值映射(列数/跨度) |