You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF DataGrid单元格向下合并的视觉效果实现问题

WPF DataGrid 单元格向下合并视觉效果实现问题

我在WPF中有一个已渲染到UI上的DataGrid,右键点击DataGridCell并选择MergeDown选项后,希望呈现出两个单元格已合并的视觉效果(需求见附图)。我已查阅相关资料但未解决问题。

附图展示了需求、当前表现以及代码运行后的结果:

  • 需求效果:需求效果
  • 当前结果:当前结果

请问如何实现该视觉效果?

现有XAML代码

<Grid>
    <DataGrid x:Name="dataGrid"></DataGrid>
</Grid>
</Window>

现有C#代码

using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace yourNamespace
{
    public partial class MainWindow : Window
    {
        public class YourDataItem
        {
            public string Column1 { get; set; }
            public string Column2 { get; set; }
        }

        public MainWindow()
        {
            InitializeComponent();
            // 创建数据集合
            ObservableCollection<YourDataItem> dataItems =
            [
                new YourDataItem { Column1 = "1", Column2 = "2"},
                new YourDataItem { Column1 = "3", Column2 = "4"},
                new YourDataItem { Column1 = "5", Column2 = "6"},
            ];

            dataGrid.ItemsSource = dataItems;
            dataGrid.Loaded += DataGrid_Loaded;
        }

        private void DataGrid_Loaded(object sender, RoutedEventArgs e)
        {
            var dataGrid = (DataGrid)sender;

            // 给每个单元格添加右键菜单
            foreach (var row in GetDataGridRows(dataGrid))
            {
                foreach (DataGridCell cell in GetVisualChildren<DataGridCell>(row))
                {
                    cell.ContextMenu = CreateContextMenu(cell);
                }
            }
        }

        private ContextMenu CreateContextMenu(DataGridCell cell)
        {
            var contextMenu = new ContextMenu();

            // 向下合并菜单项
            var mergeDownMenuItem = new MenuItem { Header = "Merge Down" };
            mergeDownMenuItem.Click += (sender, e) => MergeCells(cell, mergeRight: false, mergeDown: true);
            contextMenu.Items.Add(mergeDownMenuItem);

            return contextMenu;
        }

        private void MergeCells(DataGridCell cell, bool mergeRight, bool mergeDown)
        {
            var dataGrid = FindVisualParent<DataGrid>(cell);
            if (dataGrid == null)
                return;

            var rowIndex = GetDataGridRowIndex(cell);
            var columnIndex = GetDataGridColumnIndex(cell);

            if (mergeDown && rowIndex < dataGrid.Items.Count - 1)
            {
                var nextRow = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex + 1);

                if (nextRow != null)
                {
                    var currentRow = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
                    if (currentRow != null)
                    {
                        Grid.SetRowSpan(cell, 2);
                        cell.BorderThickness = new Thickness(1, 1, 1, 0);
                        
                        // 隐藏下一行对应单元格
                        var nextCell = GetCellFromRowAndColumn(dataGrid, rowIndex + 1, columnIndex);
                        var currentCell = GetCellFromRowAndColumn(dataGrid, rowIndex, columnIndex);

                        if (nextCell != null)
                        {
                            nextCell.Visibility = Visibility.Collapsed;
                            nextCell.BorderThickness = new Thickness(0, 0, 0, 0);
                            currentCell.BorderThickness = new Thickness(0, 0, 0, 0);
                        }
                    }
                }
            }
        }               

        private DataGridCell? GetCellFromRowAndColumn(DataGrid dataGrid, int rowIndex, int columnIndex)
        {
            if (rowIndex < 0 || rowIndex >= dataGrid.Items.Count || columnIndex < 0 || columnIndex >= dataGrid.Columns.Count)
                return null;

            var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
            if (row == null)
            {
                dataGrid.ScrollIntoView(dataGrid.Items[rowIndex]);
                dataGrid.UpdateLayout();
                row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
            }

            if (row != null)
            {
                var presenter = GetVisualChild<DataGridCellsPresenter>(row);
                if (presenter != null)
                {
                    var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
                    return cell;
                }
            }

            return null;
        }

        private T? GetVisualChild<T>(DependencyObject parent) where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
            {
                var child = VisualTreeHelper.GetChild(parent, i);
                if (child is T typedChild)
                {
                    return typedChild;
                }

                var foundChild = GetVisualChild<T>(child);
                if (foundChild != null)
                {
                    return foundChild;
                }
            }
            return null;
        }

        private IEnumerable<DataGridRow> GetDataGridRows(DataGrid grid)
        {
            var itemsSource = grid.ItemsSource as IEnumerable;
            if (itemsSource == null) yield break;

            foreach (var item in itemsSource)
            {
                var row = grid.ItemContainerGenerator.ContainerFromItem(item) as DataGridRow;
                if (row != null) yield return row;
            }
        }

        private IEnumerable<T> GetVisualChildren<T>(DependencyObject parent) where T : DependencyObject
        {
            if (parent != null)
            {
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
                {
                    var child = VisualTreeHelper.GetChild(parent, i);
                    if (child is T typedChild)
                    {
                        yield return typedChild;
                    }

                    foreach (var nestedChild in GetVisualChildren<T>(child))
                    {
                        yield return nestedChild;
                    }
                }
            }
        }

        private int GetDataGridRowIndex(DataGridCell cell)
        {
            var row = FindVisualParent<DataGridRow>(cell);
            if (row != null)
            {
                return row.GetIndex();
            }
            return -1;
        }

        private int GetDataGridColumnIndex(DataGridCell cell)
        {
            if (cell.Column != null)
            {
                return cell.Column.DisplayIndex;
            }
            return -1;
        }

        private T FindVisualParent<T>(DependencyObject child) where T : DependencyObject
        {
            DependencyObject parentObject = VisualTreeHelper.GetParent(child);

            if (parentObject == null)
                return null;

            if (parentObject is T parent)
                return parent;

            return FindVisualParent<T>(parentObject);
        }
    }
}

