WinUI 参考: 完整的属性清单与设计指南见 Layout。
Microsoft.UI.Reactor(Reactor)里的每一种布局,都是由若干职责单一的面板组合而成的 ——
你可以按 方向 来选(HStack 排一行,VStack 排一列),
按 换行行为 来选(希望元素流到新行时用 WrapGrid),
或者按 网格轴 来选(需要显式的行 × 列时用 Grid)。这些面板
之间不互相越界:Stack 只管一个轴,Grid 管两个轴,Canvas 管绝对坐标,
而 FlexPanel(见 flex-layout)提供 CSS Flexbox 那套能力,
包括 flex-grow、flex-shrink 和 flex-basis。这点很关键:
选对面板,布局开销就与子元素数量成线性关系。选错面板 —— 比如本该用
Grid 的地方套了一堆 VStack 套 HStack —— 每一层嵌套的度量与排列都要穿过
多个面板的代码路径,既更慢也更难读。
屏幕的顶层骨架先用 VStack 起步;一旦出现跨行跨列的对齐需求就换成
Grid;需要按比例分配尺寸,或者要带间隙的多行换行时,换成
FlexRow/FlexColumn。
布局¶
Reactor 提供一小组布局原语,组合起来可以搭建任意屏幕结构。每个布局元素 都接收子元素和可选的间距。
决策表¶
| 形态 | 面板 | 理由 |
|---|---|---|
| 元素沿单一方向排列 | VStack(spacing, children) / HStack(spacing, children) |
单轴堆叠,内置间距。 |
| 元素按行 × 列排布 | Grid(columns, rows, children) |
双轴,轨道尺寸显式指定(Auto、Star、Px)。 |
| 元素需要自动换行 | WrapGrid(maxRowsOrColumns, children) |
一行排满后自动流到下一行。 |
| 元素需要按比例分配尺寸或带间隙的多行换行 | FlexRow(...) / FlexColumn(...)(见 flex-layout) |
CSS Flexbox 语义:grow/shrink/basis。 |
| 绝对定位 | Canvas(children) |
自由摆放,配合 Canvas.SetLeft / SetTop。 |
| 视觉容器 | Border(child) / Card(child) |
围绕单个子元素提供背景、圆角半径与内边距。 |
| 溢出处理 | ScrollView(child) |
包裹任何可能超出可用空间的内容。 |
| 可折叠分组 | Expander(header, content) |
带标题栏,点击可切换内容面板。 |
VStack 与 HStack¶
最常用的两种布局。VStack 让子元素垂直堆叠,HStack 水平排列。 第一个参数是子元素之间的像素间距:
class StackDemo : Component
{
public override Element Render()
{
return VStack(16,
SubHeading("VStack and HStack"),
VStack(4,
TextBlock("VStack: items top to bottom"),
TextBlock("Item A"), TextBlock("Item B"), TextBlock("Item C")
),
HStack(8,
TextBlock("HStack:"),
Button("One"), Button("Two"), Button("Three")
)
);
}
}

VStack(16, ...) 让子元素自上而下彼此相隔 16 像素。
HStack(8, ...) 让子元素从左到右彼此相隔 8 像素。
省略间距参数即为零间距:VStack(child1, child2)。
它们可以自由嵌套 —— VStack 里放 HStack,HStack 里放 VStack,
想嵌多深都行。但一旦发现嵌套超过两层,就该改用 Grid(原因见
常见错误)。
Grid¶
要做行与列的布局,用 Grid。用 GridSize 辅助方法定义列和行,
再用 .Grid() 修饰符摆放子元素:
class GridDemo : Component
{
public override Element Render()
{
return VStack(8,
SubHeading("Grid"),
Grid(
columns: [GridSize.Px(120), GridSize.Star(), GridSize.Auto],
rows: [GridSize.Auto, GridSize.Auto],
TextBlock("Label").Bold().Grid(row: 0, column: 0),
TextBox("", _ => { }, placeholderText: "Input...")
.AutomationName("Input")
.Grid(row: 0, column: 1),
Button("Go").Grid(row: 0, column: 2),
TextBlock("Status").Grid(row: 1, column: 0),
TextBlock("Ready").Foreground(Theme.Accent)
.Grid(row: 1, column: 1, columnSpan: 2)
).Height(80)
);
}
}

