WPF DataGrid 为每个列集添加单独的边框
WPF DataGrid add separate border to every column set
我正在尝试实现每列都有自己的边框的效果,但仍找不到完美的解决方案。
需要这种外观,但这是通过在 3 列网格中放置 3 个边框来实现的,这不灵活,因为网格列和 DataGrid 列的大小是分开调整的
<Window x:Class="WpfApp3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp3" xmlns:usercontrols="clr-namespace:EECC.UserControls"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Background="LightGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5"/>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="1"/>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="2"/>
<DataGrid ItemsSource="{Binding Items}" ColumnWidth="*" AutoGenerateColumns="True" Padding="10" GridLinesVisibility="None" Background="Transparent" Grid.ColumnSpan="3">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="Transparent"/>
</Style>
</DataGrid.Resources>
</DataGrid>
</Grid>
如果您想使用 DataGrid
,这不是微不足道的。 “问题”是 DataGrid
使用 Grid
来承载细胞。单元格边框是使用 Grid
的功能绘制的,以显示网格线。您可以隐藏网格线,但仍然有 Grid
控制布局。
创造你想要的差距并不容易。 Grid
的布局系统努力实现可扩展的解决方案。您可以扩展 SelectiveScrollingGrid
(DataGrid
的托管面板)并在布置项目时添加间隙。了解 DataGrid
内部结构,我可以说这是可能的,但不值得付出努力。
另一种解决方案是使用 ListView
和 GridView
作为主机。这里的问题是 GridView
旨在将行显示为单个项目。您没有机会修改基于列的边距。您只能调整内容。我没有尝试修改 ListView
内部布局元素或覆盖布局算法,但在替代解决方案的上下文中,我也会使用 GridView
来规则 ListView
- 但这是可能的.不值得。
解决方案:自定义视图
我能建议的最简单的解决方案是调整数据结构以显示基于数据列。这样你就可以使用水平ListBox
。每一项构成一列。每列都实现为垂直 ListBox
。您基本上嵌套了 ListBox
个元素。
您必须处理行映射,以便允许跨垂直 ListBox
列选择公共行的单元格。
这可以通过在 CellItem
模型中添加 RowIndex
属性 来轻松实现。
想法是让水平 ListBox
显示 collection 个 ColumnItem
个模型。每个列项模型公开 collection 个 CellItem
个模型。不同列但同一行的CellItem
项必须共享相同的CellItem.RowIndex
.
作为奖励,这个解决方案非常容易设计。 ListBox
模板与明显更复杂的 DataGrid
或稍复杂的 GridView
.
相比几乎没有任何部分
为了展示这个概念不那么混乱,我选择将网格布局实现为 UserControl
。为了简单起见,初始化和托管模型和源 collections 的逻辑在这个 UserControl
中实现。我不推荐这个。项目的实例化和托管应该在控制之外,例如,在视图模型中。您应该为控件添加一个 DependencyProperty
as ItemsSource
作为内部水平 ListBox
.
的数据源
用法示例
<Window>
<ColumnsView />
</Window>
- 首先创建数据结构来填充视图。
该结构基于 ColumnItem
类型,其中包含 collection 个
CellItem
个项目,其中每个 CellItem
有一个 CellItem.RowIndex
。
逻辑上构成一行的不同列的 CellItem
项必须共享相同的 CellItem.RowIndex
.
ColumnItem.cs
public class ColumnItem
{
public ColumnItem(string header, IEnumerable<CellItem> items)
{
Header = header;
this.Items = new ObservableCollection<CellItem>(items);
}
public CellItem this[int rowIndex]
=> this.Items.FirstOrDefault(cellItem => cellItem.RowIndex.Equals(rowIndex));
public string Header { get; }
public ObservableCollection<CellItem> Items { get; }
}
CellItem.cs
public class CellItem : INotifyPropertyChanged
{
public CellItem(int rowIndex, object value)
{
this.RowIndex = rowIndex;
this.Value = value;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public event PropertyChangedEventHandler PropertyChanged;
public int RowIndex { get; }
private object value;
public object Value
{
get => this.value;
set
{
this.value = value;
OnPropertyChanged();
}
}
private bool isSelected;
public bool IsSelected
{
get => this.isSelected;
set
{
this.isSelected = value;
OnPropertyChanged();
}
}
}
- 构建并初始化数据结构。
在此示例中,这全部在 UserControl
本身中实现,目的是使示例尽可能紧凑。
ColumnsView.xaml.cs
public partial class ColumnsView : UserControl
{
public ColumnsView()
{
InitializeComponent();
this.DataContext = this;
InitializeSourceData();
}
public InitializeSourceData()
{
this.Columns = new ObservableCollection<ColumnItem>();
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var cellItems = new List<CellItem>();
int asciiChar = 65;
for (int rowIndex = 0; rowIndex < 10; rowIndex++)
{
var cellValue = $"CellItem.RowIndex:{rowIndex}, Value: {(char)asciiChar++}";
var cellItem = new CellItem(rowIndex, cellValue);
cellItems.Add(cellItem);
}
var columnHeader = $"Column {columnIndex + 1}";
var columnItem = new ColumnItem(columnHeader, cellItems);
this.Columns.Add(columnItem);
}
}
private void CellsHostListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var cellsHost = sender as Selector;
var selectedCell = cellsHost.SelectedItem as CellItem;
SelectCellsOfRow(selectedCell.RowIndex);
}
private void SelectCellsOfRow(int selectedRowIndex)
{
foreach (ColumnItem columnItem in this.Columns)
{
var cellOfRow = columnItem[selectedRowIndex];
cellOfRow.IsSelected = true;
}
}
private void ColumnGripper_DragStarted(object sender, DragStartedEventArgs e)
=> this.DragStartX = Mouse.GetPosition(this).X;
private void ColumnGripper_DragDelta(object sender, DragDeltaEventArgs e)
{
if ((sender as DependencyObject).TryFindVisualParentElement(out ListBoxItem listBoxItem))
{
double currentMousePositionX = Mouse.GetPosition(this).X;
listBoxItem.Width = Math.Max(0 , listBoxItem.ActualWidth - (this.DragStartX - currentMousePositionX));
this.DragStartX = currentMousePositionX;
}
}
public static bool TryFindVisualParentElement<TParent>(DependencyObject child, out TParent resultElement)
where TParent : DependencyObject
{
resultElement = null;
DependencyObject parentElement = VisualTreeHelper.GetParent(child);
if (parentElement is TParent parent)
{
resultElement = parent;
return true;
}
return parentElement != null
? TryFindVisualParentElement(parentElement, out resultElement)
: false;
}
public ObservableCollection<ColumnItem> Columns { get; }
private double DragStartX { get; set; }
}
- 使用水平
ListView
创建视图,将其 ColumnItem
源 collection 呈现为垂直 ListBox
元素的列表。
ColumnsView.xaml
<UserControl x:Class="ColumnsView">
<UserControl.Resources>
<Style x:Key="ColumnGripperStyle"
TargetType="{x:Type Thumb}">
<Setter Property="Margin"
Value="-2,8" />
<Setter Property="Width"
Value="4" />
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Cursor"
Value="SizeWE" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<!-- Column host. Displays cells of a column. -->
<ListBox ItemsSource="{Binding Columns}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="4"
BorderThickness="1"
BorderBrush="Black"
CornerRadius="8">
<StackPanel>
<TextBlock Text="{Binding Header}" />
<!-- Cell host. Displays cells of a column. -->
<ListBox ItemsSource="{Binding Items}"
BorderThickness="0"
Height="150"
Selector.SelectionChanged="CellsHostListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<!-- Link item container selection to CellItem.IsSelected -->
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="0,0,8,0" /> <!-- Define the column gap -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter />
<Thumb Grid.Column="1"
Style="{StaticResource ColumnGripperStyle}"
DragStarted="ColumnGripper_DragStarted"
DragDelta="ColumnGripper_DragDelta" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</UserControl>
改进注意事项
ColumnsView.Columns
属性 应该是 DependencyProperty
以允许将控件用作绑定目标。
列间距也可以是 DependencyProperty
of ColumnsView
.
通过将显示列 header 的 TextBlock
替换为 Button
,您可以轻松添加排序。具有触发排序的活动列,例如从词法上讲,您必须同步其他被动列并根据主动排序列的 CellItem.RowIndex
顺序对它们进行排序。
也许选择扩展 Control
而不是 UserControl
。
您可以实现 CellItem
以使用泛型类型参数来声明 Cellitem.Value
属性,例如 CellItem<TValue>
.
您可以实现 ColumnItem
以使用泛型类型参数来声明 ColumnItem.Items
属性,例如 ColumnItem<TColumn>
.
添加一个 ColumnsView.SelectedRow
属性 那个 returns 一个 collection of all CellItem
items of all current selected row
只要付出相当多的努力,您就可以通过扩展原生 DataGrid
.
来获得所需的外观
自定义 header 和单元格模板应注意间距,并使用适当的背景颜色。 AutoGeneratingColumn
行为需要比 XAML 更容易实现的控制,所以我选择在代码中创建模板,以便能够传递列的 PropertyName
.
细心的 reader 会问自己:“列表末尾的边框怎么办?”。没错,我们需要能够将最后一项与所有其他项区分开来,以便能够对其边框进行不同的模板化。
这是通过以下合约完成的:
public interface ICanBeLastItem
{
bool IsLastItem { get; set; }
}
行 object 需要实现才能正确绘制底部边框。
这在排序时也需要一些自定义逻辑,以更新 IsLastItem
的值。黄色背景的图片显示了 ThirdNumber
.
上的排序结果
原生 DataGrid
提供开箱即用的 Sorting
事件,但没有 Sorted
事件。模板结合了自定义事件的需要,导致我从 DataGrid
继承 ColumnView
而不是将其声明为 UserControl
.
我将 code-behind 添加到 MainWindow
,用于切换背景颜色,但这只是为了说明目的(因为我不喜欢实现 Command
模式)并且与自定义控件无关。
ColumnView
是通过绑定配置的。一如既往,请随意扩展。当前的实现期望列是自动生成的。无论哪种情况,都提供了生成模板的代码。
<local:ColumnView ItemsSource="{Binding Items}" Background="LightSteelBlue"/>
演示代码
列视图
public class ColumnView : DataGrid
{
public ColumnView()
{
HeadersVisibility = DataGridHeadersVisibility.Column;
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
// Hidden props from base DataGrid
base.ColumnWidth = new DataGridLength(1, DataGridLengthUnitType.Star);
base.AutoGenerateColumns = true;
base.GridLinesVisibility = DataGridGridLinesVisibility.None;
// Styling
ColumnHeaderStyle = CreateColumnHeaderStyle();
CellStyle = CreateCellStyle(this);
// Event handling
AutoGeneratingColumn += OnAutoGeneratingColumn;
Sorting += OnSorting;
Sorted += OnSorted;
}
#region Hidden props
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DataGridLength ColumnWidth
{
get => base.ColumnWidth;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(ColumnWidth)}.");
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DataGridGridLinesVisibility GridLinesVisibility
{
get => base.GridLinesVisibility;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(GridLinesVisibility)}.");
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new bool AutoGenerateColumns
{
get => base.AutoGenerateColumns;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(AutoGenerateColumns)}.");
}
#endregion Hidden props
#region Styling
private static Style CreateColumnHeaderStyle()
=> new Style(typeof(DataGridColumnHeader))
{
Setters =
{
new Setter(BackgroundProperty, Brushes.Transparent),
new Setter(HorizontalAlignmentProperty, HorizontalAlignment.Stretch),
new Setter(HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch)
}
};
private static Style CreateCellStyle(ColumnView columnView)
=> new Style(typeof(DataGridCell))
{
Setters =
{
new Setter(BorderThicknessProperty, new Thickness(0.0)),
new Setter(BackgroundProperty, new Binding(nameof(Background)) { Source = columnView})
}
};
#endregion Styling
#region AutoGeneratingColumn
// https://whosebug.com/questions/25643765/wpf-datagrid-databind-to-datatable-cell-in-celltemplates-datatemplate
private static void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (sender is ColumnView columnView)
{
if (e.PropertyName == nameof(ICanBeLastItem.IsLastItem))
{
e.Cancel = true;
}
else
{
var column = new DataGridTemplateColumn
{
CellTemplate = CreateCustomCellTemplate(e.PropertyName),
Header = e.Column.Header,
HeaderTemplate = CreateCustomHeaderTemplate(columnView, e.PropertyName),
HeaderStringFormat = e.Column.HeaderStringFormat,
SortMemberPath = e.PropertyName
};
e.Column = column;
}
}
}
private static DataTemplate CreateCustomCellTemplate(string path)
{
// Create the data template
var customTemplate = new DataTemplate();
// Set up the wrapping border
var border = new FrameworkElementFactory(typeof(Border));
border.SetValue(BorderBrushProperty, Brushes.Black);
border.SetValue(StyleProperty, new Style(typeof(Border))
{
Triggers =
{
new DataTrigger
{
Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
Value = false,
Setters =
{
new Setter(BackgroundProperty, Brushes.White),
}
},
new DataTrigger
{
Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
Value = true,
Setters =
{
new Setter(BackgroundProperty, SystemColors.HighlightBrush),
}
},
new DataTrigger
{
Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
Value = false,
Setters =
{
new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, -1.0)),
new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 0.0)),
}
},
new DataTrigger
{
Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
Value = true,
Setters =
{
new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, 0.0)),
new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 1.0)),
new Setter(Border.CornerRadiusProperty, new CornerRadius(0.0, 0.0, 5.0, 5.0)),
new Setter(Border.PaddingProperty, new Thickness(0.0, 0.0, 0.0, 5.0)),
}
}
}
});
// Set up the TextBlock
var textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetBinding(TextBlock.TextProperty, new Binding(path));
textBlock.SetValue(MarginProperty, new Thickness(10.0, 0.0, 5.0, 0.0));
// Set the visual tree of the data template
border.AppendChild(textBlock);
customTemplate.VisualTree = border;
return customTemplate;
}
private static DataTemplate CreateCustomHeaderTemplate(ColumnView columnView, string propName)
{
// Create the data template
var customTemplate = new DataTemplate();
// Set up the wrapping border
var border = new FrameworkElementFactory(typeof(Border));
border.SetValue(MarginProperty, new Thickness(5.0, 0.0, 5.0, 0.0));
border.SetValue(BackgroundProperty, Brushes.White);
border.SetValue(BorderBrushProperty, Brushes.Black);
border.SetValue(BorderThicknessProperty, new Thickness(1.0, 1.0, 1.0, 0.0));
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(5.0, 5.0, 0.0, 0.0));
// Set up the TextBlock
var textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetValue(TextBlock.TextProperty, propName);
textBlock.SetValue(MarginProperty, new Thickness(5.0));
// Set the visual tree of the data template
border.AppendChild(textBlock);
customTemplate.VisualTree = border;
return customTemplate;
}
#endregion AutoGeneratingColumn
#region Sorting
#region Custom Sorted Event
// https://whosebug.com/questions/9571178/datagrid-is-there-no-sorted-event
// Create a custom routed event by first registering a RoutedEventID
// This event uses the bubbling routing strategy
public static readonly RoutedEvent SortedEvent = EventManager.RegisterRoutedEvent(
nameof(Sorted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColumnView));
// Provide CLR accessors for the event
public event RoutedEventHandler Sorted
{
add => AddHandler(SortedEvent, value);
remove => RemoveHandler(SortedEvent, value);
}
// This method raises the Sorted event
private void RaiseSortedEvent()
{
var newEventArgs = new RoutedEventArgs(ColumnView.SortedEvent);
RaiseEvent(newEventArgs);
}
protected override void OnSorting(DataGridSortingEventArgs eventArgs)
{
base.OnSorting(eventArgs);
RaiseSortedEvent();
}
#endregion Custom Sorted Event
private static void OnSorting(object sender, DataGridSortingEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.HasItems)
{
if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
{
lastItem.IsLastItem = false;
}
}
}
private static void OnSorted(object sender, RoutedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.HasItems)
{
if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
{
lastItem.IsLastItem = true;
}
}
}
#endregion Sorting
}
行项目
public class RowItem : INotifyPropertyChanged, ICanBeLastItem
{
public RowItem(int firstNumber, string secondNumber, double thirdNumber)
{
FirstNumber = firstNumber;
SecondNumber = secondNumber;
ThirdNumber = thirdNumber;
}
public int FirstNumber { get; }
public string SecondNumber { get; }
public double ThirdNumber { get; }
private bool _isLastItem;
public bool IsLastItem
{
get => _isLastItem;
set
{
_isLastItem = value;
OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged
}
public interface ICanBeLastItem
{
bool IsLastItem { get; set; }
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Switch Background" Click="ButtonBase_OnClick" />
<local:ColumnView x:Name="columnView" Grid.Row="1" Padding="10"
ItemsSource="{Binding Items}"
Background="LightSteelBlue"/>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
if (columnView.Background == Brushes.LightSteelBlue)
{
columnView.Background = Brushes.DarkRed;
}
else if (columnView.Background == Brushes.DarkRed)
{
columnView.Background = Brushes.Green;
}
else if (columnView.Background == Brushes.Green)
{
columnView.Background = Brushes.Blue;
}
else if (columnView.Background == Brushes.Blue)
{
columnView.Background = Brushes.Yellow;
}
else
{
columnView.Background = Brushes.LightSteelBlue;
}
}
}
MainViewModel
public class MainViewModel
{
public MainViewModel()
{
Items = InitializeItems(200);
}
private ObservableCollection<RowItem> InitializeItems(int numberOfItems)
{
var rowItems = new ObservableCollection<RowItem>();
var random = new Random();
for (var i = 0; i < numberOfItems; i++)
{
var firstNumber = Convert.ToInt32(1000 * random.NextDouble());
var secondNumber = Convert.ToString(Math.Round(1000 * random.NextDouble()));
var thirdNumber = Math.Round(1000 * random.NextDouble());
var rowItem = new RowItem(firstNumber, secondNumber, thirdNumber);
rowItems.Add(rowItem);
}
rowItems[numberOfItems - 1].IsLastItem = true;
return rowItems;
}
public ObservableCollection<RowItem> Items { get; }
}
我正在尝试实现每列都有自己的边框的效果,但仍找不到完美的解决方案。
需要这种外观,但这是通过在 3 列网格中放置 3 个边框来实现的,这不灵活,因为网格列和 DataGrid 列的大小是分开调整的
<Window x:Class="WpfApp3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp3" xmlns:usercontrols="clr-namespace:EECC.UserControls"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Background="LightGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5"/>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="1"/>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="2"/>
<DataGrid ItemsSource="{Binding Items}" ColumnWidth="*" AutoGenerateColumns="True" Padding="10" GridLinesVisibility="None" Background="Transparent" Grid.ColumnSpan="3">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="Transparent"/>
</Style>
</DataGrid.Resources>
</DataGrid>
</Grid>
如果您想使用 DataGrid
,这不是微不足道的。 “问题”是 DataGrid
使用 Grid
来承载细胞。单元格边框是使用 Grid
的功能绘制的,以显示网格线。您可以隐藏网格线,但仍然有 Grid
控制布局。
创造你想要的差距并不容易。 Grid
的布局系统努力实现可扩展的解决方案。您可以扩展 SelectiveScrollingGrid
(DataGrid
的托管面板)并在布置项目时添加间隙。了解 DataGrid
内部结构,我可以说这是可能的,但不值得付出努力。
另一种解决方案是使用 ListView
和 GridView
作为主机。这里的问题是 GridView
旨在将行显示为单个项目。您没有机会修改基于列的边距。您只能调整内容。我没有尝试修改 ListView
内部布局元素或覆盖布局算法,但在替代解决方案的上下文中,我也会使用 GridView
来规则 ListView
- 但这是可能的.不值得。
解决方案:自定义视图
我能建议的最简单的解决方案是调整数据结构以显示基于数据列。这样你就可以使用水平ListBox
。每一项构成一列。每列都实现为垂直 ListBox
。您基本上嵌套了 ListBox
个元素。
您必须处理行映射,以便允许跨垂直 ListBox
列选择公共行的单元格。
这可以通过在 CellItem
模型中添加 RowIndex
属性 来轻松实现。
想法是让水平 ListBox
显示 collection 个 ColumnItem
个模型。每个列项模型公开 collection 个 CellItem
个模型。不同列但同一行的CellItem
项必须共享相同的CellItem.RowIndex
.
作为奖励,这个解决方案非常容易设计。 ListBox
模板与明显更复杂的 DataGrid
或稍复杂的 GridView
.
为了展示这个概念不那么混乱,我选择将网格布局实现为 UserControl
。为了简单起见,初始化和托管模型和源 collections 的逻辑在这个 UserControl
中实现。我不推荐这个。项目的实例化和托管应该在控制之外,例如,在视图模型中。您应该为控件添加一个 DependencyProperty
as ItemsSource
作为内部水平 ListBox
.
用法示例
<Window>
<ColumnsView />
</Window>
- 首先创建数据结构来填充视图。
该结构基于ColumnItem
类型,其中包含 collection 个CellItem
个项目,其中每个CellItem
有一个CellItem.RowIndex
。
逻辑上构成一行的不同列的CellItem
项必须共享相同的CellItem.RowIndex
.
ColumnItem.cs
public class ColumnItem
{
public ColumnItem(string header, IEnumerable<CellItem> items)
{
Header = header;
this.Items = new ObservableCollection<CellItem>(items);
}
public CellItem this[int rowIndex]
=> this.Items.FirstOrDefault(cellItem => cellItem.RowIndex.Equals(rowIndex));
public string Header { get; }
public ObservableCollection<CellItem> Items { get; }
}
CellItem.cs
public class CellItem : INotifyPropertyChanged
{
public CellItem(int rowIndex, object value)
{
this.RowIndex = rowIndex;
this.Value = value;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public event PropertyChangedEventHandler PropertyChanged;
public int RowIndex { get; }
private object value;
public object Value
{
get => this.value;
set
{
this.value = value;
OnPropertyChanged();
}
}
private bool isSelected;
public bool IsSelected
{
get => this.isSelected;
set
{
this.isSelected = value;
OnPropertyChanged();
}
}
}
- 构建并初始化数据结构。
在此示例中,这全部在UserControl
本身中实现,目的是使示例尽可能紧凑。
ColumnsView.xaml.cs
public partial class ColumnsView : UserControl
{
public ColumnsView()
{
InitializeComponent();
this.DataContext = this;
InitializeSourceData();
}
public InitializeSourceData()
{
this.Columns = new ObservableCollection<ColumnItem>();
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var cellItems = new List<CellItem>();
int asciiChar = 65;
for (int rowIndex = 0; rowIndex < 10; rowIndex++)
{
var cellValue = $"CellItem.RowIndex:{rowIndex}, Value: {(char)asciiChar++}";
var cellItem = new CellItem(rowIndex, cellValue);
cellItems.Add(cellItem);
}
var columnHeader = $"Column {columnIndex + 1}";
var columnItem = new ColumnItem(columnHeader, cellItems);
this.Columns.Add(columnItem);
}
}
private void CellsHostListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var cellsHost = sender as Selector;
var selectedCell = cellsHost.SelectedItem as CellItem;
SelectCellsOfRow(selectedCell.RowIndex);
}
private void SelectCellsOfRow(int selectedRowIndex)
{
foreach (ColumnItem columnItem in this.Columns)
{
var cellOfRow = columnItem[selectedRowIndex];
cellOfRow.IsSelected = true;
}
}
private void ColumnGripper_DragStarted(object sender, DragStartedEventArgs e)
=> this.DragStartX = Mouse.GetPosition(this).X;
private void ColumnGripper_DragDelta(object sender, DragDeltaEventArgs e)
{
if ((sender as DependencyObject).TryFindVisualParentElement(out ListBoxItem listBoxItem))
{
double currentMousePositionX = Mouse.GetPosition(this).X;
listBoxItem.Width = Math.Max(0 , listBoxItem.ActualWidth - (this.DragStartX - currentMousePositionX));
this.DragStartX = currentMousePositionX;
}
}
public static bool TryFindVisualParentElement<TParent>(DependencyObject child, out TParent resultElement)
where TParent : DependencyObject
{
resultElement = null;
DependencyObject parentElement = VisualTreeHelper.GetParent(child);
if (parentElement is TParent parent)
{
resultElement = parent;
return true;
}
return parentElement != null
? TryFindVisualParentElement(parentElement, out resultElement)
: false;
}
public ObservableCollection<ColumnItem> Columns { get; }
private double DragStartX { get; set; }
}
- 使用水平
ListView
创建视图,将其ColumnItem
源 collection 呈现为垂直ListBox
元素的列表。
ColumnsView.xaml
<UserControl x:Class="ColumnsView">
<UserControl.Resources>
<Style x:Key="ColumnGripperStyle"
TargetType="{x:Type Thumb}">
<Setter Property="Margin"
Value="-2,8" />
<Setter Property="Width"
Value="4" />
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Cursor"
Value="SizeWE" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<!-- Column host. Displays cells of a column. -->
<ListBox ItemsSource="{Binding Columns}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="4"
BorderThickness="1"
BorderBrush="Black"
CornerRadius="8">
<StackPanel>
<TextBlock Text="{Binding Header}" />
<!-- Cell host. Displays cells of a column. -->
<ListBox ItemsSource="{Binding Items}"
BorderThickness="0"
Height="150"
Selector.SelectionChanged="CellsHostListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<!-- Link item container selection to CellItem.IsSelected -->
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="0,0,8,0" /> <!-- Define the column gap -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter />
<Thumb Grid.Column="1"
Style="{StaticResource ColumnGripperStyle}"
DragStarted="ColumnGripper_DragStarted"
DragDelta="ColumnGripper_DragDelta" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</UserControl>
改进注意事项
ColumnsView.Columns
属性 应该是 DependencyProperty
以允许将控件用作绑定目标。
列间距也可以是 DependencyProperty
of ColumnsView
.
通过将显示列 header 的 TextBlock
替换为 Button
,您可以轻松添加排序。具有触发排序的活动列,例如从词法上讲,您必须同步其他被动列并根据主动排序列的 CellItem.RowIndex
顺序对它们进行排序。
也许选择扩展 Control
而不是 UserControl
。
您可以实现 CellItem
以使用泛型类型参数来声明 Cellitem.Value
属性,例如 CellItem<TValue>
.
您可以实现 ColumnItem
以使用泛型类型参数来声明 ColumnItem.Items
属性,例如 ColumnItem<TColumn>
.
添加一个 ColumnsView.SelectedRow
属性 那个 returns 一个 collection of all CellItem
items of all current selected row
只要付出相当多的努力,您就可以通过扩展原生 DataGrid
.
自定义 header 和单元格模板应注意间距,并使用适当的背景颜色。 AutoGeneratingColumn
行为需要比 XAML 更容易实现的控制,所以我选择在代码中创建模板,以便能够传递列的 PropertyName
.
细心的 reader 会问自己:“列表末尾的边框怎么办?”。没错,我们需要能够将最后一项与所有其他项区分开来,以便能够对其边框进行不同的模板化。
这是通过以下合约完成的:
public interface ICanBeLastItem
{
bool IsLastItem { get; set; }
}
行 object 需要实现才能正确绘制底部边框。
这在排序时也需要一些自定义逻辑,以更新 IsLastItem
的值。黄色背景的图片显示了 ThirdNumber
.
原生 DataGrid
提供开箱即用的 Sorting
事件,但没有 Sorted
事件。模板结合了自定义事件的需要,导致我从 DataGrid
继承 ColumnView
而不是将其声明为 UserControl
.
我将 code-behind 添加到 MainWindow
,用于切换背景颜色,但这只是为了说明目的(因为我不喜欢实现 Command
模式)并且与自定义控件无关。
ColumnView
是通过绑定配置的。一如既往,请随意扩展。当前的实现期望列是自动生成的。无论哪种情况,都提供了生成模板的代码。
<local:ColumnView ItemsSource="{Binding Items}" Background="LightSteelBlue"/>
演示代码
列视图
public class ColumnView : DataGrid
{
public ColumnView()
{
HeadersVisibility = DataGridHeadersVisibility.Column;
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
// Hidden props from base DataGrid
base.ColumnWidth = new DataGridLength(1, DataGridLengthUnitType.Star);
base.AutoGenerateColumns = true;
base.GridLinesVisibility = DataGridGridLinesVisibility.None;
// Styling
ColumnHeaderStyle = CreateColumnHeaderStyle();
CellStyle = CreateCellStyle(this);
// Event handling
AutoGeneratingColumn += OnAutoGeneratingColumn;
Sorting += OnSorting;
Sorted += OnSorted;
}
#region Hidden props
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DataGridLength ColumnWidth
{
get => base.ColumnWidth;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(ColumnWidth)}.");
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DataGridGridLinesVisibility GridLinesVisibility
{
get => base.GridLinesVisibility;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(GridLinesVisibility)}.");
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new bool AutoGenerateColumns
{
get => base.AutoGenerateColumns;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(AutoGenerateColumns)}.");
}
#endregion Hidden props
#region Styling
private static Style CreateColumnHeaderStyle()
=> new Style(typeof(DataGridColumnHeader))
{
Setters =
{
new Setter(BackgroundProperty, Brushes.Transparent),
new Setter(HorizontalAlignmentProperty, HorizontalAlignment.Stretch),
new Setter(HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch)
}
};
private static Style CreateCellStyle(ColumnView columnView)
=> new Style(typeof(DataGridCell))
{
Setters =
{
new Setter(BorderThicknessProperty, new Thickness(0.0)),
new Setter(BackgroundProperty, new Binding(nameof(Background)) { Source = columnView})
}
};
#endregion Styling
#region AutoGeneratingColumn
// https://whosebug.com/questions/25643765/wpf-datagrid-databind-to-datatable-cell-in-celltemplates-datatemplate
private static void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (sender is ColumnView columnView)
{
if (e.PropertyName == nameof(ICanBeLastItem.IsLastItem))
{
e.Cancel = true;
}
else
{
var column = new DataGridTemplateColumn
{
CellTemplate = CreateCustomCellTemplate(e.PropertyName),
Header = e.Column.Header,
HeaderTemplate = CreateCustomHeaderTemplate(columnView, e.PropertyName),
HeaderStringFormat = e.Column.HeaderStringFormat,
SortMemberPath = e.PropertyName
};
e.Column = column;
}
}
}
private static DataTemplate CreateCustomCellTemplate(string path)
{
// Create the data template
var customTemplate = new DataTemplate();
// Set up the wrapping border
var border = new FrameworkElementFactory(typeof(Border));
border.SetValue(BorderBrushProperty, Brushes.Black);
border.SetValue(StyleProperty, new Style(typeof(Border))
{
Triggers =
{
new DataTrigger
{
Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
Value = false,
Setters =
{
new Setter(BackgroundProperty, Brushes.White),
}
},
new DataTrigger
{
Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
Value = true,
Setters =
{
new Setter(BackgroundProperty, SystemColors.HighlightBrush),
}
},
new DataTrigger
{
Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
Value = false,
Setters =
{
new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, -1.0)),
new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 0.0)),
}
},
new DataTrigger
{
Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
Value = true,
Setters =
{
new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, 0.0)),
new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 1.0)),
new Setter(Border.CornerRadiusProperty, new CornerRadius(0.0, 0.0, 5.0, 5.0)),
new Setter(Border.PaddingProperty, new Thickness(0.0, 0.0, 0.0, 5.0)),
}
}
}
});
// Set up the TextBlock
var textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetBinding(TextBlock.TextProperty, new Binding(path));
textBlock.SetValue(MarginProperty, new Thickness(10.0, 0.0, 5.0, 0.0));
// Set the visual tree of the data template
border.AppendChild(textBlock);
customTemplate.VisualTree = border;
return customTemplate;
}
private static DataTemplate CreateCustomHeaderTemplate(ColumnView columnView, string propName)
{
// Create the data template
var customTemplate = new DataTemplate();
// Set up the wrapping border
var border = new FrameworkElementFactory(typeof(Border));
border.SetValue(MarginProperty, new Thickness(5.0, 0.0, 5.0, 0.0));
border.SetValue(BackgroundProperty, Brushes.White);
border.SetValue(BorderBrushProperty, Brushes.Black);
border.SetValue(BorderThicknessProperty, new Thickness(1.0, 1.0, 1.0, 0.0));
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(5.0, 5.0, 0.0, 0.0));
// Set up the TextBlock
var textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetValue(TextBlock.TextProperty, propName);
textBlock.SetValue(MarginProperty, new Thickness(5.0));
// Set the visual tree of the data template
border.AppendChild(textBlock);
customTemplate.VisualTree = border;
return customTemplate;
}
#endregion AutoGeneratingColumn
#region Sorting
#region Custom Sorted Event
// https://whosebug.com/questions/9571178/datagrid-is-there-no-sorted-event
// Create a custom routed event by first registering a RoutedEventID
// This event uses the bubbling routing strategy
public static readonly RoutedEvent SortedEvent = EventManager.RegisterRoutedEvent(
nameof(Sorted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColumnView));
// Provide CLR accessors for the event
public event RoutedEventHandler Sorted
{
add => AddHandler(SortedEvent, value);
remove => RemoveHandler(SortedEvent, value);
}
// This method raises the Sorted event
private void RaiseSortedEvent()
{
var newEventArgs = new RoutedEventArgs(ColumnView.SortedEvent);
RaiseEvent(newEventArgs);
}
protected override void OnSorting(DataGridSortingEventArgs eventArgs)
{
base.OnSorting(eventArgs);
RaiseSortedEvent();
}
#endregion Custom Sorted Event
private static void OnSorting(object sender, DataGridSortingEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.HasItems)
{
if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
{
lastItem.IsLastItem = false;
}
}
}
private static void OnSorted(object sender, RoutedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.HasItems)
{
if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
{
lastItem.IsLastItem = true;
}
}
}
#endregion Sorting
}
行项目
public class RowItem : INotifyPropertyChanged, ICanBeLastItem
{
public RowItem(int firstNumber, string secondNumber, double thirdNumber)
{
FirstNumber = firstNumber;
SecondNumber = secondNumber;
ThirdNumber = thirdNumber;
}
public int FirstNumber { get; }
public string SecondNumber { get; }
public double ThirdNumber { get; }
private bool _isLastItem;
public bool IsLastItem
{
get => _isLastItem;
set
{
_isLastItem = value;
OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged
}
public interface ICanBeLastItem
{
bool IsLastItem { get; set; }
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Switch Background" Click="ButtonBase_OnClick" />
<local:ColumnView x:Name="columnView" Grid.Row="1" Padding="10"
ItemsSource="{Binding Items}"
Background="LightSteelBlue"/>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
if (columnView.Background == Brushes.LightSteelBlue)
{
columnView.Background = Brushes.DarkRed;
}
else if (columnView.Background == Brushes.DarkRed)
{
columnView.Background = Brushes.Green;
}
else if (columnView.Background == Brushes.Green)
{
columnView.Background = Brushes.Blue;
}
else if (columnView.Background == Brushes.Blue)
{
columnView.Background = Brushes.Yellow;
}
else
{
columnView.Background = Brushes.LightSteelBlue;
}
}
}
MainViewModel
public class MainViewModel
{
public MainViewModel()
{
Items = InitializeItems(200);
}
private ObservableCollection<RowItem> InitializeItems(int numberOfItems)
{
var rowItems = new ObservableCollection<RowItem>();
var random = new Random();
for (var i = 0; i < numberOfItems; i++)
{
var firstNumber = Convert.ToInt32(1000 * random.NextDouble());
var secondNumber = Convert.ToString(Math.Round(1000 * random.NextDouble()));
var thirdNumber = Math.Round(1000 * random.NextDouble());
var rowItem = new RowItem(firstNumber, secondNumber, thirdNumber);
rowItems.Add(rowItem);
}
rowItems[numberOfItems - 1].IsLastItem = true;
return rowItems;
}
public ObservableCollection<RowItem> Items { get; }
}