Microsoft.UI.Reactor(Reactor)的 flex 布局是同时推理两条轴的。主轴负责
把子元素分配到可用空间上 —— JustifyContent、Flex(grow:)、Flex(shrink:)
和 Flex(basis:) 都作用在主轴上。交叉轴负责让每个子元素在容器的另一个
维度上对齐 —— AlignItems 和逐子元素的 AlignSelf 作用在交叉轴上。
同一个 FlexPanel 同时组合了这两者,正是这个组合解锁了 VStack 和
HStack 表达不了的布局。带弹性空隙的工具栏、固定侧边栏加流式内容区的
应用外壳、溢出即换行的标签行 —— 这三者都是 FlexPanel,只是主轴/交叉轴
规则不同。当你想用 Grid 去解决一个本质上是"沿一条轴分配这些元素,
同时另一条轴上要对齐"的问题时,请先读完这一页 —— 那是 FlexPanel 的形态;
而一旦你觉得上网格有点杀鸡用牛刀,那就是 Flex 该上场的时候。
Flex 布局¶
Reactor 的 FlexPanel(通过 FlexRow 和
FlexColumn 工厂方法暴露)是基于 Yoga 的 CSS Flexbox
实现。当布局需要 VStack 和 HStack 表达不了的对齐控制、
换行或按比例分配尺寸时,就用它。加上
using Microsoft.UI.Reactor.Layout;(或导入 FlexDirection、FlexJustify、
FlexAlign、FlexWrap)即可使用这些枚举类型。
速查表¶
| 属性 | 设置位置 | 效果 |
|---|---|---|
JustifyContent |
容器(with { ... }) |
主轴上的分布:FlexStart / Center / FlexEnd / SpaceBetween / SpaceAround / SpaceEvenly |
AlignItems |
容器(with { ... }) |
交叉轴对齐:Stretch / FlexStart / Center / FlexEnd / Baseline |
AlignContent |
容器(with { ... }) |
换行后的行在交叉轴上的对齐 —— 仅在开启 Wrap 且各行之间有剩余空间时才有意义 |
Wrap |
容器(with { ... }) |
NoWrap(单行,溢出)/ Wrap / WrapReverse |
ColumnGap / RowGap |
容器(with { ... }) |
子元素之间的间距 —— 换行后的行与行之间同样生效 |
FlexPadding |
容器(with { ... }) |
由 Yoga 度量的容器内边距(与 .Padding(...) 修饰符不同) |
.Flex(grow:, shrink:, basis:) |
子元素 | 主轴尺寸 |
.Flex(minWidth:, minHeight:) |
子元素 | 覆盖主轴上 CSS §4.5 的自动最小尺寸(null = CSS 的 auto;0 = 不设下限;正数 = 硬性下限)。auto 如何解析见最小尺寸表。 |
.Flex(alignSelf:) |
子元素 | 为单个子元素覆盖容器的 AlignItems |
方向¶
FlexRow 沿水平主轴摆放子元素;FlexColumn 沿垂直主轴摆放。
交叉轴就是剩下的那个方向。通过 ColumnGap(行子元素之间)
或 RowGap(列子元素之间)设置间距:
class FlexDirectionDemo : Component
{
public override Element Render()
{
return VStack(16,
SubHeading("Row (default)"),
FlexRow(
Border("A").Padding(12).Background(Theme.AccentTertiary),
Border("B").Padding(12).Background(Theme.SystemCriticalBackground),
Border("C").Padding(12).Background(Theme.SystemSuccessBackground)
) with { ColumnGap = 8 },
SubHeading("Column"),
FlexColumn(
Border("A").Padding(12).Background(Theme.AccentTertiary),
Border("B").Padding(12).Background(Theme.SystemCriticalBackground),
Border("C").Padding(12).Background(Theme.SystemSuccessBackground)
) with { RowGap = 8 }
).Padding(24);
}
}