| 辅助方法 | 含义 |
|---|---|
GridSize.Px(200) |
固定 200 像素 |
GridSize.Star() |
按比例分配 —— 填满可用空间(权重 1) |
GridSize.Star(2) |
按比例分配 —— 占据 GridSize.Star() 两倍的空间 |
GridSize.Auto |
尺寸由内容决定 |
用 .Grid(row: 0, column: 1) 摆放子元素。需要跨多个单元格时用
columnSpan 或 rowSpan:.Grid(row: 1, column: 0, columnSpan: 2)。
旧版的字符串形式重载
Grid(["120", "1*", "Auto"], ...)已移除 —— 请改用带类型的辅助方法(GridSize.Px(120)/GridSize.Star(1)/GridSize.Auto),拼错了在编译期就能发现。
Card¶
Card(child) 会把任意元素包进标准的 WinUI 卡片外观 —— 8px 圆角半径、
16px 内边距、跟随主题的背景和 1px 描边。用它,别再手写
Border(...).Background(Theme.CardBackground).WithBorder(...) 这一长串:
class CardDemo : Component
{
public override Element Render()
{
return VStack(12,
SubHeading("Card"),
Card(
VStack(8,
TextBlock("Recent activity").SemiBold(),
TextBlock("3 new messages, 2 mentions")
.Foreground(Theme.SecondaryText)
)
).Width(240)
);
}
}

继续串联其他流畅方法即可覆盖任何预设值:
Card(child).Padding(24).CornerRadius(16)。背景与描边都通过
ThemeRef 解析,所以浅色/深色/高对比度切换时会自动重绘,无需改代码 ——
见 styling。
字体阶梯工厂方法¶
要让标题和正文匹配 WinUI 3 的字体阶梯,直接用这些具名工厂方法,
不必反复写 .FontSize(...).Bold() 链:
class TypeRampDemo : Component
{
public override Element Render()
{
return VStack(8,
Title("Quarterly results"),
Subtitle("Q3 2026 highlights"),
BodyLarge("Revenue grew 18% year over year."),
BodyStrong("Net income reached an all-time high."),
Body("Full breakdown on the following pages.")
).Padding(24);
}
}

| 工厂方法 | WinUI 样式 |
|---|---|
Display(text) |
DisplayTextBlockStyle —— 68px Semibold |
TitleLarge(text) |
TitleLargeTextBlockStyle —— 40px Semibold |
Title(text) |
TitleTextBlockStyle —— 28px Semibold |
Subtitle(text) |
SubtitleTextBlockStyle —— 20px Semibold |
BodyLarge(text) |
BodyLargeTextBlockStyle —— 18px regular |
BodyStrong(text) |
BodyStrongTextBlockStyle —— 14px Semibold |
Body(text) |
BodyTextBlockStyle —— 14px regular |
Display 和 TitleLarge 超出了截图所展示的范围;Display 留给
英雄横幅(每页最多一个),TitleLarge 用于功能页或落地页的主标题。
它们之所以是工厂方法(而非流畅方法),是刻意的设计 ——
参见 spec 039 §17.6。需要偏离默认值时,
可以在结果上继续叠加修饰符(Title("…").Foreground(Theme.Accent))。
ScrollView 与 Border¶
ScrollView 用于包裹可能溢出的内容。Border 提供一个带背景、圆角半径
和内边距的视觉容器:
class ScrollBorderDemo : Component
{
public override Element Render()
{
return VStack(8,
SubHeading("ScrollView and Border"),
Border(
ScrollView(
VStack(4,
ForEach(
Enumerable.Range(1, 20),
i => TextBlock($"Scrollable item {i}").WithKey(i.ToString()))
).Padding(8)
).Height(120)
).CornerRadius(4).Background(Theme.CardBackground)
);
}
}

