WPF DataGrid: resizing columns

2020-07-20 04:08发布

问题:

I have a System.Windows.Controls.DataGrid with property CanUserResizeColumns assigned to True. Now I can adjust the width of the columns by using the mouse left button click between 2 column headers.

But I also want to be able to change the width of the columns in any row of the dataGrid, not only in the column headers. Is it possible?

回答1:

In your dataGrid you can use a DataGridTemplate column alogn with a GridSplitter to achieve this..

 <toolkit:DataGridTemplateColumn Header="Text" >
     <toolkit:DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
           <Grid>
              <Grid.ColumnDefinitions>
                 <ColumnDefinition Width="*"/>
                 <ColumnDefinition  Width="Auto"/>
              </Grid.ColumnDefinitions>
              <TextBlock Text="{Binding Text}"/>
              <GridSplitter Grid.Column="1" Width="3"
                            DragIncrement="1"
                            DragDelta="GridSplitter_DragDelta"
                            Tag="{Binding BindsDirectlyToSource=True,
                                    RelativeSource={RelativeSource
                                      AncestorType={x:Type toolkit:DataGridCell}}}"/>
           </Grid>
        </DataTemplate>
     </toolkit:DataGridTemplateColumn.CellTemplate>
 </toolkit:DataGridTemplateColumn>

Then in your code behind... do this...

    private void GridSplitter_DragDelta(
         object sender,
         System.Windows.Controls.Primitives.DragDeltaEventArgs e)
    {
        var gridSplitter = sender as GridSplitter;

        if (gridSplitter != null)
        {
            ((DataGridCell) gridSplitter.Tag).Column.Width
                = ((DataGridCell) gridSplitter.Tag).Column.ActualWidth +
                  e.HorizontalChange;
        }
    }

This way a GridSplitter at individual cell level can resize its entire column.

If you are using MVVM then the above event handler should be put in an Attached Behavior



回答2:

Following on from WPF-its excellent answer, here's how to achieve the same result with at attached behavior:

public static class SplitterOnGridCellBehaviour
{
    public static readonly DependencyProperty ChangeGridCellSizeOnDragProperty =
        DependencyProperty.RegisterAttached("ChangeGridCellSizeOnDrag", typeof (bool),
                                            typeof (SplitterOnGridCellBehaviour),
                                            new PropertyMetadata(false, OnChangeGridCellSizeOnDrag));

private static void OnChangeGridCellSizeOnDrag(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
    GridSplitter splitter = dependencyObject as GridSplitter;

    if(splitter == null)
    {
        throw new NotSupportedException("SplitterOnGridCellBehaviour can only be on a GridSplitter");
    }

    if((bool)args.NewValue)
    {
        splitter.DragDelta += SplitterOnDragDelta;
    }
    else
    {
        splitter.DragDelta -= SplitterOnDragDelta;
    }
}

private static void SplitterOnDragDelta(object sender, DragDeltaEventArgs args)
{
    GridSplitter splitter = (GridSplitter)sender;
    var containerCell = splitter.FindParent<DataGridCell>();
    containerCell.Column.Width = containerCell.Column.ActualWidth + args.HorizontalChange;
}


public static void SetChangeGridCellSizeOnDrag(UIElement element, bool value)
{
    element.SetValue(ChangeGridCellSizeOnDragProperty, value);
}

public static bool GetChangeGridCellSizeOnDrag(UIElement element)
{
    return (bool) element.GetValue(ChangeGridCellSizeOnDragProperty);
}

public static T FindParent<T>(this DependencyObject child)
   where T : DependencyObject
{
    DependencyObject parentObject = VisualTreeHelper.GetParent(child);

    if (parentObject == null) return null;

    var parent = parentObject as T;
    if (parent != null)
    {
        return parent;
    }
    return FindParent<T>(parentObject);
    }
}

To make all of the grid splitters appear as one in the DataGrid, I adjusted the BorderThickness of the DataGridCell to 0, otherwise all of the grid splitters appeared as dashes (on Windows 8 at least).

