你是否在开发WPF应用时遇到过这样的需求:需要在DataGrid上方显示分组表头,将相关的列进行逻辑分组?比如商品管理系统中,需要将"基本信息"、"价格信息"、"库存信息"等相关列进行分组显示,让用户一眼就能看出数据的逻辑结构。
传统的DataGrid只能显示单行列头,无法满足复杂业务场景下的分组展示需求。今天就来分享一个完整可用的解决方案,教你如何在WPF中优雅地实现DataGrid分组表头合并功能。
在实际的业务系统中,数据表格往往包含大量列,这些列按功能可以分为不同的逻辑组。比如:
传统方式的问题:
❌ 列头信息扁平化,缺乏层次感
❌ 用户难以快速理解数据结构
❌ 界面显得杂乱无章
我们的解决方案核心思路是:
C#// 分组表头模型
public class GroupHeader : INotifyPropertyChanged
{
public string Title { get; set; } = "";
public int StartColumnIndex { get; set; } // 起始列索引(包含)
public int ColumnSpan { get; set; } // 跨越列数
private double _width;
public double Width
{
get => _width;
set { _width = value; PropertyChanged?.Invoke(this, new(nameof(Width))); }
}
public event PropertyChangedEventHandler? PropertyChanged;
}
// 商品数据模型
public class Product
{
public string ProductCode { get; set; } = "";
public string ProductName { get; set; } = "";
public decimal PriceExclTax { get; set; }
public decimal PriceInclTax { get; set; }
public int Stock { get; set; }
}
// 视图模型
public class MainViewModel
{
public ObservableCollection<Product> Items { get; } = new();
public ObservableCollection<GroupHeader> Groups { get; } = new();
}
💡 设计要点:
GroupHeader实现INotifyPropertyChanged接口,确保宽度变化时UI能及时更新StartColumnIndex和ColumnSpan定义了分组在DataGrid中的位置范围ObservableCollection让集合变化自动反映到UIXML<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 分组表头 -->
<ItemsControl Grid.Row="0" ItemsSource="{Binding Groups}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#CFD8DC"
BorderThickness="1,1,1,0"
CornerRadius="6,6,0,0"
Background="{StaticResource GroupHeaderBackground}"
Margin="1,4,1,0"
Effect="{StaticResource GroupHeaderShadow}">
<TextBlock Text="{Binding Title}"
FontWeight="Bold"
FontSize="14"
Foreground="#37474F"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="8,12"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Width" Value="{Binding Width}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
<!-- 数据表格 -->
<DataGrid x:Name="DG" Grid.Row="1"
AutoGenerateColumns="False"
ItemsSource="{Binding Items}">
<DataGrid.Columns>
<DataGridTextColumn Header="编码" Binding="{Binding ProductCode}" Width="*" />
<DataGridTextColumn Header="名称" Binding="{Binding ProductName}" Width="2*" />
<DataGridTextColumn Header="不含税价" Binding="{Binding PriceExclTax}" Width="*" />
<DataGridTextColumn Header="含税价" Binding="{Binding PriceInclTax}" Width="*" />
<DataGridTextColumn Header="库存" Binding="{Binding Stock}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</Grid>
🎯 关键技术点:
Grid的行定义,分组表头占用Auto高度,DataGrid占用剩余空间ItemsControl.ItemContainerStyle中绑定Width属性,实现动态宽度调整C#public partial class MainWindow : Window
{
private readonly MainViewModel _vm = new();
public MainWindow()
{
InitializeComponent();
DataContext = _vm;
// 初始化数据
InitializeData();
// 关键:监听DataGrid加载完成和列宽变化
DG.Loaded += (_, __) =>
{
RecalcGroupWidths();
foreach (var col in DG.Columns)
{
DependencyPropertyDescriptor
.FromProperty(DataGridColumn.ActualWidthProperty, typeof(DataGridColumn))
?.AddValueChanged(col, (_, __2) => RecalcGroupWidths());
}
};
}
private void InitializeData()
{
// 添加示例数据
_vm.Items.Add(new Product { ProductCode = "A001", ProductName = "机加工件",
PriceExclTax = 100, PriceInclTax = 113, Stock = 50 });
_vm.Items.Add(new Product { ProductCode = "A002", ProductName = "注塑件",
PriceExclTax = 80, PriceInclTax = 90.4m, Stock = 120 });
// 定义分组:基本信息(0-1列)、价格(2-3列)、库存(4列)
_vm.Groups.Add(new GroupHeader { Title = "基本信息", StartColumnIndex = 0, ColumnSpan = 2 });
_vm.Groups.Add(new GroupHeader { Title = "价格", StartColumnIndex = 2, ColumnSpan = 2 });
_vm.Groups.Add(new GroupHeader { Title = "库存信息", StartColumnIndex = 4, ColumnSpan = 1 });
}
// 🔥 核心方法:重新计算分组宽度
private void RecalcGroupWidths()
{
foreach (var g in _vm.Groups)
{
// 获取指定范围内的列,计算总宽度
var cols = DG.Columns.Skip(g.StartColumnIndex).Take(g.ColumnSpan);
g.Width = cols.Sum(c => c.ActualWidth);
}
}
}
⚡ 技术亮点:
DependencyPropertyDescriptor监听列宽变化,比事件处理更高效Skip().Take()方法优雅地获取指定范围的列Sum()聚合函数快速计算总宽度
XML<Window.Resources>
<!-- 阴影效果 -->
<DropShadowEffect x:Key="GroupHeaderShadow"
BlurRadius="6"
ShadowDepth="2"
Direction="270"
Color="#B0BEC5"
Opacity="0.3"/>
<!-- 渐变背景 -->
<LinearGradientBrush x:Key="GroupHeaderBackground" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#F8F9FA" Offset="0"/>
<GradientStop Color="#E9ECEF" Offset="1"/>
</LinearGradientBrush>
</Window.Resources>
DataGrid.Loaded事件后才能获取到正确的ActualWidthDependencyPropertyDescriptor时要注意在适当时机移除监听器C#private System.Timers.Timer _resizeTimer = new(50); // 50ms防抖
private void RecalcGroupWidthsDebounced()
{
_resizeTimer.Stop();
_resizeTimer.Elapsed += (_, __) => Dispatcher.Invoke(RecalcGroupWidths);
_resizeTimer.Start();
}
这个方案特别适用于:
通过这个完整的解决方案,我们实现了:
这个方案不仅解决了WPF中DataGrid分组表头的技术难题,更重要的是提供了一套可复用的设计模式。无论你是C#初学者还是资深开发者,都能从中获得实用的开发技巧。
你在项目中是如何处理复杂表格展示的? 欢迎在评论区分享你的经验,或者遇到的技术挑战。如果觉得这篇文章对你有帮助,请转发给更多需要的同行!
关注我,获取更多C#开发实战技巧和最佳实践分享!
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!