UWP实现可整列调整宽度的表格视图及动态加列需求
我之前也踩过这个坑——UWP原生确实没有自带的DataGrid/DataGridView控件,用ListView+GridSplitter模拟时很容易出现只能调整当前行对应列宽的问题。下面是我亲测有效的解决方案,既能实现整列同步调整宽度,又支持动态添加列:
1. 先搞定核心数据结构
首先我们需要一个类来存储每一列的配置信息(表头、绑定路径、宽度),还要实现属性变更通知,这样宽度变化时所有行的对应列才能同步更新:
using CommunityToolkit.Mvvm.ComponentModel; public class GridColumnConfig : ObservableObject { // 默认列宽设为150,可按需调整 private double _width = 150; // 表头文本 public string Header { get; set; } // 对应行数据的绑定路径 public string BindingPath { get; set; } // 列宽,用ObservableObject的SetProperty实现自动通知 public double Width { get => _width; set => SetProperty(ref _width, value); } }
这里用到了CommunityToolkit.Mvvm包里的ObservableObject,能省掉手动写INotifyPropertyChanged的冗余代码,非常实用。
接下来是ViewModel,需要两个核心集合:列配置集合和数据行集合,还要写动态添加列的方法:
using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.ObjectModel; public class MainViewModel : ObservableObject { // 列配置集合,所有行的列定义都绑定这个集合 public ObservableCollection<GridColumnConfig> ColumnConfigs { get; } = new(); // 用Dictionary存储行数据,方便动态列的绑定(如果是强类型实体,动态列会很麻烦) public ObservableCollection<Dictionary<string, object>> Rows { get; } = new(); // 动态添加列的方法 public void AddColumn(string header, string bindingPath) { // 添加新列的配置 ColumnConfigs.Add(new GridColumnConfig { Header = header, BindingPath = bindingPath }); // 给现有所有行添加新列的默认值,避免绑定报错 foreach (var row in Rows) { if (!row.ContainsKey(bindingPath)) row[bindingPath] = string.Empty; } } }
2. XAML布局:表头+内容列表
表头部分(带可拖拽的GridSplitter)
表头用Grid,列定义直接绑定ColumnConfigs,每个列里放表头文本和GridSplitter,关键是让GridSplitter和列配置关联起来:
<Grid DataContext="{Binding}"> <!-- 表头的列定义,绑定ColumnConfigs的Width --> <Grid.ColumnDefinitions> <ItemsControl ItemsSource="{Binding ColumnConfigs}"> <ItemsControl.ItemTemplate> <DataTemplate> <ColumnDefinition Width="{Binding Width, Mode=TwoWay}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid.ColumnDefinitions> <!-- 表头内容:每个列的文本+GridSplitter --> <ItemsControl ItemsSource="{Binding ColumnConfigs}" Grid.Column="0" Grid.ColumnSpan="{Binding ColumnConfigs.Count}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Grid IsItemsHost="True" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="5" /> <!-- GridSplitter的宽度 --> </Grid.ColumnDefinitions> <!-- 表头文本 --> <TextBlock Text="{Binding Header}" VerticalAlignment="Center" Padding="5" FontWeight="SemiBold"/> <!-- GridSplitter,把当前列配置绑定到Tag,方便后台处理 --> <controls:GridSplitter Grid.Column="1" Width="5" ResizeDirection="Columns" ResizeBehavior="PreviousAndCurrent" Tag="{Binding}" DragCompleted="GridSplitter_DragCompleted"/> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid>
这里的GridSplitter来自Microsoft.Toolkit.Uwp.UI.Controls,记得先在NuGet安装这个包。
内容ListView部分
ListView的每个Item用Grid,列定义同样绑定ColumnConfigs,这样所有行的列宽和表头完全同步:
<ListView ItemsSource="{Binding Rows}" Margin="0,10,0,0" BorderBrush="#EEE" BorderThickness="1"> <ListView.ItemTemplate> <DataTemplate> <Grid> <!-- 行的列定义,绑定ViewModel的ColumnConfigs --> <Grid.ColumnDefinitions> <ItemsControl ItemsSource="{Binding DataContext.ColumnConfigs, RelativeSource={RelativeSource AncestorType=ListView}}"> <ItemsControl.ItemTemplate> <DataTemplate> <ColumnDefinition Width="{Binding Width, Mode=TwoWay}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid.ColumnDefinitions> <!-- 行内容:每个列绑定对应的数据 --> <ItemsControl ItemsSource="{Binding DataContext.ColumnConfigs, RelativeSource={RelativeSource AncestorType=ListView}}" Grid.Column="0" Grid.ColumnSpan="{Binding DataContext.ColumnConfigs.Count, RelativeSource={RelativeSource AncestorType=ListView}}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Grid IsItemsHost="True" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <ContentControl Content="{Binding DataContext[BindingPath], RelativeSource={RelativeSource AncestorType=ListViewItem}}" Padding="5" VerticalAlignment="Center" BorderBrush="#EEE" BorderThickness="0,0,1,1"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid> </DataTemplate> </ListView.ItemTemplate> </ListView>
通过RelativeSource获取ViewModel的ColumnConfigs,确保所有行的列定义和表头保持一致,并且绑定同一个Width值。
3. 后台处理GridSplitter的拖拽事件
当拖拽GridSplitter调整宽度后,需要把当前列的实际宽度同步到GridColumnConfig的Width属性,这样所有行的对应列才会跟着更新:
private void GridSplitter_DragCompleted(object sender, DragCompletedEventArgs e) { var splitter = sender as GridSplitter; var columnConfig = splitter?.Tag as GridColumnConfig; if (columnConfig == null) return; // 获取表头Grid对应的列,拿到实际宽度 var parentGrid = splitter.Parent as Grid; var columnIndex = Grid.GetColumn(parentGrid); var headerGrid = parentGrid.Parent as Grid; var column = headerGrid.ColumnDefinitions[columnIndex]; // 更新列配置的Width,触发所有绑定的列定义更新 columnConfig.Width = column.ActualWidth; }
4. 动态添加列的调用
比如在页面加个按钮,点击时调用ViewModel的AddColumn方法:
<Button Content="添加新列" Click="AddColumnButton_Click" Margin="10"/>
后台代码:
private void AddColumnButton_Click(object sender, RoutedEventArgs e) { var vm = DataContext as MainViewModel; if (vm == null) return; // 生成新列的表头和绑定路径 var columnNum = vm.ColumnConfigs.Count + 1; vm.AddColumn($"新列{columnNum}", $"Column{columnNum}"); }
关键注意点
- 一定要用
ObservableCollection和ObservableObject,确保集合/属性变化时UI自动更新。 - GridSplitter的
ResizeBehavior设为PreviousAndCurrent,避免调整时影响其他不相关的列。 - 行数据用
Dictionary是动态列场景下最灵活的选择,如果用强类型实体,动态列的绑定会非常繁琐。
内容的提问来源于stack exchange,提问作者Tatyana Olshevskaya