The XAML for the Window looks like this:

<Window x:Class="DataGridWithSplitter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridWithSplitter" Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate x:Key="CellWithSplitterTemplate">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition  Width="Auto"/>
                </Grid.ColumnDefinitions>
                <TextBlock Text="{Binding Column1}"/>
                <GridSplitter Grid.Column="1" Width="3" Background="Black" local:SplitterOnGridCellBehaviour.ChangeGridCellSizeOnDrag="True" />
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <DataGrid ItemsSource="{Binding SampleData}" GridLinesVisibility="None" HeadersVisibility="None" AutoGenerateColumns="False">
            <DataGrid.Resources>
                <!-- Makes the GridSplitters Solid -->
                <Style TargetType="DataGridCell">
                    <Setter Property="BorderThickness" Value="0" />
                </Style>
            </DataGrid.Resources>
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="First Column" CellTemplate="{StaticResource CellWithSplitterTemplate}"  />
                <DataGridTextColumn Header="Other column" Binding="{Binding Column2}" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

The rest of it is fairly obvious to work out, but for completeness the Windows DataContext was set to an instance of the following ViewModel code:

public class SampleData
{
    public string Column1 { get; set; }

    public string Column2 { get; set; }
}

public class MainWindowViewModel
{
    public IEnumerable<SampleData> SampleData
    {
        get
        {
            return new List<SampleData>()
                       {
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                            new SampleData() {Column1 = "Hello", Column2 = "World"},
                       };
        }
    }
}


回答3:

Here is an alternative solution that does not pollute your data grid contents. Layer a Canvas on top of the DataGrid, and within that Canvas have a Line that can be dragged left and right. When dragged, it updates the desired column width.

XAML:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <DataGrid x:Name="grid" Grid.Row="0" /> <!-- This is your data grid -->
    <Canvas Grid.Row="0"> <!-- Canvas layerd over data grid -->
        <Line StrokeThickness="4" Stroke="Transparent" Cursor="SizeWE"
              X1="{Binding Columns[0].ActualWidth, ElementName=grid}"
              X2="{Binding X1, RelativeSource={RelativeSource Self}}"
              Y2="{Binding ActualHeight, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}}"
              MouseLeftButtonDown="OnSplitLineMouseLeftButtonDown"
              MouseLeftButtonUp="OnSplitLineMouseLeftButtonUp"
              MouseMove="OnSplitLineMouseMove"/>
    </Canvas>
</Grid>

C# code-behind:

#region SplitBarHandling
bool splitBarDragging = false;
double splitBarMouseLastX = 0;
private void OnSplitLineMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    e.Handled = true;
    splitBarDragging = true;
    splitBarMouseLastX = e.GetPosition(null).X;
    ((UIElement)sender).CaptureMouse();
}

private void OnSplitLineMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    e.Handled = true;
    splitBarDragging = false;
    ((UIElement)sender).ReleaseMouseCapture();
}

private void OnSplitLineMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
    if (splitBarDragging)
    {
        e.Handled = true;
        double newX = e.GetPosition(null).X;
        grid.Columns[0].Width = grid.Columns[0].ActualWidth + (newX - splitBarMouseLastX);
        splitBarMouseLastX = newX;
    }
}
#endregion

Note I chose to make the line transparent so the final user will not actually see it. This is because I already rely on the data grid itself to show the vertical grid lines between columns. Also, you may choose the line thickness to whatever you find to be user-friendly without affecting the layout of the grid cells. I chose 4 because it makes it easy to pickup even though the datagrid renders the vertical grid line as 1-pixel wide.

The example code comes from my custom PropertyGrid code-base, which has only two columns, hence the hard-coded column 0. For more generalization, I'd turn this into an attached behavior with support for as many columns needed, or sub-class DataGrid itself.

Compared to the previous solution, this one only adds a few WPF elements to support the behavior regardless of how many data grid rows you have, so it might be more efficient and scalable on large data sets.