解决方案

你的代码核心问题在于DataGridCell默认模板结构导致RowSpan无法直接生效,单元格本身被限制在当前行的布局容器中,单纯设置RowSpan无法突破行边界。以下是修复步骤:

1. 自定义单元格模板,支持跨行布局

修改DataGrid的CellStyle,让单元格内容容器能响应RowSpan属性:

<DataGrid x:Name="dataGrid">
    <DataGrid.CellStyle>
        <Style TargetType="DataGridCell">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="DataGridCell">
                        <Grid Background="{TemplateBinding Background}">
                            <ContentPresenter x:Name="CellContent" 
                                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter TargetName="CellContent" Property="Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                                <Setter TargetName="CellContent" Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGrid.CellStyle>
</DataGrid>

2. 修正MergeCells方法逻辑

移除错误的边框重置,优化跨行后的视觉表现:

private void MergeCells(DataGridCell cell, bool mergeRight, bool mergeDown)
{
    var dataGrid = FindVisualParent<DataGrid>(cell);
    if (dataGrid == null)
        return;

    var rowIndex = GetDataGridRowIndex(cell);
    var columnIndex = GetDataGridColumnIndex(cell);

    if (mergeDown && rowIndex < dataGrid.Items.Count - 1)
    {
        var nextCell = GetCellFromRowAndColumn(dataGrid, rowIndex + 1, columnIndex);
        if (nextCell == null) return;

        // 设置当前单元格跨行
        Grid.SetRowSpan(cell, 2);
        // 内容垂直居中,符合合并后的视觉逻辑
        cell.VerticalContentAlignment = VerticalAlignment.Center;
        // 保留完整边框,避免合并后样式断裂
        cell.BorderThickness = new Thickness(1);

        // 隐藏下一行对应单元格
        nextCell.Visibility = Visibility.Collapsed;
        // 强制更新布局,确保RowSpan生效
        dataGrid.UpdateLayout();
    }
}

3. 处理行复用问题(可选)

如果DataGrid开启虚拟化(默认开启),滚动时行可能被复用导致合并状态丢失,需要维护合并记录并在加载行时恢复:

// 记录已合并的单元格:(行索引, 列索引)
private HashSet<(int Row, int Col)> mergedCells = new HashSet<(int, int)>();

// 在MergeCells方法中添加记录
mergedCells.Add((rowIndex, columnIndex));

// 订阅LoadingRow事件恢复合并状态
private void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
    var rowIndex = e.Row.GetIndex();
    foreach (var (mergedRow, mergedCol) in mergedCells)
    {
        if (mergedRow == rowIndex)
        {
            var cell = GetCellFromRowAndColumn(dataGrid, rowIndex, mergedCol);
            if (cell != null)
            {
                Grid.SetRowSpan(cell, 2);
                cell.VerticalContentAlignment = VerticalAlignment.Center;
                var nextCell = GetCellFromRowAndColumn(dataGrid, rowIndex + 1, mergedCol);
                nextCell?.Visibility = Visibility.Collapsed;
            }
        }
    }
}

记得在MainWindow构造函数中订阅事件:

dataGrid.LoadingRow += DataGrid_LoadingRow;

效果说明

修改后,右键点击单元格选择MergeDown,当前单元格会跨行显示,下一行对应单元格隐藏,视觉上呈现完整的合并效果,边框保持连续,内容垂直居中。


内容的提问来源于stack exchange,提问作者Ram

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 01:29:51