ScrollView 只接受一个子元素。要滚动垂直列表,就把 VStack 包起来。
Border 纯粹是视觉性的 —— 它在子元素背后绘制一个圆角矩形,
适合做卡片、面板和分组。
长列表请优先用 VirtualList,而不是
ScrollView(VStack(...))。 ScrollView 会布局每一个子元素,
哪怕它们不在可视区域内 —— 100 项以内很快,超过约 500 项就会明显变慢;
VirtualList 只实例化可视区域内的那几项。
Expander 与 Canvas¶
Expander 显示一个标题栏,点击时折叠/展开其内容。
Canvas 按绝对坐标摆放子元素:
class ExpanderCanvasDemo : Component
{
public override Element Render()
{
return VStack(12,
SubHeading("Expander"),
Expander("Click to expand", VStack(8,
TextBlock("Hidden content revealed!"),
TextBlock("Expanders are great for optional details.")
)),
SubHeading("Canvas"),
Border(
Canvas(
TextBlock("Top-left").Set(c => {
Microsoft.UI.Xaml.Controls.Canvas.SetLeft((UIElement)c, 10);
Microsoft.UI.Xaml.Controls.Canvas.SetTop((UIElement)c, 10);
}),
TextBlock("Center").Set(c => {
Microsoft.UI.Xaml.Controls.Canvas.SetLeft((UIElement)c, 120);
Microsoft.UI.Xaml.Controls.Canvas.SetTop((UIElement)c, 40);
})
).Height(90).Width(300)
).Background(Theme.CardBackground).CornerRadius(4)
);
}
}

Expander(header, content) 内部自行维护展开/折叠状态。
传 isExpanded: true 让它初始即展开,或者用 onExpandedChanged
自己跟踪这个状态。
Canvas 通过 .Set() 修饰符调用 Canvas.SetLeft 与 Canvas.SetTop
来定位子元素。它适合做浮层、示意图,以及任何不符合堆叠或网格规律的布局。
响应式布局¶
根据窗口宽度切换布局。用状态开关或 UseBreakpoint
在 HStack 与 VStack 之间切换:
class ResponsiveDemo : Component
{
public override Element Render()
{
var (wide, setWide) = UseState(true);
var content = new Element[]
{
Border(TextBlock("Panel A").Padding(16))
.Background(Theme.SystemNeutralBackground).CornerRadius(4),
Border(TextBlock("Panel B").Padding(16))
.Background(Theme.SystemCautionBackground).CornerRadius(4),
Border(TextBlock("Panel C").Padding(16))
.Background(Theme.SystemSuccessBackground).CornerRadius(4),
};
return VStack(8,
SubHeading("Responsive Layout"),
HStack(8,
TextBlock("Simulate wide screen:"),
ToggleSwitch(wide, setWide)
),
If(wide,
() => HStack(12, content),
() => VStack(8, content))
);
}
}

