Add DataGrid

This commit is contained in:
CHINBOY
2025-05-28 21:31:55 +08:00
parent 4dea506a3e
commit f6f4568710
4 changed files with 9587 additions and 183 deletions
File diff suppressed because it is too large Load Diff
+265 -162
View File
@@ -2,11 +2,14 @@ using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using AtomUI.Controls.Data;
using AtomUI.Controls.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace AtomUI.Controls;
@@ -30,11 +33,6 @@ public partial class DataGrid
private set;
}
internal DataGridColumnCollection ColumnsInternal
{
get;
}
internal double RowHeightEstimate
{
get;
@@ -75,18 +73,67 @@ public partial class DataGrid
}
}
private DataGridCellCoordinates CurrentCellCoordinates
private DataGridCellCoordinates CurrentCellCoordinates { get; set; }
internal double RowGroupHeaderHeightEstimate { get; private set; }
internal bool ContainsFocus { get; private set; }
internal double NegVerticalOffset { get; private set; }
private int NoSelectionChangeCount
{
get;
set;
get => _noSelectionChangeCount;
set
{
_noSelectionChangeCount = value;
if (value == 0)
{
FlushSelectionChanged();
}
}
}
internal double RowGroupHeaderHeightEstimate
{
get;
private set;
}
// This flag indicates whether selection has actually changed during a selection operation,
// and exists to ensure that FlushSelectionChanged doesn't unnecessarily raise SelectionChanged.
internal bool SelectionHasChanged { get; set; }
/// <summary>
/// Indicates whether or not to use star-sizing logic. If the DataGrid has infinite available space,
/// then star sizing doesn't make sense. In this case, all star columns grow to a predefined size of
/// 10,000 pixels in order to show the developer that star columns shouldn't be used.
/// </summary>
internal bool UsesStarSizing => ColumnsInternal.VisibleStarColumnCount > 0 &&
(!RowsPresenterAvailableSize.HasValue || !double.IsPositiveInfinity(RowsPresenterAvailableSize.Value.Width));
/// <summary>
/// Indicates whether or not at least one auto-sizing column is waiting for all the rows
/// to be measured before its final width is determined.
/// </summary>
internal bool AutoSizingColumns
{
get => _autoSizingColumns;
set
{
if (_autoSizingColumns && !value)
{
double adjustment = CellsWidth - ColumnsInternal.VisibleEdgedColumnsWidth;
AdjustColumnWidths(0, adjustment, false);
foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns())
{
column.IsInitialDesiredWidthDetermined = true;
}
ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
ComputeScrollBarsLayout();
InvalidateColumnHeadersMeasure();
InvalidateRowsMeasure(true);
}
_autoSizingColumns = value;
}
}
internal ScrollBar? VerticalScrollBar => _vScrollBar;
#endregion
/// <summary>
@@ -111,6 +158,21 @@ public partial class DataGrid
private const double DefaultMinColumnWidth = 20;
private const double DefaultMaxColumnWidth = double.PositiveInfinity;
private List<Exception> _bindingValidationErrors = [];
private IDisposable? _validationSubscription;
private INotifyCollectionChanged? _topLevelGroup;
private ContentControl? _clipboardContentControl;
private Visual? _bottomRightCorner;
private DataGridRowsPresenter? _rowsPresenter;
private ScrollBar? _vScrollBar;
private ScrollBar? _hScrollBar;
private ContentControl? _topLeftCornerHeader;
private ContentControl? _topRightCornerHeader;
private Control? _frozenColumnScrollBarSpacer;
// Nth row of rows 0..N that make up the RowHeightEstimate
private int _lastEstimatedRow;
private List<DataGridRow> _loadedRows;
@@ -119,9 +181,21 @@ public partial class DataGrid
private Queue<Action> _lostFocusActions;
private IndexToValueTable<bool> _showDetailsTable;
private DataGridSelectedItemsCollection _selectedItems;
private List<Exception> _bindingValidationErrors;
private double _rowHeaderDesiredWidth;
private int? _mouseOverRowIndex; // -1 is used for the 'new row'
private bool _makeFirstDisplayedCellCurrentCellPending;
private bool _measured;
private int _noSelectionChangeCount;
private bool _scrollingByHeight;
private bool _temporarilyResetCurrentCell;
private DataGridColumn? _previousCurrentColumn;
private object? _previousCurrentItem;
private double[] _rowGroupHeightsByLevel;
private object? _uneditedValue; // Represents the original current cell value at the time it enters editing mode.
// the sum of the widths in pixels of the scrolling columns preceding
// the first displayed scrolling column
private double _horizontalOffset;
// the number of pixels of the firstDisplayedScrollingCol which are not displayed
private double _negHorizontalOffset;
@@ -129,49 +203,64 @@ public partial class DataGrid
private bool _areHandlersSuspended;
private bool _autoSizingColumns;
private IndexToValueTable<bool> _collapsedSlotsTable;
// private Control _clickedElement;
private Control? _clickedElement;
// used to store the current column during a Reset
private int _desiredCurrentColumnIndex;
private int _editingColumnIndex;
// this is a workaround only for the scenarios where we need it, it is not all encompassing nor always updated
private RoutedEventArgs? _editingEventArgs;
private bool _executingLostFocusActions;
private bool _flushCurrentCellChanged;
private bool _focusEditingControl;
private Visual? _focusedObject;
private byte _horizontalScrollChangesIgnored;
private DataGridRow? _focusedRow;
private bool _ignoreNextScrollBarsLayout;
private bool _successfullyUpdatedSelection;
// An approximation of the sum of the heights in pixels of the scrolling rows preceding
// the first displayed scrolling row. Since the scrolled off rows are discarded, the grid
// does not know their actual height. The heights used for the approximation are the ones
// set as the rows were scrolled off.
private double _verticalOffset;
private byte _verticalScrollChangesIgnored;
private void FlushCurrentCellChanged()
{
// if (_makeFirstDisplayedCellCurrentCellPending)
// {
// return;
// }
// if (SelectionHasChanged)
// {
// // selection is changing, don't raise CurrentCellChanged until it's done
// _flushCurrentCellChanged = true;
// FlushSelectionChanged();
// return;
// }
//
// // We don't want to expand all intermediate currency positions, so we only expand
// // the last current item before we flush the event
// if (_collapsedSlotsTable.Contains(CurrentSlot))
// {
// DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(CurrentSlot));
// Debug.Assert(rowGroupInfo != null);
// if (rowGroupInfo != null)
// {
// ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot);
// }
// }
//
// if (CurrentColumn != _previousCurrentColumn
// || CurrentItem != _previousCurrentItem)
// {
// CoerceSelectedItem();
// _previousCurrentColumn = CurrentColumn;
// _previousCurrentItem = CurrentItem;
//
// OnCurrentCellChanged(EventArgs.Empty);
// }
//
// _flushCurrentCellChanged = false;
if (_makeFirstDisplayedCellCurrentCellPending)
{
return;
}
if (SelectionHasChanged)
{
// selection is changing, don't raise CurrentCellChanged until it's done
_flushCurrentCellChanged = true;
FlushSelectionChanged();
return;
}
// We don't want to expand all intermediate currency positions, so we only expand
// the last current item before we flush the event
if (_collapsedSlotsTable.Contains(CurrentSlot))
{
var rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(CurrentSlot));
Debug.Assert(rowGroupInfo != null);
ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot);
}
if (CurrentColumn != _previousCurrentColumn
|| CurrentItem != _previousCurrentItem)
{
CoerceSelectedItem();
_previousCurrentColumn = CurrentColumn;
_previousCurrentItem = CurrentItem;
OnCurrentCellChanged(EventArgs.Empty);
}
_flushCurrentCellChanged = false;
}
#region 事件处理器
@@ -186,121 +275,121 @@ public partial class DataGrid
private void HandleKeyUp(object? sender, KeyEventArgs e)
{
// if (e.Key == Key.Tab && CurrentColumnIndex != -1 && e.Source == this)
// {
// bool success =
// ScrollSlotIntoView(
// CurrentColumnIndex, CurrentSlot,
// forCurrentCellChange: false,
// forceHorizontalScroll: true);
// Debug.Assert(success);
// if (CurrentColumnIndex != -1 && SelectedItem == null)
// {
// SetRowSelection(CurrentSlot, isSelected: true, setAnchorSlot: true);
// }
// }
if (e.Key == Key.Tab && CurrentColumnIndex != -1 && e.Source == this)
{
bool success =
ScrollSlotIntoView(
CurrentColumnIndex, CurrentSlot,
forCurrentCellChange: false,
forceHorizontalScroll: true);
Debug.Assert(success);
if (CurrentColumnIndex != -1 && SelectedItem == null)
{
SetRowSelection(CurrentSlot, isSelected: true, setAnchorSlot: true);
}
}
}
private void HandleGotFocus(object? sender, RoutedEventArgs e)
{
// if (!ContainsFocus)
// {
// ContainsFocus = true;
// ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
// if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot))
// {
// if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row)
// {
// row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
// }
// }
// }
//
// // Keep track of which row contains the newly focused element
// DataGridRow focusedRow = null;
// Visual focusedElement = e.Source as Visual;
// _focusedObject = focusedElement;
// while (focusedElement != null)
// {
// focusedRow = focusedElement as DataGridRow;
// if (focusedRow != null && focusedRow.OwningGrid == this && _focusedRow != focusedRow)
// {
// ResetFocusedRow();
// _focusedRow = focusedRow.IsVisible ? focusedRow : null;
// break;
// }
// focusedElement = focusedElement.GetVisualParent();
// }
if (!ContainsFocus)
{
ContainsFocus = true;
ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot))
{
if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row)
{
row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
}
}
}
// Keep track of which row contains the newly focused element
DataGridRow? focusedRow = null;
Visual? focusedElement = e.Source as Visual;
_focusedObject = focusedElement;
while (focusedElement != null)
{
focusedRow = focusedElement as DataGridRow;
if (focusedRow != null && focusedRow.OwningGrid == this && _focusedRow != focusedRow)
{
ResetFocusedRow();
_focusedRow = focusedRow.IsVisible ? focusedRow : null;
break;
}
focusedElement = focusedElement.GetVisualParent();
}
}
private void HandleLostFocus(object? sender, RoutedEventArgs e)
{
// _focusedObject = null;
// if (ContainsFocus)
// {
// bool focusLeftDataGrid = true;
// bool dataGridWillReceiveRoutedEvent = true;
// Visual focusedObject = FocusManager.GetFocusManager(this)?.GetFocusedElement() as Visual;
// DataGridColumn editingColumn = null;
//
// while (focusedObject != null)
// {
// if (focusedObject == this)
// {
// focusLeftDataGrid = false;
// break;
// }
//
// // Walk up the visual tree. If we hit the root, try using the framework element's
// // parent. We do this because Popups behave differently with respect to the visual tree,
// // and it could have a parent even if the VisualTreeHelper doesn't find it.
// var parent = focusedObject.Parent as Visual;
// if (parent == null)
// {
// parent = focusedObject.GetVisualParent();
// }
// else
// {
// dataGridWillReceiveRoutedEvent = false;
// }
// focusedObject = parent;
// }
//
// if (EditingRow != null && EditingColumnIndex != -1)
// {
// editingColumn = ColumnsItemsInternal[EditingColumnIndex];
//
// if (focusLeftDataGrid && editingColumn is DataGridTemplateColumn)
// {
// dataGridWillReceiveRoutedEvent = false;
// }
// }
//
// if (focusLeftDataGrid && !(editingColumn is DataGridTemplateColumn))
// {
// ContainsFocus = false;
// if (EditingRow != null)
// {
// CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true);
// }
// ResetFocusedRow();
// ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
// if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot))
// {
// if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row)
// {
// row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
// }
// }
// }
// else if (!dataGridWillReceiveRoutedEvent)
// {
// if (focusedObject is Control focusedElement)
// {
// focusedElement.LostFocus += ExternalEditingElement_LostFocus;
// }
// }
// }
_focusedObject = null;
if (ContainsFocus)
{
bool focusLeftDataGrid = true;
bool dataGridWillReceiveRoutedEvent = true;
Visual? focusedObject = FocusUtils.GetFocusManager(this)?.GetFocusedElement() as Visual;
DataGridColumn? editingColumn = null;
while (focusedObject != null)
{
if (focusedObject == this)
{
focusLeftDataGrid = false;
break;
}
// Walk up the visual tree. If we hit the root, try using the framework element's
// parent. We do this because Popups behave differently with respect to the visual tree,
// and it could have a parent even if the VisualTreeHelper doesn't find it.
var parent = focusedObject.Parent as Visual;
if (parent == null)
{
parent = focusedObject.GetVisualParent();
}
else
{
dataGridWillReceiveRoutedEvent = false;
}
focusedObject = parent;
}
if (EditingRow != null && EditingColumnIndex != -1)
{
editingColumn = ColumnsItemsInternal[EditingColumnIndex];
if (focusLeftDataGrid && editingColumn is DataGridTemplateColumn)
{
dataGridWillReceiveRoutedEvent = false;
}
}
if (focusLeftDataGrid && !(editingColumn is DataGridTemplateColumn))
{
ContainsFocus = false;
if (EditingRow != null)
{
CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true);
}
ResetFocusedRow();
ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot))
{
if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row)
{
row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
}
}
}
else if (!dataGridWillReceiveRoutedEvent)
{
if (focusedObject is Control focusedElement)
{
focusedElement.LostFocus += HandleExternalEditingElementLostFocus;
}
}
}
}
private void HandleColumnsInternalCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
@@ -330,11 +419,6 @@ public partial class DataGrid
// PseudoClasses.Set(":empty-rows", !DataConnection.Any());
}
internal DataGridColumnCollection CreateColumnsInstance()
{
return new DataGridColumnCollection(this);
}
private void SetValueNoCallback<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
{
_areHandlersSuspended = true;
@@ -347,4 +431,23 @@ public partial class DataGrid
_areHandlersSuspended = false;
}
}
private void EnsureVerticalGridLines()
{
if (AreColumnHeadersVisible)
{
double totalColumnsWidth = 0;
foreach (DataGridColumn column in ColumnsInternal)
{
totalColumnsWidth += column.ActualWidth;
column.HeaderCell.AreSeparatorsVisible = (column != ColumnsInternal.LastVisibleColumn || totalColumnsWidth < CellsWidth);
}
}
foreach (DataGridRow row in GetAllRows())
{
row.EnsureGridLines();
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff