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; namespace Main.Test; /// 带默认样式、行号和行选择勾选框的 DataGrid。 public partial class StyledDataGrid : DataGrid { public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register( nameof(SelectedItems), typeof(IList), typeof(StyledDataGrid), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged)); /// 可绑定的选中集合。使用 ObservableCollection 可从 VM 增删选中项;未绑定时返回原生选中集合。 public new IList SelectedItems { get => (IList?)GetValue(SelectedItemsProperty) ?? base.SelectedItems; set => SetValue(SelectedItemsProperty, value); } /// 批量恢复性能测试前的选择,只发出一次选择变更。 internal void RestoreSelection(IEnumerable selected) { var available = Items.Cast().ToHashSet(); BeginUpdateSelectedItems(); try { 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。"); 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().ToHashSet(); var desired = boundItems.Cast().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().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().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().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; /// 点击行头勾选框后进入批量选择,取消所有选中后退出。 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; /// 全部选中为 true,部分选中为 null,未选中为 false。 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); } /// 勾选状态与行 IsSelected 同步,可通过 SelectedItems 获取。 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 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; } } }