真正的响应式布局请用 UseBreakpoint
(minWidth)(需要显式指定宿主窗口时用 (window, minWidth)),
窗口宽度至少为 minWidth 像素时它返回 true。配合 If() 切换布局:
var wide = UseBreakpoint(800);
return If(wide,
() => HStack(12, panelA, panelB),
() => VStack(8, panelA, panelB));
如果需要精确尺寸做更细粒度的控制,UseWindowSize(window) 会返回
(Width, Height)。
对齐与尺寸¶
每个元素都支持尺寸与对齐类的修饰符:
class AlignmentSizingDemo : Component
{
public override Element Render() => VStack(8,
SubHeading("Alignment and sizing"),
TextBlock("Centered").HAlign(HorizontalAlignment.Center),
Border(TextBlock("Fixed width"))
.Width(200).Height(40)
.Background(Theme.ControlFillSecondary),
VStack(8,
TextBlock("Item A"),
TextBlock("Item B")
).Margin(24).Padding(16).Background(Theme.ControlFillSecondary)
);
}
| 修饰符 | 效果 |
|---|---|
.Width(n) |
固定宽度,单位为像素 |
.Height(n) |
固定高度,单位为像素 |
.Margin(n) / .Margin(thickness) |
外间距 —— 统一的 n,或通过 Thickness 逐边指定 |
.Padding(n) / .Padding(thickness) |
内边距 —— 统一的 n,或通过 Thickness 逐边指定 |
.HAlign(alignment) |
水平对齐(Left、Center、Right、Stretch) |
.VAlign(alignment) |
垂直对齐(Top、Center、Bottom、Stretch) |
.HorizontalContentAlignment(alignment) |
Control 内部子内容/内容的水平对齐 |
.VerticalContentAlignment(alignment) |
Control 内部子内容/内容的垂直对齐 |
当控件自身需要拉伸、但其子内容也需要一个明确的摆放约定时,就用 内容对齐类的流畅方法。一个典型场景是:一个占满整行的按钮,其内部的 行内容也要拉伸:
class ContentAlignmentDemo : Component
{
public override Element Render() => VStack(8,
SubHeading("Content alignment"),
// The Button stretches to fill the row, and its inner content
// stretches too — without HorizontalContentAlignment the label
// would stay centered in an otherwise full-width button.
Button(TextBlock("Open"), () => { })
.AutomationName("Open")
.HAlign(HorizontalAlignment.Stretch)
.HorizontalContentAlignment(HorizontalAlignment.Stretch)
).Width(320);
}
这些修饰符内部如何串联,见 modifier-system。
注意: 使用
*(星号)尺寸的行列,Grid在首次布局过程中会对 子元素度量两次 —— 一次用Available = Infinity确定内容的自然尺寸, 一次用Available = ActualWidth解析星号轨道。子元素在 100 个以内时 这个双次度量很快,超过约 500 个就会有可感知的开销。对于渲染进星号 尺寸单元格的大列表,请用VirtualList或LazyVStack包一层,让面板只实例化可视窗口内的内容;宿主的星号轨道 只度量一次视口尺寸。固定(Px)和Auto轨道不承担双次度量的开销。
模式¶
应用外壳骨架¶
典型的管理后台式应用外壳 —— 标题栏、侧边栏、主内容区、状态栏。
一条 Grid 声明搞定;只需一次度量与排列:
// 应用外壳骨架:用一条 Grid 声明实现「标题栏 + 侧边栏 + 内容区」。
// 这种两列 / 三行的形态是管理后台类应用的标准结构,
// 取代手写的 HStack/VStack 嵌套。
class AppShellExample : Component
{
public override Element Render()
{
return Grid(
columns: [GridSize.Px(220), GridSize.Star()],
rows: [GridSize.Px(44), GridSize.Star(), GridSize.Px(28)],
// 顶栏 —— 横跨两列。
Border(TextBlock("My App").Bold().Padding(12))
.Background(Theme.LayerFill)
.Grid(row: 0, column: 0, columnSpan: 2),
// 侧边栏。
Border(VStack(4,
TextBlock("Dashboard"),
TextBlock("Reports"),
TextBlock("Settings"))
.Padding(12))
.Background(Theme.CardBackground)
.Grid(row: 1, column: 0),
// 主内容区。
ScrollView(VStack(8,
Title("Welcome"),
Body("Stack -> Grid for the shell, VStack inside the panes.")))
.Padding(16)
.Grid(row: 1, column: 1),
// 状态栏。
TextBlock("Ready").Padding(6)
.Grid(row: 2, column: 0, columnSpan: 2)
).Width(560).Height(360);
}
}

用嵌套的 HStack/VStack 也能做出来的朴素版本,但会把行与列的宽度
耦合在一起 —— 调整侧边栏宽度就得改动每一条与之相交的行。
Grid 让两个轴彼此独立。
自动网格¶
照片墙、磁贴仪表盘、标签云 —— 任何需要随容器宽度自动流成多行的内容 ——
都可以用 WrapGrid:
// 自动网格:一行排满后,WrapGrid 会把元素序列自动换行成列,
// 无需手动指定行/列位置。这里设定「每行最多 4 个」。
class AutoGridExample : Component
{
public override Element Render()
{
return WrapGrid(maxRowsOrColumns: 4,
children: Enumerable.Range(1, 11)
.Select(i =>
Border(TextBlock($"Tile {i}").Padding(12))
.Background(Theme.CardBackground)
.CornerRadius(6)
.Width(110).Height(60))
.Cast<Element?>()
.ToArray()
);
}
}

