1. 引言
本文介绍如何使用DataGrid进行合并单元格数据。
2. DataGrid 概述
DataGrid 是一种用于在应用程序中显示和操作数据表格的控件。它常用于显示来自数据库或集合的数据,以便用户可以查看、编辑、排序和筛选这些数据。DataGrid 提供了一种灵活的方式来呈现和管理数据,使得开发人员可以快速构建用户界面。
3. 合并单元格的基本概念
合并单元格是指将相邻的单元合并为一个大的单元格,这样可以使得表格数据呈现出更清晰的结构和逻辑关系。
4. 实现合并单元格的方法
-
4.1. 定义数据结构
-
/// <summary> /// 数据类 /// </summary> public class EquipmentInfo { /// <summary> /// 部门名称 /// </summary> public string DepartmentName { get; set; } /// <summary> /// 设备名称 /// </summary> public string EquipmentName { get; set; } /// <summary> /// 设备类型 /// </summary> public string EquipmentType { get; set; } /// <summary> /// 设备状态 /// </summary> public string EquipmentStatus { get; set; } public bool Equals(EquipmentInfo? other) { if (other == null) return false; return this.DepartmentName == other.DepartmentName && this.EquipmentName == other.EquipmentName && this.EquipmentType == other.EquipmentType; } }
-
4.2. 定义计算单元格高度转化类
-
/// <summary> /// 单元格高度转换类 /// </summary> public class DataGridCellHeightConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { int height = 40; int finalHeight = 40; if (value is null) return height; if (value is CollectionViewGroup viewGroup) { var itemsCount = viewGroup.Items.Count; finalHeight = height * itemsCount; } return finalHeight; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return DependencyProperty.UnsetValue; } }
-
4.3. 定义单元格数据源转换类
-
/// <summary> /// 单元格数据源转换类 /// </summary> public class DataGridCellDataSourceConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is null || parameter is null) return Enumerable.Empty<object>(); var readOnlyCollection = value as ReadOnlyObservableCollection<object>; if (readOnlyCollection == null) return Enumerable.Empty<object>(); if (int.TryParse(parameter.ToString(), out int displayIndex)) { ObservableCollection<EquipmentInfo> collection = new(); foreach (var item in readOnlyCollection) { collection.Add((EquipmentInfo)item); } var view = CollectionViewSource.GetDefaultView(collection); if (displayIndex == 1) { view.GroupDescriptions.Clear(); view.GroupDescriptions.Add(new PropertyGroupDescription("EquipmentType")); return view.Groups; } if (displayIndex == 2) { view.GroupDescriptions.Clear(); view.GroupDescriptions.Add(new PropertyGroupDescription("EquipmentName")); return view.Groups; } if (displayIndex == 3) { view.GroupDescriptions.Clear(); view.GroupDescriptions.Add(new PropertyGroupDescription("EquipmentStatus")); return collection; } return collection; } return Enumerable.Empty<object>(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return DependencyProperty.UnsetValue; } }
4.4 定义相关资源样式
-
<Window.Resources> <SolidColorBrush x:Key="DataGrid.Background" Color="LightGray" /> <Style x:Key="DataGrid.Base" TargetType="{x:Type DataGrid}"> <Setter Property="AutoGenerateColumns" Value="False" /> <Setter Property="RowHeaderWidth" Value="0" /> <Setter Property="CanUserAddRows" Value="False" /> <Setter Property="CanUserDeleteRows" Value="False" /> <Setter Property="CanUserResizeColumns" Value="False" /> <Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" /> <Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" /> <Setter Property="VirtualizingPanel.CacheLengthUnit" Value="Item" /> <Setter Property="VirtualizingPanel.ScrollUnit" Value="Item" /> <Setter Property="ScrollViewer.CanContentScroll" Value="True" /> <Setter Property="DataGrid.GridLinesVisibility" Value="Horizontal" /> <Setter Property="DataGrid.HeadersVisibility" Value="Column" /> <Setter Property="DataGrid.RowDetailsVisibilityMode" Value="VisibleWhenSelected" /> <Setter Property="ScrollViewer.CanContentScroll" Value="true" /> <Setter Property="ScrollViewer.PanningMode" Value="Both" /> <Setter Property="Stylus.IsFlicksEnabled" Value="False" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGrid}"> <Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True"> <ScrollViewer Name="DG_ScrollViewer" Focusable="false"> <ScrollViewer.Template> <ControlTemplate TargetType="{x:Type ScrollViewer}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Button Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" Command="{x:Static DataGrid.SelectAllCommand}" Focusable="false" Style="{DynamicResource {ComponentResourceKey ResourceId=DataGridSelectAllButtonStyle, TypeInTargetAssembly={x:Type DataGrid}}}" Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.All}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" /> <Border Grid.Row="0" Grid.Column="1" Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Column}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"> <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" /> </Border> <ScrollContentPresenter Name="PART_ScrollContentPresenter" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" CanContentScroll="{TemplateBinding CanContentScroll}" /> <ScrollBar Name="PART_VerticalScrollBar" Grid.Row="1" Grid.Column="2" Maximum="{TemplateBinding ScrollableHeight}" Orientation="Vertical" ViewportSize="{TemplateBinding ViewportHeight}" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" /> <Grid Grid.Row="2" Grid.Column="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="{Binding NonFrozenColumnsViewportHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <ScrollBar Name="PART_HorizontalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableWidth}" Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" /> </Grid> </Grid> </ControlTemplate> </ScrollViewer.Template> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" /> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="DataGrid.VerticalGridLinesBrush" Value="{Binding HorizontalGridLinesBrush, RelativeSource={RelativeSource Self}}" /> <Style.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="ItemsControl.IsGrouping" Value="true" /> <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false" /> </MultiTrigger.Conditions> <Setter Property="ScrollViewer.CanContentScroll" Value="false" /> </MultiTrigger> </Style.Triggers> </Style> <!-- 外部DataGrid --> <Style x:Key="DataGrid.Main" BasedOn="{StaticResource DataGrid.Base}" TargetType="{x:Type DataGrid}"> <Setter Property="Padding" Value="0" /> <Setter Property="BorderBrush" Value="Black" /> <Setter Property="BorderThickness" Value="2,2,1,2" /> <Setter Property="HorizontalGridLinesBrush" Value="Transparent" /> <Setter Property="VerticalGridLinesBrush" Value="Black" /> <Setter Property="GridLinesVisibility" Value="All" /> <Setter Property="Background" Value="{StaticResource DataGrid.Background}" /> </Style> <!-- 列标题样式 --> <Style x:Key="DataGridColumnHeaderStyle" TargetType="{x:Type DataGridColumnHeader}"> <Setter Property="Background" Value="#1D90CE" /> <Setter Property="HorizontalContentAlignment" Value="Center" /> <Setter Property="Foreground" Value="White" /> <Setter Property="FontSize" Value="15" /> <Setter Property="Height" Value="30" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGridColumnHeader}"> <Border Height="{TemplateBinding Height}" Background="{TemplateBinding Background}" BorderBrush="{Binding VerticalGridLinesBrush, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" BorderThickness="0,0,1,1"> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Content="{TemplateBinding Content}" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- 外部单元格样式 --> <Style x:Key="DataGridCell.Main" TargetType="{x:Type DataGridCell}"> <Setter Property="Margin" Value="0" /> <Setter Property="Padding" Value="0" /> <Setter Property="BorderThickness" Value="0" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGridCell}"> <Border Background="{Binding Background, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}"> <ContentPresenter /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- 内部单元格样式 --> <Style x:Key="DataGridCell.Inner" TargetType="{x:Type DataGridCell}"> <Setter Property="Margin" Value="0" /> <Setter Property="Padding" Value="0" /> <Setter Property="BorderThickness" Value="0" /> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGridCell}"> <Border Background="{Binding Background, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" BorderBrush="Black" BorderThickness="0,1,0,0"> <ContentPresenter /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- 内部DataGrid样式 --> <Style x:Key="DataGrid.Inner" BasedOn="{StaticResource DataGrid.Base}" TargetType="{x:Type DataGrid}"> <Setter Property="Margin" Value="0" /> <Setter Property="Padding" Value="0" /> <Setter Property="BorderThickness" Value="0" /> <Setter Property="HeadersVisibility" Value="None" /> <Setter Property="HorizontalGridLinesBrush" Value="Transparent" /> <Setter Property="VerticalGridLinesBrush" Value="Transparent" /> <Setter Property="Background" Value="{StaticResource DataGrid.Background}" /> </Style> <!-- DataGridRow样式 --> <Style TargetType="{x:Type DataGridRow}"> <Setter Property="Background" Value="Transparent" /> </Style> <!-- TextBlcok样式 --> <Style x:Key="TextBlock.Style" TargetType="{x:Type TextBlock}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="FontSize" Value="15" /> <Setter Property="VerticalAlignment" Value="Bottom" /> <Setter Property="TextOptions.TextFormattingMode" Value="Ideal" /> <Setter Property="TextOptions.TextRenderingMode" Value="Auto" /> <Setter Property="TextOptions.TextHintingMode" Value="Auto" /> <Setter Property="UseLayoutRounding" Value="True" /> <Setter Property="TextTrimming" Value="CharacterEllipsis" /> <Setter Property="ToolTip" Value="{Binding Text, RelativeSource={RelativeSource Mode=Self}}" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="FontFamily" Value="Microsoft YaHei" /> </Style> <local:DataGridCellDataSourceConverter x:Key="DataGridCellDataSourceConverter" /> <local:DataGridCellHeightConverter x:Key="DataGridCellHeightConverter" /> </Window.Resources>
4.5 定义DataGrid
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Button
x:Name="btnLoadData"
Click="btnLoadData_Click"
Content="加载数据" />
</StackPanel>
<DataGrid
x:Name="datagrid"
Grid.Row="1"
Width="680"
Margin="0,10,0,0"
VerticalAlignment="Center"
CellStyle="{StaticResource DataGridCell.Main}"
Style="{StaticResource DataGrid.Main}">
<DataGrid.Columns>
<DataGridTemplateColumn
Width="197"
Header="部门名称"
HeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border
Width="196"
BorderBrush="{Binding VerticalGridLinesBrush, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}"
BorderThickness="0,1,0,0">
<TextBlock Style="{StaticResource TextBlock.Style}" Text="{Binding Name}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn
Width="200"
Header="设备类型"
HeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataGrid
CellStyle="{StaticResource DataGridCell.Inner}"
ItemsSource="{Binding Items, Converter={StaticResource DataGridCellDataSourceConverter}, ConverterParameter=1}"
Style="{StaticResource DataGrid.Inner}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid Height="{Binding ., Converter={StaticResource DataGridCellHeightConverter}}">
<TextBlock Style="{StaticResource TextBlock.Style}" Text="{Binding Name}" />
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn
Width="200"
Header="设备名称"
HeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataGrid
Margin="0"
CanUserAddRows="False"
CanUserDeleteRows="False"
CellStyle="{StaticResource DataGridCell.Inner}"
ItemsSource="{Binding Items}"
RowHeight="40"
Style="{StaticResource DataGrid.Inner}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Style="{StaticResource TextBlock.Style}" Text="{Binding EquipmentName}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn
Width="80"
Header="状态"
HeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataGrid
Margin="0"
CellStyle="{StaticResource DataGridCell.Inner}"
ItemsSource="{Binding Items, Converter={StaticResource DataGridCellDataSourceConverter}, ConverterParameter=3}"
MinRowHeight="40"
Style="{StaticResource DataGrid.Inner}">
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Border
Height="{Binding ., Converter={StaticResource DataGridCellHeightConverter}}"
VerticalAlignment="Center"
BorderBrush="Black"
BorderThickness="0,1,0,0">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Name}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
</DataGrid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
4.6 后台代码
/// <summary>
/// DataGridMergerCell.xaml 的交互逻辑
/// </summary>
[INotifyPropertyChanged]
public partial class DataGridMergerCell : Window
{
public DataGridMergerCell()
{
InitializeComponent();
this.Loaded += DataGridMergerCell_Loaded;
}
private List<EquipmentInfo> EquipmentInfosList = new();
private async void DataGridMergerCell_Loaded(object sender, RoutedEventArgs e)
{
// 模拟数据
var list = new EquipmentInfo[]
{
new EquipmentInfo{ DepartmentName="生产部", EquipmentType="加工设备", EquipmentName="车床", EquipmentStatus="正常" },
new EquipmentInfo{ DepartmentName="生产部", EquipmentType="加工设备", EquipmentName="钻床", EquipmentStatus="正常" },
new EquipmentInfo{ DepartmentName="加工部", EquipmentType="切割设备", EquipmentName="激光切割机", EquipmentStatus="异常" },
new EquipmentInfo{ DepartmentName="加工部", EquipmentType="切割设备", EquipmentName="水刀切割机", EquipmentStatus="异常" },
new EquipmentInfo{ DepartmentName="维修部", EquipmentType="焊接设备", EquipmentName="MIG焊机", EquipmentStatus="正常" },
new EquipmentInfo{ DepartmentName="维修部", EquipmentType="焊接设备", EquipmentName="TIG焊机", EquipmentStatus="正常" },
new EquipmentInfo{ DepartmentName="维修部", EquipmentType="焊接设备", EquipmentName="电弧焊机", EquipmentStatus="异常" },
};
this.EquipmentInfosList = list.OrderBy(x => x.DepartmentName).ToList();
var view = CollectionViewSource.GetDefaultView(EquipmentInfosList);
view.GroupDescriptions.Clear();
view.GroupDescriptions.Add(new PropertyGroupDescription("DepartmentName"));
await Task.Delay(500);
this.datagrid.ItemsSource = view.Groups;
view.Refresh();
}
[ObservableProperty]
private ObservableCollection<EquipmentInfo> equipmentInfosCollection = new();
private void btnLoadData_Click(object sender, RoutedEventArgs e)
{
var view = CollectionViewSource.GetDefaultView(EquipmentInfosList);
view.GroupDescriptions.Clear();
view.GroupDescriptions.Add(new PropertyGroupDescription("DepartmentName"));
this.datagrid.ItemsSource = view.Groups;
view.Refresh();
}
}
4.7注意要点以及效果
对数据源的处理很关键,通过将数据集进行分组,对于首列的显示可以直接进行绑定Name。对于其它列,在CellTemplate中嵌入DataGrid控件类进行实现合并单元格,注意这里的DataGrid的数据源需要进行分组操作,通过将ReadOnlyObservableCollection(通过ICollectionView的Groups属性拿到)进行转换成ObservableCollection,可以对其进行分组操作(DataGridCellDataSourceConverter类),之后还需要进行Cell的高度计算(DataGridCellHeightConverter),已确保水平线可以对齐显示。