diff --git a/docs/performances/DataGrid/README.md b/docs/performances/DataGrid/README.md index f4b5ad9ed..99dde5692 100644 --- a/docs/performances/DataGrid/README.md +++ b/docs/performances/DataGrid/README.md @@ -1,7 +1,7 @@ # DataGrid 性能优化 > 路线图位置:[`../desktop-controls-optimization-roadmap.md`](../desktop-controls-optimization-roadmap.md) Phase F / Tier 4 -> 状态:T4.1 cell header-state binding + row/row group header pointer handler + core input handler + rows presenter scroll gesture / clip geometry + row gridline / details presenter / cell clip geometry cleanup partial done;T4.2 column filter flyout lifecycle + filter indicator binding + column/group header pointer handler cleanup partial done;T4.3 special column detach lifecycle + row expander details binding partial done;T4.4 column reorder drag indicator render partial done;T4.5 row details presenter measure registration partial done;DataGrid core / row / virtualization 仍待后续专项。 +> 状态:T4.1 cell header-state binding + row/row group header pointer handler + core input handler + rows presenter scroll gesture / clip geometry + row gridline / details presenter / cell clip geometry cleanup partial done;T4.2 column filter flyout lifecycle + filter indicator binding + column/group header pointer handler + column header clip cleanup partial done;T4.3 special column detach lifecycle + row expander details binding partial done;T4.4 column reorder drag indicator render partial done;T4.5 row details presenter measure registration partial done;DataGrid core / row / virtualization 仍待后续专项。 --- @@ -15,6 +15,7 @@ - filter indicator 可见性不再为每个列头创建 3 路 `MultiBinding` + converter,改为列头 DirectProperty + `{TemplateBinding}`。 - column header hover 不再为每个 realized `DataGridColumnHeader` 注册本地 `PointerEntered` / `PointerExited` handler,改走 Avalonia 的 virtual override 路径。 - column header press/release/move 不再为每个 realized `DataGridColumnHeader` 注册本地 handler;内部 resize/reorder/sort 逻辑和 `DataGridColumn.HeaderPointerPressed/Released` 转发都改为 class handler。 +- `DataGridColumnHeader` 在 frozen-column 裁剪路径不再每次 header presenter arrange 新建 clip `RectangleGeometry`,改为 header 级复用;不需要裁剪时仍清空 `Clip`。 - column group header press/release 不再为每个 realized `DataGridColumnGroupHeader` 注册本地 forwarding lambda,改为 class handler 转发到 `DataGridColumnGroupItem.HeaderPointerPressed/Released`。 - row header press 不再为每个 realized `DataGridRowHeader` 注册本地 `PointerPressed` handler,改为 class handler,同时保留 focus / selection / current slot 行为。 - row group header press 不再为每个 realized `DataGridRowGroupHeader` 注册本地 `PointerPressed` lambda,改为 class handler,同时保留 current slot 行为。 @@ -96,6 +97,7 @@ Gallery source:`controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/DataG | Runtime `Filters` collection visibility refresh | binding-path dependent | explicit `CollectionChanged` refresh | correctness path added | 正确性修复;add/clear filter items 均验证 | | Column header local hover routed handlers | 2 per realized header | 0 per realized header | 100.00% removed | 结构/分配优化;hover 语义保留在 `OnPointerEntered` / `OnPointerExited` override | | Column header local press/release/move handlers | 5 per standard header | 0 per standard header | 100.00% removed | 结构/分配优化;internal handler + public column event forwarding 均迁到 class handler | +| Column header clip geometry allocation | 1 new `RectangleGeometry` per clipped header arrange | 0 new `RectangleGeometry` per clipped header arrange after first | 100.00% repeated-arrange allocation removed | 结构/分配优化;frozen-column header clip instance 复用、Rect 更新和 clear 已验证 | | Column group header forwarding handlers | 2 per group header | 0 per group header | 100.00% removed | 结构/分配优化;Gallery group header 示例有 4 个 group items | | Row header local press handler | 1 per row header | 0 per row header | 100.00% removed | 结构/分配优化;行头点击 selection / current slot 语义已验证 | | Row group header local press handler | 1 per row group header | 0 per row group header | 100.00% removed | 结构/分配优化;分组头点击 current slot 语义已验证 | @@ -569,6 +571,32 @@ Gallery `Grouping table head` 示例声明 4 个 `DataGridColumnGroupItem`,页 | DataGrid.RowGroups | 10.897 | 2998.9 | 321.0 | 1.0 | smoke 通过;timing 只作异常检查 | | DataGrid.GalleryShape | 45.449 | 12482.3 | 1260.0 | 5.0 | smoke 通过;timing 只作异常检查 | +### 2.20 Column header clip geometry allocation + +本轮优化点:`DataGridColumnHeadersPresenter.EnsureColumnHeaderClip()` 原先只要 scrolling column header 被 frozen columns 或右侧边界裁剪,每次 header presenter arrange 都会创建新的 `RectangleGeometry` 并赋给 `DataGridColumnHeader.Clip`。真实 Gallery `DataGridShowCase` 声明 65 个 `DataGrid`、210 个 column declarations,多个 frozen-column 示例设置 `LeftFrozenColumnCount` / `RightFrozenColumnCount`;水平滚动、窗口尺寸变化、列宽变化和 grid relayout 都会触发 header presenter arrange。 + +修复后 `DataGridColumnHeader` 持有一个 `_clipGeometry`,首次需要裁剪时创建,后续只更新 `Rect`;裁剪不再需要或列头进入 frozen 区时仍清空 `Clip`。由于 `Visual.Clip` 换引用会触发 `AffectsRender`,而复用 geometry 只更新 `RectangleGeometry.Rect`,本轮在 `Rect` 变化时显式 `InvalidateVisual()`,保持裁剪刷新语义。 + +| 场景 | baseline | optimized | 改善 | 结论 | +| --- | --- | --- | --- | --- | +| Column header clip allocation / repeated arrange | 1 new `RectangleGeometry` / clipped header arrange | 0 new `RectangleGeometry` / clipped header arrange after first | 100.00% repeated allocation removed | 有效;每 header 生命周期内从 `N` 次分配降到 `1` 次 | +| 6-column frozen-header verification pass | 1 new `RectangleGeometry` / clipped scrolling header pass | 0 new `RectangleGeometry` / clipped scrolling header pass after first | 100.00% repeated allocation removed | 结构收益;按验证场景单个 clipped scrolling header 估算 | +| Clip geometry instance | replaced every arrange | reused across repeated arrange and horizontal offset change | reuse verified | `--verify-datagrid-states` 覆盖 | +| Clip clear path | assign `Clip = null` | `ClearClipGeometry()` clears `Clip` while keeping reusable cache | 行为保留 | frozen clipping 取消后 `Clip` 清空已验证 | + +当前工作区 smoke: + +| Scenario | ms/item | KB/item | Visual/root | Logical/root | 结论 | +| --- | ---: | ---: | ---: | ---: | --- | +| DataGrid.Basic | 16.045 | 2964.7 | 305.0 | 1.0 | smoke 通过;timing 只作异常检查 | +| DataGrid.Filter.Menu.Closed | 10.194 | 2599.2 | 267.0 | 1.0 | smoke 通过;timing 只作异常检查 | +| DataGrid.Filter.Tree.Closed | 9.336 | 2596.4 | 267.0 | 1.0 | smoke 通过;timing 只作异常检查 | +| DataGrid.RowHeaders | 11.296 | 3122.9 | 336.0 | 1.0 | smoke 通过;timing 只作异常检查 | +| DataGrid.RowDetails.Collapsed | 11.803 | 3041.9 | 320.0 | 1.0 | smoke 通过;timing 只作异常检查 | +| DataGrid.GroupHeaders | 9.993 | 2592.3 | 266.0 | 1.0 | smoke 通过;timing 只作异常检查 | +| DataGrid.RowGroups | 11.546 | 2998.7 | 321.0 | 1.0 | smoke 通过;timing 只作异常检查 | +| DataGrid.GalleryShape | 47.777 | 12479.6 | 1260.0 | 5.0 | smoke 通过;timing 只作异常检查 | + --- ## 3. 验证 @@ -585,7 +613,7 @@ dotnet run -c Release -f net10.0 --no-build \ --verify-datagrid-states ``` -结果:`DataGrid state verification passed.` 覆盖:无 filter items 不创建 shell、关闭态 filter content lazy、运行时 add/clear filter items 后 indicator 可见性和 flyout shell 同步、realized column header 不再注册本地 hover / press / release / move routed handlers、`HeaderPointerPressed` class handler 转发 exactly once 且 sender 保持为 `DataGridColumn`、realized column group header 不再注册本地 press / release forwarding handlers、group `HeaderPointerPressed` class handler 转发 exactly once 且 sender 保持为 `DataGridColumnGroupItem`、realized row header 不再注册本地 press handler 且左键点击仍选中行并更新 `CurrentSlot`、realized row group header 不再注册本地 press handler 且左键点击仍更新 `CurrentSlot`、realized DataGrid core 不再注册本地 `KeyDown` / `KeyUp` / `GotFocus` / `LostFocus` handlers,且 `GotFocus` 更新 `ContainsFocus`、`Down` key 仍推动 current slot、realized rows presenter 不再注册本地 `ScrollGesture` handler,且滚动手势仍 handled 并更新 `VerticalOffset`、rows presenter clip `RectangleGeometry` 跨重复 arrange 和尺寸变化复用且 `Rect` 会更新、row bottom grid-line clip `RectangleGeometry` 跨重复 arrange 和水平偏移变化复用且 `Rect` 会更新、cell clip `RectangleGeometry` 跨 repeated cells presenter arrange 和水平偏移变化复用且 clipping 取消后清空 `Clip`、details presenter clip `RectangleGeometry` 跨重复 arrange 和水平偏移变化复用且 frozen 模式清空 `Clip`、分组行头内 row header 的 owner/template 顺序不再空引用、DataGridCell 跟随 header sort / reorder state 且 detach 后释放订阅、special columns 在 `Columns.Clear()` 后释放 cached owning grid、column reorder drag indicator 复用 cached render pen 且 foreground 变化后重建、DetailsPresenter `ContentHeight` 改变后触发 measure invalidation、RowExpander checked/details visibility 双向同步和 detach 释放。 +结果:`DataGrid state verification passed.` 覆盖:无 filter items 不创建 shell、关闭态 filter content lazy、运行时 add/clear filter items 后 indicator 可见性和 flyout shell 同步、realized column header 不再注册本地 hover / press / release / move routed handlers、`HeaderPointerPressed` class handler 转发 exactly once 且 sender 保持为 `DataGridColumn`、realized column group header 不再注册本地 press / release forwarding handlers、group `HeaderPointerPressed` class handler 转发 exactly once 且 sender 保持为 `DataGridColumnGroupItem`、realized row header 不再注册本地 press handler 且左键点击仍选中行并更新 `CurrentSlot`、realized row group header 不再注册本地 press handler 且左键点击仍更新 `CurrentSlot`、realized DataGrid core 不再注册本地 `KeyDown` / `KeyUp` / `GotFocus` / `LostFocus` handlers,且 `GotFocus` 更新 `ContainsFocus`、`Down` key 仍推动 current slot、realized rows presenter 不再注册本地 `ScrollGesture` handler,且滚动手势仍 handled 并更新 `VerticalOffset`、rows presenter clip `RectangleGeometry` 跨重复 arrange 和尺寸变化复用且 `Rect` 会更新、row bottom grid-line clip `RectangleGeometry` 跨重复 arrange 和水平偏移变化复用且 `Rect` 会更新、cell clip `RectangleGeometry` 跨 repeated cells presenter arrange 和水平偏移变化复用且 clipping 取消后清空 `Clip`、column header clip `RectangleGeometry` 跨 repeated header presenter arrange 和水平偏移变化复用且 clipping 取消后清空 `Clip`、details presenter clip `RectangleGeometry` 跨重复 arrange 和水平偏移变化复用且 frozen 模式清空 `Clip`、分组行头内 row header 的 owner/template 顺序不再空引用、DataGridCell 跟随 header sort / reorder state 且 detach 后释放订阅、special columns 在 `Columns.Clear()` 后释放 cached owning grid、column reorder drag indicator 复用 cached render pen 且 foreground 变化后重建、DetailsPresenter `ContentHeight` 改变后触发 measure invalidation、RowExpander checked/details visibility 双向同步和 detach 释放。 ```bash dotnet run -c Release -f net10.0 --no-build \ @@ -597,11 +625,11 @@ dotnet run -c Release -f net10.0 --no-build \ | Scenario | ms/item | KB/item | Visual/root | Logical/root | | --- | ---: | ---: | ---: | ---: | -| DataGrid.Basic | 15.326 | 2964.9 | 305.0 | 1.0 | -| DataGrid.Filter.Menu.Closed | 10.343 | 2600.7 | 267.0 | 1.0 | -| DataGrid.Filter.Tree.Closed | 8.870 | 2596.6 | 267.0 | 1.0 | -| DataGrid.RowHeaders | 10.448 | 3123.2 | 336.0 | 1.0 | -| DataGrid.RowDetails.Collapsed | 10.622 | 3042.1 | 320.0 | 1.0 | -| DataGrid.GroupHeaders | 10.193 | 2592.2 | 266.0 | 1.0 | -| DataGrid.RowGroups | 10.897 | 2998.9 | 321.0 | 1.0 | -| DataGrid.GalleryShape | 45.449 | 12482.3 | 1260.0 | 5.0 | +| DataGrid.Basic | 16.045 | 2964.7 | 305.0 | 1.0 | +| DataGrid.Filter.Menu.Closed | 10.194 | 2599.2 | 267.0 | 1.0 | +| DataGrid.Filter.Tree.Closed | 9.336 | 2596.4 | 267.0 | 1.0 | +| DataGrid.RowHeaders | 11.296 | 3122.9 | 336.0 | 1.0 | +| DataGrid.RowDetails.Collapsed | 11.803 | 3041.9 | 320.0 | 1.0 | +| DataGrid.GroupHeaders | 9.993 | 2592.3 | 266.0 | 1.0 | +| DataGrid.RowGroups | 11.546 | 2998.7 | 321.0 | 1.0 | +| DataGrid.GalleryShape | 47.777 | 12479.6 | 1260.0 | 5.0 | diff --git a/docs/performances/README.md b/docs/performances/README.md index b52ef49ba..987d309f5 100644 --- a/docs/performances/README.md +++ b/docs/performances/README.md @@ -164,7 +164,7 @@ Avalonia 12 控件库研发的成本模型与高频踩雷点(含 `path:line` | Data Display | Carousel | Done | [Carousel](Carousel/README.md);nav/progress/PageTransition 已按需创建,真实 Gallery alloc `6113.86KB -> 5213.10KB`,cold `164.35ms -> 149.05ms`,repeated timing 基本持平 | | Data Display | Collapse | Done | [Collapse](Collapse/README.md);addon、no-arrow、closed content 已按需创建,Gallery repeated mean 提升约 `16.03%` | | Data Display | Descriptions | Done | [Descriptions](Descriptions/README.md);`DescriptionsShowCase` repeated mean `75.84ms -> 68.64ms`,visuals `579 -> 571`;binding/collection/window 订阅生命周期已补齐验证 | -| Data Display | DataGrid | Partial | [DataGrid](DataGrid/README.md);filter flyout 内容延迟到首次打开,关闭态过滤菜单项 `7 -> 0`;filter indicator 可见性由 `MultiBinding` 收敛到 `TemplateBinding`;column/group/row header 本地 routed handlers 已收敛(column header hover `2/header -> 0/header`,press/release/move + forwarding `5/header -> 0/header`,group header forwarding `2/header -> 0/header`,row header press `1/header -> 0/header`,row group header press `1/header -> 0/header`);DataGrid core input handlers `4/grid -> 0/grid`;rows presenter scroll gesture handler `1/presenter -> 0/presenter`;rows presenter clip geometry repeated arrange allocation `1 -> 0` after first;row bottom grid-line clip repeated arrange allocation `1/row -> 0/row` after first;details presenter clip repeated arrange allocation `1/details -> 0/details` after first;cell clip repeated arrange allocation `1/cell -> 0/cell` after first;DataGridCell header-state binding converter 闭包已移除;special columns 在 `Columns.Clear()` 后释放 grid 事件链;列重排拖拽 indicator 复用 dashed `Pen`;DetailsPresenter measure registration 与 RowExpander details binding cleanup 已完成;分组行头内 row header owner/template 顺序空引用已修复 | +| Data Display | DataGrid | Partial | [DataGrid](DataGrid/README.md);filter flyout 内容延迟到首次打开,关闭态过滤菜单项 `7 -> 0`;filter indicator 可见性由 `MultiBinding` 收敛到 `TemplateBinding`;column/group/row header 本地 routed handlers 已收敛(column header hover `2/header -> 0/header`,press/release/move + forwarding `5/header -> 0/header`,group header forwarding `2/header -> 0/header`,row header press `1/header -> 0/header`,row group header press `1/header -> 0/header`);column header clip repeated arrange allocation `1/header -> 0/header` after first;DataGrid core input handlers `4/grid -> 0/grid`;rows presenter scroll gesture handler `1/presenter -> 0/presenter`;rows presenter clip geometry repeated arrange allocation `1 -> 0` after first;row bottom grid-line clip repeated arrange allocation `1/row -> 0/row` after first;details presenter clip repeated arrange allocation `1/details -> 0/details` after first;cell clip repeated arrange allocation `1/cell -> 0/cell` after first;DataGridCell header-state binding converter 闭包已移除;special columns 在 `Columns.Clear()` 后释放 grid 事件链;列重排拖拽 indicator 复用 dashed `Pen`;DetailsPresenter measure registration 与 RowExpander details binding cleanup 已完成;分组行头内 row header owner/template 顺序空引用已修复 | | Data Display | Expander | Done | [Expander](Expander/README.md);closed content/addon/no-arrow slot 已按需创建,Gallery repeated mean 提升约 `18.11%` | | Data Display | Empty | Done | [Empty](Empty/README.md);低风险正确性修复与热路径收敛,Gallery repeated mean 小幅改善,visual tree 不变 | | Data Display | GroupBox | Done | [GroupBox](GroupBox/README.md);header icon presenter 已按需创建,`GroupBoxShowCase` visuals `151 -> 143`,repeated mean `27.79ms -> 26.16ms` | diff --git a/docs/performances/desktop-controls-optimization-roadmap.md b/docs/performances/desktop-controls-optimization-roadmap.md index b763a494e..961ba86a1 100644 --- a/docs/performances/desktop-controls-optimization-roadmap.md +++ b/docs/performances/desktop-controls-optimization-roadmap.md @@ -121,7 +121,7 @@ | 控件 | 状态 | 跨包依赖 | | --- | --- | --- | -| DataGrid | Partial | Primitives.ArrowDecoratedBox, Buttons.Button, TextBlock, Pagination, PopupConfirm, ScrollViewer, Spin, Tooltip.ToolTip, TreeView.TreeViewItemTheme;cell header-state binding / filter flyout lifecycle / filter indicator binding / column/group/row/row group header pointer handler cleanup / core input handler cleanup / rows presenter scroll gesture + clip geometry cleanup / row bottom gridline clip cleanup / details presenter clip cleanup / cell clip cleanup / special column detach lifecycle / column reorder indicator pen cache / details presenter measure registration / row expander details binding 已完成,详见 [DataGrid](DataGrid/README.md) | +| DataGrid | Partial | Primitives.ArrowDecoratedBox, Buttons.Button, TextBlock, Pagination, PopupConfirm, ScrollViewer, Spin, Tooltip.ToolTip, TreeView.TreeViewItemTheme;cell header-state binding / filter flyout lifecycle / filter indicator binding / column/group/row/row group header pointer handler cleanup / column header clip cleanup / core input handler cleanup / rows presenter scroll gesture + clip geometry cleanup / row bottom gridline clip cleanup / details presenter clip cleanup / cell clip cleanup / special column detach lifecycle / column reorder indicator pen cache / details presenter measure registration / row expander details binding 已完成,详见 [DataGrid](DataGrid/README.md) | | DataGridRow / DataGridCell / DataGridColumnHeader 等 ~40 内部子控件 | Pending | 同上 | DataGrid 是最重的复合控件,独立子表才是合理的优化粒度。 @@ -240,7 +240,7 @@ DataGrid 是单文件最复杂的控件之一(~110 个公开类型)。依赖 | # | 子模块 | 备注 | | --- | --- | --- | | T4.1 | DataGrid 核心(DataGrid / DataGridRow / DataGridCell / Presenter 系列) | Partial:DataGridCell header sort / reorder state binding converter 闭包已移除,cell clip repeated arrange 分配已收敛;DataGridRowHeader press 本地 handler 已迁到 class handler,且 owner/template 顺序空引用已修复;DataGridRowGroupHeader press 本地 handler 已迁到 class handler;DataGrid core input handlers 已迁到 virtual override;DataGridRowsPresenter scroll gesture handler 已迁到 class handler,presenter clip geometry repeated arrange 分配已收敛;DataGridRow bottom gridline clip repeated arrange 分配已收敛;DataGridDetailsPresenter clip repeated arrange 分配已收敛;DetailsPresenter measure registration 已收敛;DataGrid / DataGridRow / 其他 Presenter / virtualization 仍待专项 | -| T4.2 | 列模型(DataGridColumn / DataGridColumnHeader / FilterFlyout / SortIndicator) | Partial:filter flyout closed-state 内容已延迟;filter indicator visibility binding 已收敛;column header hover / press / release / move 本地 handler 已清理;column group header press / release forwarding handler 已清理;其余 sort / resize / reorder 仍待专项 | +| T4.2 | 列模型(DataGridColumn / DataGridColumnHeader / FilterFlyout / SortIndicator) | Partial:filter flyout closed-state 内容已延迟;filter indicator visibility binding 已收敛;column header hover / press / release / move 本地 handler 已清理;column header clip repeated arrange 分配已收敛;column group header press / release forwarding handler 已清理;其余 sort / resize / reorder 仍待专项 | | T4.3 | 行展开 / 详情(RowExpander / DetailsPresenter) | Partial:detail / reorder / selection / checkbox / operation columns 的 `Columns.Clear()` detach 事件链已释放;DetailsPresenter `ContentHeight` measure registration 已移到 static constructor;DetailsPresenter clip repeated arrange 分配已收敛且 frozen clear 已验证;RowExpander details visibility binding 已从 `RelayBind` 收敛到 direct observable + detach 释放;其余 row details layout 仍需与 T4.1 协同 | | T4.4 | 行重排 / 选择(RowReorder / SelectionColumn) | Partial:column reorder dragging-over indicator 已缓存 dashed `Pen`;拖拽全局订阅仍是 Lifecycle Pairing 重点 | | T4.5 | 分组 / 过滤(CollectionView / FilterDescription / GroupDescription) | 数据层成本,不在主题路径上但是真热点 | diff --git a/src/AtomUI.Desktop.Controls.DataGrid/Column/DataGridColumnHeader.cs b/src/AtomUI.Desktop.Controls.DataGrid/Column/DataGridColumnHeader.cs index d03d4961e..b0d286884 100644 --- a/src/AtomUI.Desktop.Controls.DataGrid/Column/DataGridColumnHeader.cs +++ b/src/AtomUI.Desktop.Controls.DataGrid/Column/DataGridColumnHeader.cs @@ -288,6 +288,7 @@ internal partial class DataGridColumnHeader : ContentControl private bool _areHandlersSuspended; private bool _desiredSeparatorVisibility = true; private INotifyCollectionChanged? _subscribedFilterItems; + private RectangleGeometry? _clipGeometry; private static Lazy ResizeCursor = new (() => new Cursor(StandardCursorType.SizeWestEast)); static DataGridColumnHeader() @@ -329,6 +330,35 @@ internal partial class DataGridColumnHeader : ContentControl SetValueNoCallback(IsSeparatorsVisibleProperty, newVisibility); } } + + internal void UpdateClipGeometry(Rect clipRect) + { + if (_clipGeometry is null) + { + _clipGeometry = new RectangleGeometry + { + Rect = clipRect + }; + } + else if (!_clipGeometry.Rect.Equals(clipRect)) + { + _clipGeometry.Rect = clipRect; + InvalidateVisual(); + } + + if (!ReferenceEquals(Clip, _clipGeometry)) + { + Clip = _clipGeometry; + } + } + + internal void ClearClipGeometry() + { + if (Clip is not null) + { + Clip = null; + } + } internal void HandleMouseLeftButtonUpClick(KeyModifiers keyModifiers, ref bool handled) { diff --git a/src/AtomUI.Desktop.Controls.DataGrid/Column/DataGridColumnHeadersPresenter.cs b/src/AtomUI.Desktop.Controls.DataGrid/Column/DataGridColumnHeadersPresenter.cs index 8e1c58bac..e944e447b 100644 --- a/src/AtomUI.Desktop.Controls.DataGrid/Column/DataGridColumnHeadersPresenter.cs +++ b/src/AtomUI.Desktop.Controls.DataGrid/Column/DataGridColumnHeadersPresenter.cs @@ -132,7 +132,7 @@ public sealed class DataGridColumnHeadersPresenter : Panel, IChildIndexProvider if (dataGridColumn.IsLeftFrozen) { columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height)); - columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform + columnHeader.ClearClipGeometry(); // The layout system could have clipped this because it's not aware of our render transform if (DragColumn == dataGridColumn && DragIndicator != null) { dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset; @@ -174,7 +174,7 @@ public sealed class DataGridColumnHeadersPresenter : Panel, IChildIndexProvider { frozenRightEdge -= dataGridColumn.ActualWidth; columnHeader.Arrange(new Rect(frozenRightEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height)); - columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform + columnHeader.ClearClipGeometry(); // The layout system could have clipped this because it's not aware of our render transform columnHeader.IsFrozen = true; columnHeader.FrozenShadowPosition = FrozenColumnShadowPosition.Left; var horizontalScrollBarVisible = OwningGrid.HorizontalScrollBar?.IsVisible ?? false; @@ -241,30 +241,28 @@ public sealed class DataGridColumnHeadersPresenter : Panel, IChildIndexProvider frozenLeftEdge > columnHeaderLeftEdge && columnHeaderRightEdge > frozenRightEdge) { - RectangleGeometry rg = new RectangleGeometry(); double xClip = Math.Round(Math.Min(width, frozenLeftEdge - columnHeaderLeftEdge)); - rg.Rect = new Rect(xClip, 0, Math.Max(0, width - (frozenLeftEdge - columnHeaderLeftEdge) - (columnHeaderRightEdge - frozenRightEdge)), height); - columnHeader.Clip = rg; + columnHeader.UpdateClipGeometry(new Rect( + xClip, + 0, + Math.Max(0, width - (frozenLeftEdge - columnHeaderLeftEdge) - (columnHeaderRightEdge - frozenRightEdge)), + height)); } else if (columnHeader.OwningColumn != null && !columnHeader.OwningColumn.IsFrozen && frozenLeftEdge > columnHeaderLeftEdge) { - RectangleGeometry rg = new RectangleGeometry(); double xClip = Math.Round(Math.Min(width, frozenLeftEdge - columnHeaderLeftEdge)); - rg.Rect = new Rect(xClip, 0, Math.Max(0, width - xClip), height); - columnHeader.Clip = rg; + columnHeader.UpdateClipGeometry(new Rect(xClip, 0, Math.Max(0, width - xClip), height)); } else if (columnHeader.OwningColumn != null && !columnHeader.OwningColumn.IsFrozen && columnHeaderRightEdge > frozenRightEdge) { - RectangleGeometry rg = new RectangleGeometry(); double xClip = Math.Round(Math.Min(width, columnHeaderRightEdge - frozenRightEdge)); - rg.Rect = new Rect(0, 0, Math.Max(0, width - xClip), height); - columnHeader.Clip = rg; + columnHeader.UpdateClipGeometry(new Rect(0, 0, Math.Max(0, width - xClip), height)); } else { - columnHeader.Clip = null; + columnHeader.ClearClipGeometry(); } } diff --git a/tools/performances/AtomUI.Performance/Suites/DataGrid/DataGridStateVerification.cs b/tools/performances/AtomUI.Performance/Suites/DataGrid/DataGridStateVerification.cs index fb70c83eb..cc7a6766b 100644 --- a/tools/performances/AtomUI.Performance/Suites/DataGrid/DataGridStateVerification.cs +++ b/tools/performances/AtomUI.Performance/Suites/DataGrid/DataGridStateVerification.cs @@ -33,6 +33,7 @@ internal static partial class Program VerifyDataGridRowsPresenterReusesClipGeometry(failures); VerifyDataGridRowReusesBottomGridLineClipGeometry(failures); VerifyDataGridCellsPresenterReusesCellClipGeometry(failures); + VerifyDataGridColumnHeadersPresenterReusesHeaderClipGeometry(failures); VerifyDataGridCellHeaderStateBindings(failures); VerifyDataGridSpecialColumnsReleaseGridSubscriptionsOnClear(failures); VerifyDataGridColumnDragIndicatorCachesPen(failures); @@ -645,6 +646,91 @@ internal static partial class Program failures); } + private static void VerifyDataGridColumnHeadersPresenterReusesHeaderClipGeometry(ICollection failures) + { + var grid = CreateDataGridShell(8); + grid.Width = 260; + grid.LeftFrozenColumnCount = 1; + grid.RightFrozenColumnCount = 1; + var columns = new List(); + for (var i = 0; i < 6; i++) + { + var column = new DataGridTextColumn + { + Header = $"Column {i + 1}", + Width = new DataGridLength(120), + Binding = new Binding(GetDataGridBindingPath(i)) + }; + columns.Add(column); + grid.Columns.Add(column); + } + + using var realized = RealizeControl(grid); + + Expect(UpdateDataGridHorizontalOffset(grid, 50), + "DataGrid should accept a reflected horizontal offset update for column header clip verification.", + failures); + RefreshLayout(realized.Window); + + var header = GetDataGridColumnHeader(columns[1], failures); + if (header is null) + { + return; + } + + var firstClip = header.Clip as RectangleGeometry; + Expect(firstClip is not null && firstClip.Rect.X > 0, + $"DataGrid scrolling column header should be clipped by a RectangleGeometry after horizontal offset. Actual: {header.Clip?.GetType().Name ?? "null"} {firstClip?.Rect}.", + failures); + if (firstClip is null) + { + return; + } + + var firstRect = firstClip.Rect; + Expect(firstRect.Height > 0, + $"DataGrid scrolling column header clip should have usable bounds. Actual: {firstRect}.", + failures); + + foreach (var presenter in GetDataGridColumnHeadersPresenters(grid)) + { + presenter.InvalidateArrange(); + } + RefreshLayout(realized.Window); + + var secondClip = header.Clip as RectangleGeometry; + Expect(ReferenceEquals(firstClip, secondClip), + "DataGrid column header should reuse its clip RectangleGeometry across repeated header presenter arrange passes.", + failures); + + Expect(UpdateDataGridHorizontalOffset(grid, 80), + "DataGrid should accept a second horizontal offset update for column header clip verification.", + failures); + RefreshLayout(realized.Window); + + var scrolledClip = header.Clip as RectangleGeometry; + Expect(ReferenceEquals(firstClip, scrolledClip), + "DataGrid column header should keep reusing the same clip RectangleGeometry after horizontal offset changes.", + failures); + Expect(scrolledClip is not null && !scrolledClip.Rect.Equals(firstRect), + $"DataGrid reused column header clip geometry should update Rect after horizontal offset changes. Before: {firstRect}, after: {scrolledClip?.Rect}.", + failures); + + grid.LeftFrozenColumnCount = 0; + grid.RightFrozenColumnCount = 0; + grid.Width = 900; + UpdateDataGridHorizontalOffset(grid, 0); + foreach (var presenter in GetDataGridColumnHeadersPresenters(grid)) + { + presenter.InvalidateArrange(); + } + RefreshLayout(realized.Window); + + Expect(header.Clip is null, + "DataGrid column header should clear Clip when frozen-column clipping is no longer needed.", + failures); + } + private static void VerifyDataGridCellHeaderStateBindings(ICollection failures) { var grid = CreateDataGridShell(4); @@ -1004,6 +1090,13 @@ internal static partial class Program .ToList(); } + private static List GetDataGridColumnHeadersPresenters(Control root) + { + return root.GetSelfAndVisualDescendants() + .OfType() + .ToList(); + } + private static List GetDataGridDetailsPresenters(Control root) { return root.GetSelfAndVisualDescendants()