maxRowsOrColumns 限制主轴上的数量;达到该数量后面板就换行。
若希望按可用像素宽度(而非元素个数)换行,请用带
Wrap = FlexWrap.Wrap 的 FlexRow(...)。
响应式切换器¶
最简的响应式配方 —— 窄屏与宽屏呈现不同形态:
// 响应式切换器:同一份内容在宽屏下渲染为 HStack,
// 窗口窄于 480px 时渲染为 VStack。UseBreakpoint 是这个模式的标准 Hook。
class ResponsiveSwitcherExample : Component
{
public override Element Render()
{
var (simulated, setSimulated) = UseState(true); // 演示用,没有真实窗口句柄
var panels = new Element[]
{
Border(TextBlock("Panel A").Padding(16))
.Background(Theme.CardBackground).CornerRadius(4),
Border(TextBlock("Panel B").Padding(16))
.Background(Theme.CardBackground).CornerRadius(4),
Border(TextBlock("Panel C").Padding(16))
.Background(Theme.CardBackground).CornerRadius(4),
};
return VStack(8,
HStack(8,
TextBlock("Simulate wide window:"),
ToggleSwitch(simulated, setSimulated)),
If(simulated,
() => HStack(12, panels),
() => VStack(8, panels))
).Padding(24);
}
}

同一个 panels 数组可以直接放进任意一个容器。真正的生产版本应该读取
UseBreakpoint(window, 480),而不是手动拨开关 —— 参见上面的
响应式布局一节。
常见错误¶
本该用 Grid 的地方嵌套了过深的 Stack¶
// 反例 —— 层层嵌套的 VStack/HStack 掩盖了真实意图,
// 而 Grid 用一次度量过程就能吸收掉这部分布局开销。
class DontDeepStack : Component
{
public override Element Render()
{
return HStack(8,
VStack(4,
TextBlock("Label").Bold(),
TextBlock("Sublabel").Foreground(Theme.SecondaryText)),
VStack(0,
HStack(4, TextBlock("First:"), TextBlock("Value 1")),
HStack(4, TextBlock("Second:"), TextBlock("Value 2")),
HStack(4, TextBlock("Third:"), TextBlock("Value 3")))
).Padding(24);
}
}
用三层 HStack/VStack 去排一个标签-值列表,读起来比它本来的
行 × 列形态费劲得多。同样的形态用 Grid 表达:
// 正例 —— 同样的形态写成两列 Grid。第 0 列的标签按内容自动定尺寸;
// 第 1 列拉伸。
class DoGridForForms : Component
{
public override Element Render()
{
return Grid(
columns: [GridSize.Auto, GridSize.Star()],
rows: [GridSize.Auto, GridSize.Auto, GridSize.Auto],
TextBlock("First:").Margin(4).Grid(row: 0, column: 0),
TextBlock("Value 1").Margin(4).Grid(row: 0, column: 1),
TextBlock("Second:").Margin(4).Grid(row: 1, column: 0),
TextBlock("Value 2").Margin(4).Grid(row: 1, column: 1),
TextBlock("Third:").Margin(4).Grid(row: 2, column: 0),
TextBlock("Value 3").Margin(4).Grid(row: 2, column: 1)
).Padding(24).Width(300);
}
}

