合并控件
This commit is contained in:
1 parent
233ec5d58d
commit
b842292817
11 files changed
+1400
-463
No files matched your search
+260
-111
@@ -1,7 +1,9 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
@@ -9,122 +11,269 @@ using System.Windows.Threading;
|
||||
namespace Main.Test;
|
||||
|
||||
/// <summary>带默认样式、行号和行选择勾选框的 DataGrid。</summary>
|
||||
public class StyledDataGrid : DataGrid
|
||||
public partial class StyledDataGrid : DataGrid
|
||||
{
|
||||
public static readonly RoutedUICommand ToggleSelectAllCommand = new("全选或取消全选", nameof(ToggleSelectAllCommand), typeof(StyledDataGrid));
|
||||
private static readonly DependencyPropertyKey AreAllRowsSelectedPropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(AreAllRowsSelected), typeof(bool?), typeof(StyledDataGrid), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty AreAllRowsSelectedProperty = AreAllRowsSelectedPropertyKey.DependencyProperty;
|
||||
/// <summary>全部选中为 true,部分选中为 null,未选中为 false。</summary>
|
||||
public bool? AreAllRowsSelected => (bool?)GetValue(AreAllRowsSelectedProperty);
|
||||
public static readonly DependencyProperty SelectedRowBackgroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowBackground), typeof(Brush), typeof(StyledDataGrid),
|
||||
new PropertyMetadata(new SolidColorBrush(Color.FromRgb(75, 178, 255))));
|
||||
public static readonly DependencyProperty SelectedRowForegroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowForeground), typeof(Brush), typeof(StyledDataGrid), new PropertyMetadata(Brushes.White));
|
||||
public static readonly DependencyProperty ShowCheckBoxProperty = DependencyProperty.Register(
|
||||
nameof(ShowCheckBox), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
public static readonly DependencyProperty ShowRowNumberProperty = DependencyProperty.Register(
|
||||
nameof(ShowRowNumber), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
|
||||
nameof(SelectedItems), typeof(IList), typeof(StyledDataGrid),
|
||||
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));
|
||||
|
||||
public Brush SelectedRowBackground { get => (Brush)GetValue(SelectedRowBackgroundProperty); set => SetValue(SelectedRowBackgroundProperty, value); }
|
||||
public Brush SelectedRowForeground { get => (Brush)GetValue(SelectedRowForegroundProperty); set => SetValue(SelectedRowForegroundProperty, value); }
|
||||
/// <summary>勾选状态与行 IsSelected 同步,可通过 SelectedItems 获取。</summary>
|
||||
public bool ShowCheckBox { get => (bool)GetValue(ShowCheckBoxProperty); set => SetValue(ShowCheckBoxProperty, value); }
|
||||
public bool ShowRowNumber { get => (bool)GetValue(ShowRowNumberProperty); set => SetValue(ShowRowNumberProperty, value); }
|
||||
/// <summary>可绑定的选中集合。使用 ObservableCollection 可从 VM 增删选中项;未绑定时返回原生选中集合。</summary>
|
||||
public new IList SelectedItems
|
||||
{
|
||||
get => (IList?)GetValue(SelectedItemsProperty) ?? base.SelectedItems;
|
||||
set => SetValue(SelectedItemsProperty, value);
|
||||
}
|
||||
|
||||
static StyledDataGrid()
|
||||
/// <summary>批量恢复性能测试前的选择,只发出一次选择变更。</summary>
|
||||
internal void RestoreSelection(IEnumerable<object> selected)
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(StyledDataGrid), new FrameworkPropertyMetadata(typeof(StyledDataGrid)));
|
||||
}
|
||||
|
||||
public StyledDataGrid()
|
||||
{
|
||||
CommandBindings.Add(new CommandBinding(ToggleSelectAllCommand, (_, e) =>
|
||||
var available = Items.Cast<object>().ToHashSet();
|
||||
BeginUpdateSelectedItems();
|
||||
try
|
||||
{
|
||||
if (AreAllRowsSelected == true) UnselectAll();
|
||||
else SelectAll();
|
||||
e.Handled = true;
|
||||
}, (_, e) =>
|
||||
{
|
||||
e.CanExecute = SelectionMode == DataGridSelectionMode.Extended &&
|
||||
SelectionUnit != DataGridSelectionUnit.Cell && SelectableItemCount > 0;
|
||||
e.Handled = true;
|
||||
}));
|
||||
LoadingRow += (_, e) => UpdateRowNumber(e.Row);
|
||||
ItemContainerGenerator.ItemsChanged += (_, _) => QueueHeaderUpdate();
|
||||
Loaded += (_, _) => QueueHeaderUpdate();
|
||||
}
|
||||
|
||||
private static void HeaderOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
=> ((StyledDataGrid)d).QueueHeaderUpdate();
|
||||
|
||||
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnItemsChanged(e);
|
||||
QueueHeaderUpdate();
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private int SelectableItemCount => Items.Count - (Items.Contains(System.Windows.Data.CollectionView.NewItemPlaceholder) ? 1 : 0);
|
||||
|
||||
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
|
||||
{
|
||||
base.OnSelectionChanged(e);
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private void UpdateSelectAllState()
|
||||
{
|
||||
SetValue(AreAllRowsSelectedPropertyKey, SelectedItems.Count == 0 ? false :
|
||||
SelectedItems.Count == SelectableItemCount ? true : (bool?)null);
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
if (e.Property == FontSizeProperty || e.Property == FontFamilyProperty ||
|
||||
e.Property == FontStyleProperty || e.Property == FontWeightProperty || e.Property == FontStretchProperty)
|
||||
QueueHeaderUpdate();
|
||||
}
|
||||
|
||||
private bool headerUpdatePending;
|
||||
private void QueueHeaderUpdate()
|
||||
{
|
||||
if (headerUpdatePending) return;
|
||||
headerUpdatePending = true;
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
||||
{
|
||||
headerUpdatePending = false;
|
||||
// 使用最宽数字估算所有行号,避免虚拟化滚动时行头宽度跳变。
|
||||
var digits = Math.Max(1, Items.Count.ToString(CultureInfo.InvariantCulture).Length);
|
||||
var numberWidth = 0d;
|
||||
if (ShowRowNumber)
|
||||
for (var digit = '0'; digit <= '9'; digit++)
|
||||
{
|
||||
var text = new FormattedText(new string(digit, digits), CultureInfo.CurrentCulture,
|
||||
FlowDirection, new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
|
||||
FontSize, Foreground ?? Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip);
|
||||
numberWidth = Math.Max(numberWidth, Math.Ceiling(text.WidthIncludingTrailingWhitespace) + 16);
|
||||
}
|
||||
SetCurrentValue(RowHeaderWidthProperty, numberWidth + (ShowCheckBox ? 32 : 0) +
|
||||
(ShowRowNumber && ShowCheckBox ? 1 : 0) + (ShowRowNumber || ShowCheckBox ? 1 : 0));
|
||||
foreach (var row in GetRealizedRows(this)) UpdateRowNumber(row);
|
||||
}));
|
||||
}
|
||||
|
||||
private void UpdateRowNumber(DataGridRow row)
|
||||
=> row.SetCurrentValue(DataGridRow.HeaderProperty, ShowRowNumber ? (row.GetIndex() + 1).ToString(CultureInfo.CurrentCulture) : null);
|
||||
|
||||
private IEnumerable<DataGridRow> GetRealizedRows(DependencyObject parent)
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is DataGridRow row && ItemsControl.ItemsControlFromItemContainer(row) == this) yield return row;
|
||||
else if (child is not DataGrid)
|
||||
foreach (var descendant in GetRealizedRows(child)) yield return descendant;
|
||||
base.SelectedItems.Clear();
|
||||
foreach (var item in selected)
|
||||
if (available.Contains(item)) base.SelectedItems.Add(item);
|
||||
}
|
||||
finally { EndUpdateSelectedItems(); }
|
||||
}
|
||||
|
||||
private bool synchronizingSelection;
|
||||
|
||||
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var grid = (StyledDataGrid)d;
|
||||
if (e.NewValue is IList { IsReadOnly: true } or IList { IsFixedSize: true })
|
||||
throw new ArgumentException("SelectedItems 必须绑定到可增删的集合,例如 ObservableCollection<T>。");
|
||||
if (e.OldValue is INotifyCollectionChanged oldCollection)
|
||||
CollectionChangedEventManager.RemoveHandler(oldCollection, grid.OnBoundSelectionChanged);
|
||||
if (e.NewValue is INotifyCollectionChanged newCollection)
|
||||
CollectionChangedEventManager.AddHandler(newCollection, grid.OnBoundSelectionChanged);
|
||||
grid.ApplyBoundSelection();
|
||||
}
|
||||
|
||||
private void OnBoundSelectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => ApplyBoundSelection();
|
||||
|
||||
private void ApplyBoundSelection()
|
||||
{
|
||||
if (synchronizingSelection || GetValue(SelectedItemsProperty) is not IList boundItems ||
|
||||
ReferenceEquals(boundItems, base.SelectedItems) || SelectionUnit == DataGridSelectionUnit.Cell) return;
|
||||
var available = Items.Cast<object>().ToHashSet();
|
||||
var desired = boundItems.Cast<object>().Where(item => available.Contains(item) &&
|
||||
item != System.Windows.Data.CollectionView.NewItemPlaceholder).Distinct().ToList();
|
||||
if (SelectionMode == DataGridSelectionMode.Single) desired = desired.Take(1).ToList();
|
||||
synchronizingSelection = true;
|
||||
try
|
||||
{
|
||||
var desiredSet = desired.ToHashSet();
|
||||
var current = base.SelectedItems.Cast<object>().ToHashSet();
|
||||
if (SelectionMode == DataGridSelectionMode.Single)
|
||||
SetCurrentValue(SelectedItemProperty, desired.FirstOrDefault());
|
||||
else if (desired.Count == 0) UnselectAll();
|
||||
else
|
||||
{
|
||||
foreach (var item in current)
|
||||
if (!desiredSet.Contains(item)) base.SelectedItems.Remove(item);
|
||||
foreach (var item in desired)
|
||||
if (!current.Contains(item)) base.SelectedItems.Add(item);
|
||||
}
|
||||
}
|
||||
finally { synchronizingSelection = false; }
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private void PublishSelection()
|
||||
{
|
||||
if (synchronizingSelection || GetValue(SelectedItemsProperty) is not IList boundItems ||
|
||||
ReferenceEquals(boundItems, base.SelectedItems)) return;
|
||||
synchronizingSelection = true;
|
||||
try
|
||||
{
|
||||
var current = base.SelectedItems.Cast<object>().ToArray();
|
||||
if (current.Length == 0) boundItems.Clear();
|
||||
else
|
||||
{
|
||||
var currentSet = current.ToHashSet();
|
||||
for (var i = boundItems.Count - 1; i >= 0; i--)
|
||||
if (!currentSet.Contains(boundItems[i]!)) boundItems.RemoveAt(i);
|
||||
var boundSet = boundItems.Cast<object>().ToHashSet();
|
||||
foreach (var item in current)
|
||||
if (boundSet.Add(item)) boundItems.Add(item);
|
||||
}
|
||||
}
|
||||
finally { synchronizingSelection = false; }
|
||||
}
|
||||
|
||||
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
|
||||
{
|
||||
var wasSynchronizing = synchronizingSelection;
|
||||
synchronizingSelection = true;
|
||||
try { base.OnItemsSourceChanged(oldValue, newValue); }
|
||||
finally { synchronizingSelection = wasSynchronizing; }
|
||||
ApplyBoundSelection();
|
||||
}
|
||||
private static readonly DependencyPropertyKey IsSelectionModeActivePropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(IsSelectionModeActive), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty IsSelectionModeActiveProperty = IsSelectionModeActivePropertyKey.DependencyProperty;
|
||||
/// <summary>点击行头勾选框后进入批量选择,取消所有选中后退出。</summary>
|
||||
public bool IsSelectionModeActive => (bool)GetValue(IsSelectionModeActiveProperty);
|
||||
public static readonly RoutedUICommand ToggleSelectAllCommand = new("全选或取消全选", nameof(ToggleSelectAllCommand), typeof(StyledDataGrid));
|
||||
private static readonly DependencyPropertyKey AreAllRowsSelectedPropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(AreAllRowsSelected), typeof(bool?), typeof(StyledDataGrid), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty AreAllRowsSelectedProperty = AreAllRowsSelectedPropertyKey.DependencyProperty;
|
||||
/// <summary>全部选中为 true,部分选中为 null,未选中为 false。</summary>
|
||||
public bool? AreAllRowsSelected => (bool?)GetValue(AreAllRowsSelectedProperty);
|
||||
public static readonly DependencyProperty SelectedRowBackgroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowBackground), typeof(Brush), typeof(StyledDataGrid),
|
||||
new PropertyMetadata(new SolidColorBrush(Color.FromRgb(75, 178, 255))));
|
||||
public static readonly DependencyProperty SelectedRowForegroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowForeground), typeof(Brush), typeof(StyledDataGrid), new PropertyMetadata(Brushes.White));
|
||||
public static readonly DependencyProperty ShowCheckBoxProperty = DependencyProperty.Register(
|
||||
nameof(ShowCheckBox), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
public static readonly DependencyProperty ShowRowNumberProperty = DependencyProperty.Register(
|
||||
nameof(ShowRowNumber), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
|
||||
public Brush SelectedRowBackground { get => (Brush)GetValue(SelectedRowBackgroundProperty); set => SetValue(SelectedRowBackgroundProperty, value); }
|
||||
public Brush SelectedRowForeground { get => (Brush)GetValue(SelectedRowForegroundProperty); set => SetValue(SelectedRowForegroundProperty, value); }
|
||||
/// <summary>勾选状态与行 IsSelected 同步,可通过 SelectedItems 获取。</summary>
|
||||
public bool ShowCheckBox { get => (bool)GetValue(ShowCheckBoxProperty); set => SetValue(ShowCheckBoxProperty, value); }
|
||||
public bool ShowRowNumber { get => (bool)GetValue(ShowRowNumberProperty); set => SetValue(ShowRowNumberProperty, value); }
|
||||
|
||||
static StyledDataGrid()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(StyledDataGrid), new FrameworkPropertyMetadata(typeof(StyledDataGrid)));
|
||||
}
|
||||
|
||||
public StyledDataGrid()
|
||||
{
|
||||
InitializeImageLoadingPause();
|
||||
CommandBindings.Add(new CommandBinding(ToggleSelectAllCommand, (_, e) =>
|
||||
{
|
||||
if (AreAllRowsSelected == true) UnselectAll();
|
||||
else
|
||||
{
|
||||
SelectAll();
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
}
|
||||
e.Handled = true;
|
||||
}, (_, e) =>
|
||||
{
|
||||
e.CanExecute = SelectionMode == DataGridSelectionMode.Extended &&
|
||||
SelectionUnit != DataGridSelectionUnit.Cell && SelectableItemCount > 0;
|
||||
e.Handled = true;
|
||||
}));
|
||||
AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(OnRowCheckBoxClick), true);
|
||||
LoadingRow += (_, e) => UpdateRowNumber(e.Row);
|
||||
ItemContainerGenerator.ItemsChanged += (_, _) => QueueHeaderUpdate();
|
||||
Loaded += (_, _) => { ApplyBoundSelection(); QueueHeaderUpdate(); };
|
||||
}
|
||||
|
||||
private void OnRowCheckBoxClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is not CheckBox checkBox) return;
|
||||
DependencyObject? parent = checkBox;
|
||||
while (parent != null && parent != this)
|
||||
{
|
||||
if (parent is DataGridRowHeader header &&
|
||||
ItemsControl.ItemsControlFromItemContainer(FindRow(header)) == this)
|
||||
{
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
return;
|
||||
}
|
||||
parent = VisualTreeHelper.GetParent(parent);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
|
||||
{
|
||||
if (!e.Handled && ShowCheckBox && ToggleSelectionOnRowHeaderClick && SelectionUnit != DataGridSelectionUnit.Cell)
|
||||
{
|
||||
for (var source = e.OriginalSource as DependencyObject; source != null && source != this;
|
||||
source = source is FrameworkContentElement content ? content.Parent : VisualTreeHelper.GetParent(source))
|
||||
{
|
||||
// 保留行高拖动和嵌套表格自己的鼠标处理。
|
||||
if (source is Thumb || source is DataGrid) break;
|
||||
if (source is not DataGridRowHeader header) continue;
|
||||
var row = FindRow(header);
|
||||
if (row == null || ItemsControl.ItemsControlFromItemContainer(row) != this ||
|
||||
row.Item == System.Windows.Data.CollectionView.NewItemPlaceholder) break;
|
||||
|
||||
// 在 DataGridRowHeader 默认单选逻辑之前处理整个行头,
|
||||
// 包括勾选框四周的留白;阻止 CheckBox 再次切换造成双重反转。
|
||||
e.Handled = true;
|
||||
row.SetCurrentValue(DataGridRow.IsSelectedProperty, !row.IsSelected);
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
base.OnPreviewMouseLeftButtonDown(e);
|
||||
}
|
||||
|
||||
private static DataGridRow? FindRow(DependencyObject element)
|
||||
{
|
||||
for (DependencyObject? parent = element; parent != null; parent = parent is FrameworkContentElement content ? content.Parent : VisualTreeHelper.GetParent(parent))
|
||||
if (parent is DataGridRow row) return row;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void HeaderOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
=> ((StyledDataGrid)d).QueueHeaderUpdate();
|
||||
|
||||
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnItemsChanged(e);
|
||||
QueueHeaderUpdate();
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private int SelectableItemCount => Items.Count - (Items.Contains(System.Windows.Data.CollectionView.NewItemPlaceholder) ? 1 : 0);
|
||||
|
||||
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
|
||||
{
|
||||
PublishSelection();
|
||||
base.OnSelectionChanged(e);
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private void UpdateSelectAllState()
|
||||
{
|
||||
SetValue(AreAllRowsSelectedPropertyKey, base.SelectedItems.Count == 0 ? false :
|
||||
base.SelectedItems.Count == SelectableItemCount ? true : (bool?)null);
|
||||
if (base.SelectedItems.Count == 0) SetValue(IsSelectionModeActivePropertyKey, false);
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
if (e.Property == FontSizeProperty || e.Property == FontFamilyProperty ||
|
||||
e.Property == FontStyleProperty || e.Property == FontWeightProperty || e.Property == FontStretchProperty)
|
||||
QueueHeaderUpdate();
|
||||
}
|
||||
|
||||
private bool headerUpdatePending;
|
||||
private void QueueHeaderUpdate()
|
||||
{
|
||||
if (headerUpdatePending) return;
|
||||
headerUpdatePending = true;
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
||||
{
|
||||
headerUpdatePending = false;
|
||||
foreach (var row in GetRealizedRows(this)) UpdateRowNumber(row);
|
||||
}));
|
||||
}
|
||||
|
||||
private void UpdateRowNumber(DataGridRow row)
|
||||
=> row.SetCurrentValue(DataGridRow.HeaderProperty, ShowRowNumber ? (row.GetIndex() + 1).ToString(CultureInfo.CurrentCulture) : null);
|
||||
|
||||
private IEnumerable<DataGridRow> GetRealizedRows(DependencyObject parent)
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is DataGridRow row && ItemsControl.ItemsControlFromItemContainer(row) == this) yield return row;
|
||||
else if (child is not DataGrid)
|
||||
foreach (var descendant in GetRealizedRows(child)) yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user