WPF自定义AutoGrid控件:动态网格布局的优雅解决方案 📅 发布时间:2026/9/15 2:27:26 👁 浏览次数: 最近在调 WPF 上位机界面又遇到那个绕不开的老需求界面上要动态显示一组工位状态卡片工位数不固定今天可能是 6 台明天加了产线就变成 14 台卡片还得按网格对齐。最笨的办法是每次在后台代码里往 Grid 的 RowDefinitions 和 ColumnDefinitions 里循环 Add代码写起来又臭又长数据一变就得重建一遍。也试过 UniformGrid它虽然省事但所有格子强制等大卡片内容一旦长度不一样布局马上变得很难看。后来我干脆自己写了一个自定义控件 AutoGrid专门解决“内容自动排成网格”的场景用了一段时间效果很稳这中间踩过的坑也不少整理出来供大家参考。AutoGrid 本质上是一个增强版的 Grid核心能力是“不手动定义行列也能自动按子元素数量生成行列并支持固定的行数或列数、支持行优先或列优先的排列方向还能无缝放进 ItemsControl 的 ItemsPanel 里配合数据绑定使用”。它解决的不只是上位机看板问题任何需要动态卡片、动态瓷砖、动态网格布局的 WPF 应用都可以直接用。1. 先聊聊我为什么要写这个控件Grid 和 UniformGrid 都不够用1.1 用 Grid 做动态布局代码又长又不好维护WPF 原生的 Grid 本身是非常强大的布局容器支持 Star、Auto、固定像素三种尺寸模式也支持跨行跨列。但它的强大建立在“行列结构明确”这个前提上。一旦行列数量在运行时才能确定代码就开始变味了。假设我要排 8 个工位卡片4 列 2 行用原生 Grid 写大概是这个感觉var grid new Grid(); for (int i 0; i 4; i) { grid.ColumnDefinitions.Add(new ColumnDefinition { Width new GridLength(1, GridUnitType.Star) }); } for (int i 0; i 2; i) { grid.RowDefinitions.Add(new RowDefinition { Height new GridLength(1, GridUnitType.Star) }); } for (int i 0; i list.Count; i) { var card CreateCard(list[i]); Grid.SetRow(card, i / 4); Grid.SetColumn(card, i % 4); grid.Children.Add(card); }如果行数列数还不固定比如要根据集合数量自动算那还要套一层 Math.Ceiling 去计算。这套逻辑本身不难但它是一坨“重复的、命令式的布局代码”散落在各个窗口里。今天这个页面要 4 列明天那个页面要 2 行每个地方写一遍维护起来很痛苦。所以第一个诉求很明确把这些“按数量自动生成行列”的逻辑收进控件内部对外只暴露一个 Rows 或 Columns 属性。使用方只需要声明“我要 4 列”剩下的由控件自己算。1.2 UniformGrid 的解与不解有人会说我用 UniformGrid 不行吗UniformGrid 确实是 WPF 里一个比较冷门但好用的控件它的排列逻辑就是“子元素数量决定行列然后所有格子等大排列”。放几个元素就是几个格子适合做九宫格、图标矩阵这类场景。但 UniformGrid 有两个明显的毛病第一尺寸模式单一。UniformGrid 没有 Star、Auto、像素的概念所有格子永远是等分的。你想让某一列稍微宽一点、某一行根据内容自适应高度它做不到。Grid 的那套 GridLength 控制能力在它身上完全不存在。第二排列方向不可控。UniformGrid 内部默认是从左到右、从上到下排列没有行优先和列优先的区分。但我做上位机看板的时候经常遇到“先从上往下填满第一列再填第二列”的需求类似仪表盘里的纵向分组UniformGrid 满足不了。AutoGrid 的目标就是同时吸收两边的优点保留 Grid 的行列定义能力同时像 UniformGrid 一样根据子元素数量自动生成行列结构还允许指定排列方向。1.3 我给 AutoGrid 定的目标清单综合上面的痛点写之前我列了一个功能清单也算是我对它验收的标准只设置 Columns行数根据子元素数量自动计算。只设置 Rows列数根据子元素数量自动计算。什么都不设置自动生成接近方阵的网格行列数由子元素总数开平方决定。支持 Orientation 属性Horizontal 表示行优先填充Vertical 表示列优先填充。保留 Grid 的跨行跨列能力用户手动参与定位时不能被自动分配逻辑覆盖。能在 ItemsControl 的 ItemsPanel 中使用配合 MVVM 实现纯数据驱动布局。后面所有代码都是围绕这六条展开的。2. AutoGrid 的布局核心在 Measure 阶段自动生成行列2.1 继承 Grid 还是 Panel我选了 Grid写自定义布局控件最纠结的就是继承关系。继承 Panel 的好处是完全掌控 MeasureOverride 和 ArrangeOverride想怎么排就怎么排坏处是 Grid 的附加属性体系Grid.Row、Grid.Column、Grid.RowSpan、Grid.ColumnSpan用起来没那么自然需要自己实现一套。我最终选择继承 Grid。原因很实际项目里已经有大量 XAML 和 DataTemplate 用了 Grid.Row 这类附加属性如果 AutoGrid 继承自普通 Panel这些写法在视觉树上会让人精神分裂——一会儿是 Grid 的附加属性一会儿又是 AutoGrid 自己的自动分配规则。继承 Grid 之后AutoGrid 本质上还是一个 Grid所有基于 Grid 的布局知识可以直接迁移过来学习成本最低。但继承 Grid 有个代价Grid 自身的 MeasureOverride 逻辑非常复杂你没法绕过它去自己控制每个子元素的位置只能在它开始测量之前先把行列定义准备好剩下的交给基类。这就是整个控件的核心思路——不是“自己画格子”而是“替 Grid 把格子画好”。2.2 行列数量是怎么算出来的行列计算逻辑是整个控件的大脑代码其实很短protected override Size MeasureOverride(Size availableSize) { // 结构未变化时避免重复重建这个 flag 的作用后面细说 if (!_isUpdating) { _isUpdating true; try { BuildStructure(); } finally { _isUpdating false; } } return base.MeasureOverride(availableSize); } private void BuildStructure() { int count InternalChildren.Count; if (count 0) return; int targetRows Rows; int targetColumns Columns; // 行列都没指定生成接近方阵的网格 if (targetRows 0 targetColumns 0) { targetColumns (int)Math.Ceiling(Math.Sqrt(count)); targetRows (int)Math.Ceiling(count / (double)targetColumns); } // 只指定了列数行数向上取整 else if (targetRows 0) { targetRows (int)Math.Ceiling(count / (double)targetColumns); } // 只指定了行数列数向上取整 else if (targetColumns 0) { targetColumns (int)Math.Ceiling(count / (double)targetRows); } EnsureRowDefinitions(targetRows); EnsureColumnDefinitions(targetColumns); AssignChildIndex(targetRows, targetColumns); }这个计算逻辑不复杂只有几个分支但它是整个控件所有行为的基础。重点说一下几个边界情况行列都没指定时如果用Math.Sqrt(count)算出来列数可能会出现列数比行数多的情况比如 7 个元素就是 3 列 3 行最后一个格子空着。这是符合预期的方阵布局本来就会留空。只指定列数时行数必须用Math.Ceiling向上取整否则元素会溢出到可视区域外面。比如 10 个元素排 4 列3 行能容纳 12 个行数算出来是 2.5向上取整为 3。如果显式设置了RowDefinitions和ColumnDefinitions理论上 Rows 和 Columns 属性就可以不设置但为了行为一致我建议这两者只取其一混用容易产生认知负担。2.3 行列定义生成的防抖与缓存这里要提到刚才代码里的_isUpdating标志。我最初写的第一版没这个标志直接就在 MeasureOverride 里调用 BuildStructure结果窗口一加载 CPU 直接拉满布局线程死循环。原因是这样Grid 在 MeasureOverride 时会读取RowDefinitions和ColumnDefinitions。如果我在 MeasureOverride 过程中反复向这两个集合添加或删除元素Grid 会认为行列结构变了于是再次触发 MeasureMeasure 又触发 BuildStructureBuildStructure 又修改集合循环不止。解决办法很简单BuildStructure 里先检查当前定义数量和目标数量一致就直接返回不一致才更新。_isUpdating标志则是为了防住“数量一致但内容需要刷新”的场景比如 ChildMargin 变了或者子元素的排列方向变了这些情况不涉及行列数量变化但需要重新分配索引。行列定义本身也做了缓存不会每次 Measure 都 new 一堆对象private static readonly GridLength StarLength new GridLength(1, GridUnitType.Star); private void EnsureRowDefinitions(int rows) { while (RowDefinitions.Count rows) { RowDefinitions.Add(new RowDefinition { Height StarLength }); } while (RowDefinitions.Count rows) { RowDefinitions.RemoveAt(RowDefinitions.Count - 1); } } private void EnsureColumnDefinitions(int columns) { while (ColumnDefinitions.Count columns) { ColumnDefinitions.Add(new ColumnDefinition { Width StarLength }); } while (ColumnDefinitions.Count columns) { ColumnDefinitions.RemoveAt(ColumnDefinitions.Count - 1); } }如果使用方在 XAML 里手动定义了RowDefinition或ColumnDefinition这些代码会保留用户定义的部分只补充不够的行列超出部分再截断。自动补充的行列默认是 Star 等分这也是最常见的网格布局需求。2.4 Orientation 决定填充方向行优先还是列优先这个属性很多人不理解有什么用。我举个实际场景设备看板上有一个“报警信息区”要求纵向排列第一条报警在最上面一列放不下再换到第二列。这就是典型的行列优先需求但用原生 Grid 实现非常繁琐你要先知道报警数量再手动计算每个报警的 Grid.Row 和 Grid.Column。AutoGrid 处理起来就一行 XAML 的事controls:AutoGrid Rows8 OrientationVertical !-- 子元素会先填满第一列再填第二列 -- /controls:AutoGrid实现上其实就是索引映射公式不同。Horizontal 模式下子元素按照row index / columns、col index % columns来分配位置Vertical 模式下按照row index % rows、col index / rows来分配位置private void AssignChildIndex(int targetRows, int targetColumns) { int count InternalChildren.Count; for (int i 0; i count; i) { var child InternalChildren[i]; // 附加属性 AutoIndex 为 false 的子元素跳过自动分配 if (!GetAutoIndex(child)) continue; int row, col; if (Orientation Orientation.Horizontal) { row i / targetColumns; col i % targetColumns; } else { row i % targetRows; col i / targetRows; } // 值没变化就不重复 Set避免无意义触发布局 if (Grid.GetRow(child) ! row) Grid.SetRow(child, row); if (Grid.GetColumn(child) ! col) Grid.SetColumn(child, col); } }注意这里有个细节我在设置附加属性之前先读取当前值如果一样就跳过去。原因稍后在踩坑章节详细讲简单说就是 WPF 里强制设置一次 Grid.Row 即使值相同也可能触发父级重新布局高频场景下白白浪费性能。3. 让它能打跨行跨列与 ItemsControl 数据绑定场景3.1 附加属性 AutoIndex手动定位和自动定位的开关一个网格控件如果只能强制平均分配每个子元素的格子那它是不完整的。实际场景里经常有某个卡片要特别显眼占两个格子高或者某个面板需要跨两列展示。Grid 原生支持 Grid.RowSpan 和 Grid.ColumnSpanAutoGrid 既然继承了 Grid天然就支持这两个属性。问题是自动分配索引时如果某个子元素希望手动指定位置两者会冲突。所以我给 AutoGrid 加了一个附加属性AutoIndex默认是 true表示由 AutoGrid 自动分配 Row 和 Column一旦设置为 false就表示这个子元素自己管理位置AutoGrid 不碰它。public static readonly DependencyProperty AutoIndexProperty DependencyProperty.RegisterAttached( AutoIndex, typeof(bool), typeof(AutoGrid), new FrameworkPropertyMetadata( true, FrameworkPropertyMetadataOptions.AffectsParentMeasure)); public static bool GetAutoIndex(DependencyObject obj) { return (bool)obj.GetValue(AutoIndexProperty); } public static void SetAutoIndex(DependencyObject obj, bool value) { obj.SetValue(AutoIndexProperty, value); }用法大概是这样的controls:AutoGrid Columns4 Border Grid.Row0 Grid.Column0 Grid.RowSpan2 controls:AutoGrid.AutoIndexFalse !-- 这个块手动占据第一格并向下跨两行 -- /Border Border controls:AutoGrid.AutoIndexTrue/ Border controls:AutoGrid.AutoIndexTrue/ /controls:AutoGrid这里要说明一个边界场景AutoIndex 为 true 的子元素仍然按顺序向后填充并不会因为某个手动定位的块占了位置就智能跳过。所以如果手动定位的元素和自动定位的元素混在一起布局很可能会重叠。我的建议是要么全部自动要么少量手动元素放在自动元素之前把它当成“钉在网格左上角的固定块”。3.2 在 ItemsControl 中作为 ItemsPanelAutoGrid 最有价值的用法是放进 ItemsControl 的 ItemsPanel 里实现真正的数据驱动布局。这样一来后台只需要维护一个 ObservableCollection集合增删元素界面上的网格自动重排完全不用写一行布局代码。先看 ViewModel 这一层public class DeviceCardViewModel { public string DeviceName { get; set; } public string DeviceStatus { get; set; } public Brush StatusBrush { get; set; } } public class MainViewModel { public ObservableCollectionDeviceCardViewModel Devices { get; set; } public MainViewModel() { Devices new ObservableCollectionDeviceCardViewModel(); // 模拟从 PLC 或 MES 拉到的实时数据 Devices.Add(new DeviceCardViewModel { DeviceName 工位 01, DeviceStatus 运行中, StatusBrush Brushes.Green }); // ... 更多设备 } }XAML 这一层就清爽了ItemsControl ItemsSource{Binding Devices} ItemsControl.ItemsPanel ItemsPanelTemplate controls:AutoGrid Columns4 ChildMargin4/ /ItemsPanelTemplate /ItemsControl.ItemsPanel ItemsControl.ItemTemplate DataTemplate Border BorderBrush{Binding StatusBrush} BorderThickness2 CornerRadius4 Padding8 StackPanel TextBlock Text{Binding DeviceName} FontWeightBold FontSize14/ TextBlock Text{Binding DeviceStatus} Foreground{Binding StatusBrush} Margin0,4,0,0/ /StackPanel /Border /DataTemplate /ItemsControl.ItemTemplate /ItemsControl这段代码里有几个细节值得展开第一在 ItemsPanel 中使用的控件必须是 Panel 的子类Grid 天然满足所以 AutoGrid 可以直接塞进去。ItemsControl 每生成一个容器项都会向 AutoGrid 的 Children 里添加一个元素AutoGrid 的 MeasureOverride 会在合适的时机被触发然后重建行列结构。第二AutoGrid 不需要设置 Rows因为 ItemsControl 里到底有多少项是在运行时由数据集合决定的。我们只需要声明“我要 4 列”行数交给 AutoGrid 去算。第三ChildMargin 是我在 AutoGrid 上额外加的一个依赖属性类型是 Thickness作用是在 Measure 之前统一给所有子元素设置 Margin。如果不做这个每个卡片都要包一层带 Margin 的 Border或者手工给每个子元素设置 MarginXAML 会膨胀得很厉害。3.3 常见误区AutoGrid 不是 ItemsControl写博客的过程里我回答过几次问题发现很多人把 AutoGrid 和 ItemsControl 混在一起。这里必须说清楚AutoGrid 是一个 Panel它只负责布局由它托管的一组子元素本身不提供 ItemsSource 属性也听不懂数据集合增删的通知。如果你只是要在窗口里固定放几个 Grid 的子元素那直接在 XAML 里写controls:AutoGrid Columns3 Border/ Border/ Border/ /controls:AutoGrid如果你要根据数据集合动态生成卡片那必须用一个宿主控件最常用的就是 ItemsControl把 AutoGrid 塞进它的 ItemsPanel 里数据驱动由 ItemsControl 负责网格排列由 AutoGrid 负责各司其职。另外一个相关的“坑”是不要试图在 AutoGrid 里直接监听任何数据集合变化也最好不要自己往InternalChildren里直接 Add。Panel 的 Children 集合在 ItemsControl 场景下由 WPF 的 ItemContainerGenerator 管理手动干预会导致界面和数据错乱。4. 实测中的几个坑布局循环、附加属性残留、大数据量4.1 布局循环Measure 里重建 Definition 的危险这个坑前面已经提到了但我觉得有必要单独拿出来再说一遍因为它太隐蔽了。现象是窗口启动后 CPU 占用率飙升到 100%但界面看起来一切正常通过 Visual Studio 的实时可视化树也看不出明显异常只有用 Performance Profiler 才会发现 Layout pass 在无限循环。根源在于我最初在 BuildStructure 里用了类似这样的代码RowDefinitions.Clear(); for (int i 0; i rows; i) RowDefinitions.Add(new RowDefinition());RowDefinitions集合一旦发出变化通知Grid 就认为自己的布局结构需要重新计算于是请求新的一轮 Measure新的一轮 Measure 又进到 BuildStructure又触发一次集合变化循环往复。正确的做法是每次都先判断“目标数量和当前数量是否一致”不一致才操作集合并且用一个_isUpdating标志位在 BuildStructure 执行期间避免集合变化通知再次进入 BuildStructure。代码里的 EnsureRowDefinitions 和 EnsureColumnDefinitions 用的 while 增删法就是为了避免无谓 Clear 操作和重复 new 对象。如果你的 AutoGrid 在运行时行列数量会动态变化建议参考我第 6 章扩展的版本用字符串属性解析行列定义而不是每次在 Measure 里去重建。4.2 Grid.Row 的残留容器复用导致的错位这个坑在 ItemsControl 场景下特别容易踩。WPF 的 ItemsControl 在数据集合变化时会复用已经生成过的容器元素而不是全部销毁重建。复用的意思是之前排在第 5 位的那个 Border重新生成为第 2 位时它身上的 Grid.Row 还残留着 4因为之前被 AutoGrid 设置为 4。AutoGrid 的 AssignChildIndex 在每次 Measure 时都会重新计算所有子元素的位置所以正常情况它能纠正这些残留值。但我在最初的版本里为了“优化性能”只对新增的子元素设置位置没有对已有子元素做校正结果就是数据更新后卡片位置乱掉。最终方案就是现在代码里的做法每次 BuildStructure 都遍历所有子元素计算期望的 Row 和 Column先和当前值比较不一样才设置。这样既不会因为反复 Set 触发额外布局也能保证位置一定是正确的。4.3 大数据量下的性能AutoGrid 不虚拟化AutoGrid 本身不继承 VirtualizingPanel所以它不具备 UI 虚拟化能力。数据量在几十个以内的时候体感不明显几百个的时候 Measure 和 Arrange 的时间会指数上升上千个的时候界面基本上就卡得不能看了。我实际项目里遇到过一次性加载 200 多个工位卡片的情况打开页面的瞬间要等两秒钟虽然能接受但明显不够流畅。后来我换了个思路上位机看板并不需要一次性展示所有工位我可以让用户按产线筛选或者加一个分页控件一次只显示 30 个工位。这样 AutoGrid 的布局压力就降到了完全无感的水平。如果你的场景确实需要大数据量网格并且要求流畅滚动那不要指望 AutoGrid 能解决所有问题考虑换成 VirtualizingStackPanel 配合 DataGrid或者自己继承 VirtualizingPanel 重写虚拟化逻辑。那是一个更大的工程量不适合塞进一个布局控件里。5. 实战演示上位机工位状态网格搭建5.1 需求与字段设计假设现场有 12 个工位按 4 列布局每张卡片显示设备名称、运行状态、关键参数。数据来自 MES 接口可能是实时刷新的。工位数不固定后续可能扩展到 16 台甚至更多。这个场景如果用原生 Grid每次数据刷新都要清空 Children 重建。用 AutoGrid 之后后台只需要维护一个 ObservableCollection界面自动响应。我整理一下要点ViewModel 里设备卡片集合用 ObservableCollection这是 WPF 数据绑定的标准做法。卡片外观用 DataTemplate 定义数据变化时通过属性通知刷新控件布局部分完全不用管。AutoGrid 只负责按 4 列自动生成行并把每个卡片放到对应的 Row 和 Column 上。5.2 XAML 布局关键代码完整可跑的 XAML 大概是下面这样这里我加了 2 行简要注释Window.Resources Style x:KeyStatusBorderStyle TargetTypeBorder Setter PropertyCornerRadius Value4/ Setter PropertyPadding Value10/ Setter PropertyBorderThickness Value2/ Setter PropertyMargin Value4/ Setter PropertyBackground ValueWhite/ /Style /Window.Resources Grid ItemsControl ItemsSource{Binding WorkCells} ItemsControl.ItemsPanel ItemsPanelTemplate controls:AutoGrid Columns4/ /ItemsPanelTemplate /ItemsControl.ItemsPanel ItemsControl.ItemTemplate DataTemplate Border Style{StaticResource StatusBorderStyle} BorderBrush{Binding StatusColor} StackPanel TextBlock Text{Binding CellName} FontSize16 FontWeightBold/ TextBlock Text{Binding StatusText} Foreground{Binding StatusColor} Margin0,6,0,0/ TextBlock Text{Binding CurrentWorkOrder} FontSize12 Foreground#666 Margin0,4,0,0/ /StackPanel /Border /DataTemplate /ItemsControl.ItemTemplate /ItemsControl /Grid5.3 后台数据刷新与线程处理后台 ViewModel 里关键点如下。注意 ObservableCollection 的增删和属性更新必须发生在 UI 线程上如果从后台线程拿到 PLC 数据要用 Dispatcher 调度回 UI 线程public class WorkCellViewModel : INotifyPropertyChanged { public string CellName { get; set; } private string _statusText; public string StatusText { get _statusText; set { _statusText value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusText))); } } private Brush _statusColor; public Brush StatusColor { get _statusColor; set { _statusColor value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusColor))); } } public event PropertyChangedEventHandler PropertyChanged; } // 后台线程刷新示例实际项目中建议封装一个 DispatcherHelper Application.Current.Dispatcher.Invoke(() { cell.StatusText 报警; cell.StatusColor Brushes.Red; });这里有个容易被忽略的问题如果每次 PLC 刷新你都往集合里 RemoveAt/AddAutoGrid 会频繁重建行列定义虽然数据量不大时只是轻微卡顿但高频刷新下还是会有明显开销。更合理的做法是只更新每个卡片对象的属性让 WPF 走属性通知刷新 UI而不是重新生成集合。5.4 运行效果与在线调整实际跑起来后12 个工位在 4 列布局下会自动排成 3 行 4 列每个卡片等比占满网格。如果某一天需求变成“工位分组展示每组占一列”我可以直接把 Columns 改成 3或者设置 Rows 和 Orientation 来调整排列方向改动量非常小。如果你需要固定 2 行比如大屏上只显示上下两排可以直接这样写controls:AutoGrid Rows2 OrientationHorizontal/这样 AutoGrid 会自动计算列数所有卡片先从左到右填满第一行再从左到右填满第二行。视觉上比横向滚动列表整齐得多也不需要人肉数行数。6. 还可以扩展的几个方向6.1 支持字符串形式的行列权重配置目前 AutoGrid 自动添加的行列都是1*也就是等分。但实际布局经常有“左边这一列固定 300 像素中间自适应右边占两倍空间”的需求。一个比较实用的扩展是给 AutoGrid 增加一个字符串类型的属性比如RowDefinitionsSourceAuto,*,2*,100在 BuildStructure 的时候解析字符串生成对应的 RowDefinition。GridLengthConverter 可以直接把字符串转成 GridLength代码量不大private GridLength ParseGridLength(string token) { var converter new GridLengthConverter(); return (GridLength)converter.ConvertFromString(token.Trim()); }这个扩展建议保留我前面 EnsureRowDefinitions 的缓存逻辑不要每次 Measure 都重新解析字符串可以在属性变化回调里解析一次并缓存结果。6.2 数据驱动的跨行跨列AutoGrid 的常规用法是每个卡片占一个格子但有些仪表盘需求是“某个重要指标卡要占两个格子”。配合数据绑定可以让 ViewModel 里的某个卡片对象提供 RowSpan 和 ColumnSpan 属性然后在 DataTemplate 里绑定DataTemplate Border Grid.RowSpan{Binding RowSpan} Grid.ColumnSpan{Binding ColumnSpan} controls:AutoGrid.AutoIndexFalse ... /Border /DataTemplate注意我特意设置了AutoIndexFalse表示这个卡片不参与自动索引它的 Row 和 Column 需要在 ViewModel 里手动指定。这样做的好处是卡片的位置和跨幅都由数据驱动适合做中控大屏、数据驾驶舱这类灵活的宫格布局。6.3 虚拟化与大数据量下的取舍前面说过 AutoGrid 没有虚拟化能力。如果你确实要做几千个元素的网格我的个人建议是不要强行扩展 AutoGrid可以换个思路数据分页每次只加载一页。用 DataGrid 代替自定义卡片DataGrid 自带虚拟化。如果一定要自定义卡片网格可以考虑自己实现一个继承 VirtualizingPanel 的控件但这个工程量大得多需要处理 IScrollInfo、容器回收、缓存等多个底层问题。如果说这个控件还要再加一个最实用的功能我会把行列权重字符串解析做了因为实际项目里“等分”并不是万能答案固定宽度加自适应混合的场景太常见了。至于跨行跨列的数据驱动和虚拟化属于可选的进阶需求看项目规模决定要不要投入。你现在拿到这份 AutoGrid 的实现把前三章的核心代码读透已经足够覆盖绝大多数上位机看板和动态卡片布局了。