粗看之下 FlexRow 很像 HStack,但 FlexPanel 额外提供了
主轴分布、交叉轴对齐、换行和按比例分配尺寸 —— 这些 HStack 一个都不支持。
主轴分布与对齐¶
JustifyContent 沿主轴分配子元素;AlignItems 在交叉轴上对齐它们。
它们是同一个容器上两个各自独立的旋钮 —— 请把它们当成两句话分别读:
class JustifyAlignDemo : Component
{
public override Element Render()
{
return VStack(16,
SubHeading("JustifyContent: SpaceBetween"),
FlexRow(
Border("Left").Padding(8).Background(Theme.AccentTertiary),
Border("Center").Padding(8).Background(Theme.SystemCriticalBackground),
Border("Right").Padding(8).Background(Theme.SystemSuccessBackground)
) with { JustifyContent = FlexJustify.SpaceBetween },
SubHeading("AlignItems: Center"),
FlexRow(
Border("Short").Padding(8).Background(Theme.AccentTertiary),
Border("Tall\nItem").Padding(8).Background(Theme.SystemCriticalBackground),
Border("Med").Padding(8).Background(Theme.SystemSuccessBackground)
) with {
AlignItems = FlexAlign.Center,
ColumnGap = 8
}
).Padding(24).Height(300);
}
}

JustifyContent |
效果(主轴) |
|---|---|
FlexStart |
从起点开始排布(默认) |
Center |
沿主轴居中 |
FlexEnd |
从终点开始排布 |
SpaceBetween |
元素之间间距相等,两端不留 |
SpaceAround |
每个元素周围间距相等 |
SpaceEvenly |
元素之间及两端间距全部相等 |
AlignItems |
效果(交叉轴) |
|---|---|
Stretch |
填满交叉轴(默认) |
FlexStart |
对齐到交叉轴起点 |
Center |
在交叉轴上居中 |
FlexEnd |
对齐到交叉轴终点 |
Baseline |
按文本基线对齐 |
换行与间隙¶
当子元素在主轴上即将溢出容器时,设置 Wrap = FlexWrap.Wrap
把它们流到第二行。ColumnGap 和 RowGap 的应用是一致的 ——
既作用于同一行内子元素之间,也作用于换行后的行与行之间:
class WrapGapDemo : Component
{
public override Element Render()
{
var tags = new[] {
"C#", "WinUI", "Reactor", ".NET", "XAML",
"Flex", "Layout", "Desktop", "Native"
};
return VStack(12,
SubHeading("Wrapping Tags"),
FlexRow(
tags.Select(tag =>
Border(tag)
.Padding(horizontal: 6, vertical: 12)
.Background(Theme.ControlFillSecondary)
.CornerRadius(12)
.WithKey(tag)
).ToArray()
) with {
Wrap = FlexWrap.Wrap,
ColumnGap = 8,
RowGap = 8
}
).Padding(24);
}
}

标签云、芯片行、面包屑溢出、筛选药丸 —— 任何元素个数会变、 容器宽度会收缩的地方 —— 都是适合换行的形态。如何用虚拟化渲染 长列表元素集合,见 Collections。
grow、shrink、basis¶
子元素上的 .Flex(grow:, shrink:, basis:) 决定它如何与兄弟元素
分配主轴空间:
grow—— 在所有 basis 尺寸排布完成后,子元素可以认领的额外 空间的份额(默认0—— 子元素保持其内容尺寸)。shrink—— 当 basis 总和超出容器时,子元素让出的缺口的份额 (默认1—— 子元素按比例收缩)。basis—— 应用 grow/shrink 之前的起始尺寸,单位为像素。null表示"先度量我的内容"(Yoga 的auto)。minWidth/minHeight—— 子元素主轴尺寸的显式下限。null(默认)表示 auto —— 见下文"最小尺寸(CSS §4.5)"。
class GrowShrinkDemo : Component
{
public override Element Render()
{
return VStack(16,
SubHeading("Grow: sidebar + content"),
FlexRow(
Border("Sidebar")
.Padding(16).Background(Theme.AccentTertiary)
.Flex(basis: 200, shrink: 0),
Border("Main content area")
.Padding(16).Background(Theme.CardBackground)
.Flex(grow: 1)
) with { ColumnGap = 8 },
SubHeading("Equal columns"),
FlexRow(
Border("Column 1").Padding(16).Background(Theme.SystemCriticalBackground).Flex(grow: 1),
Border("Column 2").Padding(16).Background(Theme.SystemSuccessBackground).Flex(grow: 1),
Border("Column 3").Padding(16).Background(Theme.AccentTertiary).Flex(grow: 1)
) with { ColumnGap = 8 }
).Padding(24);
}
}