Auto 列紧贴标签宽度;Star 列拉伸填满。加一行只需加一条声明。
而嵌套 Stack 的写法每加一行都要重新缩进所有兄弟节点。经验法则:
只要布局里有任意两个元素需要跨行或跨列对齐,就用 Grid。
本该填满的模板里漏了 * 尺寸¶
// 别这样:
Grid(
columns: [GridSize.Px(220), GridSize.Auto], // 内容列按内容定尺寸
rows: [GridSize.Star()],
Sidebar(), Content()) // Content 会收缩到它的固有宽度
Auto 的含义是"度量我的内容,用那个尺寸";Star 的含义是
"拿走剩下的空间"。一个应当填满剩余横向空间的主内容单元格必须声明
GridSize.Star() —— 否则它会缩到固有尺寸,旁边就露出一片空白。
分析器(启用时)的 REACTOR_GRID_001 会标记这种典型的
"列未被使用"症状。
在同一条轴上混用 Flex 与 Stack¶
FlexRow/FlexColumn(由 FlexPanel 支撑)和 Stack 都是沿一条轴布局,
但 flex 面板遵循 CSS Flexbox 语义(flex-grow、flex-shrink、
flex-basis),而 Stack 遵循的是更简单的规则:
"每个子元素取自然尺寸,间隙固定"。在同一条轴上混用两者,
意味着外层 flex 面板的 grow/shrink 计算要基于不透明的 Stack
度量结果 —— 简单场景尚可预测,一旦涉及动态内容就会出现意外。
每条轴上只选一种。何时该用哪种形态,见 flex-layout。
Grid 的 Auto 轨道里放 HStack/VStack 会塌缩成零尺寸¶
// 别这样:
Grid(
columns: [GridSize.Auto, GridSize.Star()],
rows: [GridSize.Star()],
HStack(SaveButton(), CancelButton()).Grid(column: 0)) // 这一行消失了 —— 度量为 0×0
Grid 的 Auto 轨道在该轴上是以无限可用空间去度量内容的。
而 StackPanel(HStack/VStack)会顺序度量子元素并累加它们的期望尺寸 ——
于是任何依赖拉伸尺寸的子元素都会上报 0,堆栈累加得到 0,
整列(或整行)就静默塌缩成 0×0。那一行操作按钮就这么凭空消失,且不报任何错。
用 Border 把堆栈包一层并不能解决问题 —— Border 会跟着它那个
尺寸为 0 的子元素走。真正的修法是:
- 改用
Star轨道(GridSize.Star()),让单元格给堆栈一个有限宽度去拉伸,或者 - 给堆栈显式设置
.Width(...)(HStack)/.Height(...)(VStack)—— 或.MinWidth(...)/.MinHeight(...),或者 - 给堆栈的子元素设置显式(或最小)尺寸,让它们不再依赖拉伸。
// 应该这样:
Grid(
columns: [GridSize.Star(), GridSize.Star()], // 用 Star,不用 Auto
rows: [GridSize.Star()],
HStack(SaveButton(), CancelButton()).Grid(column: 0, columnSpan: 2));
Reactor 会在调试期把这类陷阱暴露出来:当
ReactorFeatureFlags.WarnLayoutFootguns 被设置时(DEBUG 构建下始终开启),
协调器会发出一次性警告,点名出问题的那个堆栈及其所在的 Auto 轨道,
省得你自己二分排查到底是哪一处塌缩了。
小贴士¶
从 VStack 起步。 大多数屏幕都是若干区块的垂直堆叠。
在这些区块内部需要并排内容时加 HStack;当有两个元素需要跨行或跨列对齐时,
就该用 Grid 了。
表单用 Grid。 两列网格配上
[GridSize.Auto, GridSize.Star()],左侧标签对齐、右侧输入框拉伸 ——
见 do-grid-for-forms 片段。
长内容用 ScrollView 包起来;超过约 500 项就改用 VirtualList。
ScrollView(VStack(...)) 会布局每一个子元素,哪怕不在可视区内。
VirtualList 只实例化可视窗口内的元素。
优先用间距,而不是外边距。 VStack(12, ...) 让子元素之间保持一致的
间距。而给每个子元素单独加 .Margin(12) 更难维护,还会在边界处产生双倍间距。
分组优先用 Card,而不是手搓 Border。 Card(content) 提供
8px 圆角半径、16px 内边距、跟随主题的背景和 1px 描边 —— 标准的 WinUI 外观。
只有在需要非卡片外观时才用 Border(比如彩色强调、单边边框、非标准圆角)。
下一步¶
- Hooks —— 前一篇:
UseState、UseMemo以及 Hook 表面的其余部分 - Flex Layout —— 下一篇:
FlexRow/FlexColumn,用于 grow/shrink/basis 与带间隙的多行换行 - Styling and Theming —— 为布局应用颜色、字体与主题
- Collections —— 用
VirtualList承载会撑破ScrollView的大数据集 - Forms and Input —— 用文本框、复选框与校验构建数据录入表单
- Modifier System ——
.Width()、.Margin()等修饰符内部如何串联