WPF DataGrid: resizing columns
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?
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
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"},
};
}
}
}
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.
精彩评论