侧边栏用 basis: 200, shrink: 0 固定宽度;内容列用 grow: 1
吸收剩余的一切。等宽列那一行给每个子元素 grow: 1,
于是可用空间被平均分配。
注意: 不带
basis的Flex(grow: 0)+Flex(shrink: 1)会默认basis: auto—— Yoga 会在第一遍度量子元素的内容尺寸,第二遍才做分布。 对于一行 200 个列表单元格的情况,第一遍度量会主导布局开销: 面板在做任何分配之前,都要先向每个单元格询问它的期望尺寸。 显式设置basis: 0(或任意像素值)可以把两遍合成一遍: 只做分布,不度量内容。这个差别在 5 个按钮的工具栏上无感, 但在每次列宽调整都要重建的 200 行表格头上就是决定性的。
最小尺寸(CSS §4.5)¶
默认情况下,basis: auto 的 flex 子元素不会收缩到其
min-content(最小内容) 尺寸以下 —— 即在不溢出自身内容的前提下
它能占据的最小尺寸。这符合 CSS Flexbox
规范 §4.5
("Automatic Minimum Size of Flex Items"),也正是大多数开发者在排布
一行卡片或一列区块时直觉上期待的行为:元素不会被压得比它的文字或图标更小。
Reactor 的 FlexPanel 按如下方式实现自动最小尺寸规则:
| basis | minWidth / minHeight | 最终下限 |
|---|---|---|
auto(默认) |
null(默认) |
min-content |
0 |
null |
0(短路 —— basis 本身已经不参与内容度量) |
像素值 N > 0 |
null |
min(N, min-content) |
| 任意值 | 显式 0 |
0(你主动放弃这个下限) |
| 任意值 | 显式 N > 0 |
N(你设置的硬性下限) |
自动最小尺寸规则只作用于主轴(FlexDirection.Row 时为宽度,
FlexDirection.Column 时为高度)。在交叉轴上,元素可以自由收缩到 0,
除非你显式设置了 minWidth/minHeight。
class MinSizingDemo : Component
{
public override Element Render() => VStack(16,
SubHeading("Default — items keep their min-content size"),
FlexRow(
Border("Long text that won't truncate").Flex(shrink: 1)
.Background(Theme.AccentTertiary).Padding(8),
Border("Short").Flex(shrink: 1)
.Background(Theme.SystemCriticalBackground).Padding(8)
) with { ColumnGap = 8 },
SubHeading("Opt out — minWidth: 0 lets items shrink below content"),
FlexRow(
Border("Long text that may be clipped").Flex(shrink: 1, minWidth: 0)
.Background(Theme.AccentTertiary).Padding(8),
Border("Short").Flex(shrink: 1, minWidth: 0)
.Background(Theme.SystemCriticalBackground).Padding(8)
) with { ColumnGap = 8 },
SubHeading("Explicit floor — never below 80px regardless of content"),
FlexRow(
Border("Hard floor").Flex(shrink: 1, minWidth: 80)
.Background(Theme.SystemSuccessBackground).Padding(8)
) with { ColumnGap = 8 }
).Width(360);
}
注意:性能。 计算
min-content需要在主 flex 算法运行之前, 对每个受影响的子元素做一次 WinUI 的Measure(width: marginH, height: ∞)。 对典型界面来说没问题,但元素非常多时可能产生影响。有两种方式跳过这次预度量:
- 显式设置
basis: 0—— 自动最小尺寸直接塌缩为 0,无需度量。- 显式设置
minWidth: 0(或minHeight: 0)—— 你主动放弃了这个下限。类型为
ScrollView或ScrollViewer的子元素下限始终是0: 它们的 min-content 会迫使虚拟化内容被实例化,那就违背了虚拟化的初衷。注意:显式
Width上 CSS 与 WinUI 的差异。 在 CSS 中,width: 200px; flex-shrink: 1; min-width: 0的元素允许收缩到 200 以下。 而在 WinUI 中,FrameworkElement.Width = 200是一个硬性尺寸约束 —— 无论可用空间多少,控件都会上报DesiredSize.Width = 200, 并在分配到的槽位里按 200 排列。因此在一个 FlexPanel 内部, 对于任何你希望它能收缩的元素,请优先用.Flex(basis: 200, ...)而不是.Width(200)。Yoga 读的是 basis;WinUI 的Width并不像 CSS 的width那样参与 flex 分配。
实践示例:带弹性空隙的工具栏¶
左对齐标题加右对齐按钮的工具栏,是最典型的 flex 形态。
用 Empty().Flex(grow: 1) 作为弹性空隙 —— 它吸收每一个多余像素,
把按钮推到末尾一侧:
class ToolbarDemo : Component
{
public override Element Render()
{
var (selected, setSelected) = UseState("Home");
return VStack(0,
FlexRow(
TextBlock("MyApp").Bold().Flex(shrink: 0),
Empty().Flex(grow: 1),
Button("Home", () => setSelected("Home")),
Button("Settings", () => setSelected("Settings")),
Button("About", () => setSelected("About"))
) with {
AlignItems = FlexAlign.Center,
ColumnGap = 8,
FlexPadding = new Thickness(16, 8, 16, 8)
},
TextBlock($"Current page: {selected}")
.Padding(24).FontSize(18)
);
}
}

空隙模式可以组合 —— 三个按钮之间放三个空隙,就能在容器上不用
SpaceBetween 的情况下让它们均匀分布;当只有部分子元素需要伸缩时,
这一点很重要。
Flex 与 VStack/HStack、Grid 的取舍¶
选容器要看它承诺的形态,而不是看哪个写起来省事:
class FlexVsStackDemo : Component
{
public override Element Render()
{
return VStack(16,
SubHeading("HStack (fixed spacing)"),
HStack(8,
Button("A"), Button("B"), Button("C")
),
SubHeading("FlexRow (justify + align)"),
FlexRow(
Button("A"), Button("B"), Button("C")
) with {
JustifyContent = FlexJustify.SpaceEvenly,
AlignItems = FlexAlign.Center
}
).Padding(24);
}
}

| 能力 | VStack / HStack |
FlexRow / FlexColumn |
Grid |
|---|---|---|---|
| 固定的子元素间距 | 支持 | 支持(ColumnGap / RowGap) |
支持 |
| 主轴分布 | 不支持 | 支持 | 逐行/逐列 |
| 交叉轴对齐 | 不支持 | 支持 | 每格 VAlign |
| 溢出换行 | 不支持 | 支持 | 隐式的行/列矩阵 |
| 按比例分配尺寸 | 不支持 | 支持(Flex(grow:)) |
支持(GridSize.Star) |
| 二维布局 | 不支持 | 不支持 | 支持 |
如果布局本身具有行列结构(表单、设置网格,或任何列要跨行对齐的场景),
请用 Grid。如果它是"沿一条轴分配子元素,另一条轴上要对齐",
那 FlexPanel 才是正确的形态。
模式¶
应用外壳 —— 固定侧边栏,流式内容¶
固定宽度的导航栏加吸收剩余空间的内容面板,是生产环境中最常见的
flex 形态。关键在于侧边栏上的 basis: 220, shrink: 0(锁定宽度、
防止塌缩),配合内容区的 grow: 1, basis: 0(在一次分布过程中拿走剩余空间):
class AppShellDemo : Component
{
public override Element Render()
{
return FlexRow(
// 侧边栏 —— 固定 220px,绝不小于它。
VStack(8,
TextBlock("Inbox").Padding(8),
TextBlock("Drafts").Padding(8),
TextBlock("Sent").Padding(8)
).Background(Theme.CardBackground)
.Flex(basis: 220, shrink: 0),
// 内容区 —— 显式 basis: 0 + grow: 1,只做一次分布,
// 不必先度量内部文本。
VStack(12,
Heading("Inbox"),
TextBlock("Three messages, one starred. The sidebar stays 220px wide; this column absorbs every spare pixel.")
).Padding(16)
.Flex(grow: 1, basis: 0)
) with { ColumnGap = 1 };
}
}
内容区上显式的 basis: 0 正对应上面那条注意事项 —— 不做内容度量,
只做分布。侧边栏的 shrink: 0 让导航栏在窗口窄于内容固有最小宽度时
依然保持 220 像素。
溢出即换行的响应式导航栏¶
断点之上,导航栏是单行;断点之下,元素换到第二行。
FlexWrap.Wrap + RowGap + ColumnGap 就是全部机制 ——
不需要任何媒体查询之类的东西:
class ResponsiveNavDemo : Component
{
public override Element Render()
{
// 当窄视口再也放不下一行时,换行就生效了。
// RowGap 和 ColumnGap 在换行后的行与行之间同样生效 —— 无需手写 margin。
return FlexRow(
Border("Home").Padding(8).Background(Theme.AccentTertiary),
Border("Catalog").Padding(8).Background(Theme.AccentTertiary),
Border("Pricing").Padding(8).Background(Theme.AccentTertiary),
Border("Docs").Padding(8).Background(Theme.AccentTertiary),
Border("About").Padding(8).Background(Theme.AccentTertiary),
Border("Contact").Padding(8).Background(Theme.AccentTertiary),
Border("Status").Padding(8).Background(Theme.AccentTertiary)
) with {
Wrap = FlexWrap.Wrap,
ColumnGap = 8,
RowGap = 8,
AlignItems = FlexAlign.Center
};
}
}
同样的形态也适用于标签行、面包屑溢出和工具栏溢出。如果断点需要驱动
完全不同的布局(比如折叠成汉堡菜单),可以配合
UseWindowSize;但仅就原地换行而言,Flex 容器本身就够了。
带弹性空隙的工具栏¶
上面已经演示过 —— 这里再提一遍,因为这个模式最清楚地体现了
主轴与交叉轴的分工。AlignItems = FlexAlign.Center 让高度不一的按钮
垂直居中;Empty().Flex(grow: 1) 把末尾的按钮推到右边缘。
两个互不相干的旋钮,同一个容器。
常见错误¶
用 FlexPanel 去做网格形态的布局¶
// 别这样 —— 行与行之间的二维对齐不是 Flex 的职责。
FlexColumn(
FlexRow(TextBlock("Name:"), TextBox(name, setName)) with { ColumnGap = 8 },
FlexRow(TextBlock("Email:"), TextBox(email, setEmail)) with { ColumnGap = 8 },
FlexRow(TextBlock("Phone:"), TextBox(phone, setPhone)) with { ColumnGap = 8 }
)
标签不会对齐 —— 每一行的 flex 分配都是各自独立的。任何列要跨行对齐的
布局(表单、设置网格、数据表)都用 Grid。FlexPanel
一次只能理解一条轴。
把 .Width(...) 和 .Flex(grow:) 混用¶
class WidthVsGrowWrong : Component
{
public override Element Render()
{
// 别这样:.Width(200) 设置的是 WinUI 的 Width,但在 FlexPanel 内部
// 子元素尺寸由 Flex(basis/grow/shrink) 决定。当 grow > 0 填满可用
// 空间时,这个 200 会被静默忽略。
return FlexRow(
Border("Stays 200?")
.Width(200) // 被忽略 —— grow 优先
.Flex(grow: 1)
.Background(Theme.SystemCriticalBackground)
) with { ColumnGap = 8 };
}
}
.Width(200) 设置的是底层 FrameworkElement.Width,但在 FlexPanel
内部,子元素的主轴尺寸来自 Flex(basis:, grow:, shrink:) ——
而 grow: 1 无论 Width 是多少都会填满可用空间。那个 200 被静默忽略了。
请把期望的尺寸编码成 basis:
class WidthVsGrowRight : Component
{
public override Element Render()
{
// 应该这样:把期望尺寸编码为 basis 并配 shrink: 0 ——
// 尺寸计算归 Flex 管,就不会有意料之外的覆盖。
return FlexRow(
Border("Exactly 200")
.Flex(basis: 200, shrink: 0)
.Background(Theme.SystemSuccessBackground)
.Padding(8)
) with { ColumnGap = 8 };
}
}
通用规则:在 FlexPanel 内部,优先用 .Flex(basis: N) 而不是 .Width(N)。
basis 才是 Yoga 算法读取的值;width 是 WinUI 度量过程读取的值,
两者只在某些情况下一致。
忘了 Wrap,于是在窄宽度下溢出¶
// 别这样 —— FlexPanel 默认是 NoWrap。在 600px 宽的窗口上
// 这一行会溢出并裁掉末尾的元素。
FlexRow(/* 7 个导航项 */) with { ColumnGap = 8 }
默认的 FlexWrap.NoWrap 只产生一行,当子元素总宽超过可用空间时就会溢出容器。
对于元素个数或窗口宽度会变化的行,请设置 Wrap = FlexWrap.Wrap。
对于实际不需要换行的行,这个开销为零 —— 一行放得下时 Yoga 会短路。
想要 FlexPadding 却用了 .Padding¶
FlexPadding 是 FlexPanel 自身的属性,由 Yoga 在分配子元素的同一次算法中度量。
而 .Padding(...) 修饰符设置的是底层 WinUI 的 FrameworkElement.Margin /
Padding,由 WinUI 在自己的那一遍里应用。要获得一致的容器内边距 ——
尤其是在涉及换行或 JustifyContent 时 —— 请用 FlexPadding。
.Padding(...) 留给不是 FlexPanel 的元素。
小贴士¶
从 VStack/HStack 起步。 它们生成的元素树更简单,也能覆盖大多数布局。 只有当你确实需要主轴分布、对齐、换行或按比例分配尺寸时,才上 FlexPanel。
用 ColumnGap 和 RowGap,而不是逐子元素加外边距。
间隙会自动作用在子元素之间 —— 没有首元素/末元素的不对称,
而且与换行组合时行为正确。
给每个固定宽度的元素加上 shrink: 0。 侧边栏、图标列或固定的操作按钮,
没有 shrink: 0 就会在窗口变窄时塌缩。把 shrink: 0 与 basis 搭配使用,
即可锁定精确尺寸。
用 Empty().Flex(grow: 1) 作弹性空隙。 这就是 flex 里的弹簧 ——
它吸收全部剩余空间,把兄弟元素推到对侧。与伸缩属性各异的兄弟元素组合时
行为同样正确。
容器属性请记得用 with { } 语法。 FlexElement 是一个 C# record,
所以要通过 FlexRow(...) with { JustifyContent = FlexJustify.Center }
来设置 JustifyContent、AlignItems、Wrap、ColumnGap 和 RowGap。
下一步¶
- Layout —— 前一篇:VStack、HStack、Grid、ScrollView 等核心容器
- Forms and Input —— 下一篇:受控输入控件、校验与表单组合
- Collections —— 用虚拟化渲染动态列表与网格
- Styling and Theming —— 主题令牌、深色/浅色模式,以及 flex 容器的轻量样式
- Input and Gestures —— flex 子元素上的指针与键盘事件