最终,这是根本原因:
引用自https://dev59.com/jnA75IYBdhLWcg3w3NHf#3062692:
你正在遇到物理滚动和逻辑滚动之间的差异。正如你已经发现的,每种方式都有其权衡利弊。
物理滚动(CanContentScroll=false)仅按像素进行滚动,因此:
- 视口始终代表滚动范围的完全相同部分,提供平滑的滚动体验;
- 但是,数据网格的所有内容必须应用所有模板并测量和排列以确定滚动条的大小,导致加载期间出现长时间延迟和高内存使用率,并且它实际上不会滚动项目,因此无法很好地理解 ScrollIntoView。
逻辑滚动(CanContentScroll=true)通过项目而不是像素计算其滚动视口和范围,因此:
- 视口可能在不同的时间显示不同数量的项目,这意味着视口中的项目数与范围中的项目数相比会有所变化,从而导致滚动条长度改变;
- 滚动从一个项目移动到下一个项目,而不是在两个项目之间移动,导致“抖动”滚动;
- 但是,只要在幕后使用 VirtualizingStackPanel,它就只需要应用模板并测量和排列当前实际可见的项目,而且 ScrollIntoView 更简单,因为它只需要将正确的项目索引置于视图中。
这些是 WPF 提供的唯一两种滚动方式。您必须根据上述权衡来选择它们。通常,逻辑滚动最适合中等到大型数据集,而物理滚动最适合小型数据集。
加快物理滚动加载的技巧是将您的项目包装在一个自定义装饰器中,该装饰器具有固定大小,并在其不可见时将其子项的 Visibility 设置为 Hidden。这可以防止 ApplyTemplate、Measure 和 Arrange 在该项目的后代控件上发生,直到您准备好进行操作为止。
使物理滚动的 ScrollIntoView 更可靠的技巧是调用它两次:一次立即调用,一次在 DispatcherPriority.ApplicationIdle 的调度程序回调中调用。
使逻辑滚动滚动条更稳定
如果您的所有项目高度相同,则任何时候在视口中可见的项目数将保持不变,从而导致滚动拇指大小保持不变(因为与总项目数的比率不会改变)。
还可以修改 ScrollBar 自身的行为,以便始终计算拇指的固定大小。要在没有任何 hacky 代码的情况下执行此操作:
- 子类化 Track,以在 MeasureOverride 中使用自己的 Thumb 位置和大小替换其计算。
- 将用于逻辑滚动 ScrollBar 的 ScrollBar 模板更改为使用您的子类化 Track 而不是常规模板。
- 将 ScrollViewer 模板更改为在逻辑滚动 ScrollBar 上显式设置您的自定义 ScrollBar 模板(而不是使用默认模板)。
- 将 ListBox 模板更改为在它创建的 ScrollViewer 上显式设置您的自定义 ScrollViewer 模板。
这意味着复制大量内置 WPF 模板代码,因此这不是非常优雅的解决方案。但是,这样做的替代方法是使用 hacky 代码,在等待所有模板扩展之后,找到 ScrollBar 并将 ScrollBar 模板替换为使用您的自定义 Track 的模板。这段代码可以节省两个大型模板(ListBox、ScrollViewer),但代价是一些非常棘
Height
属性和控件上的限制))和滚动条。它存在的目的是让你可以绑定它,而不必使用值转换器来计算ActualHeight - (IsHorizontalScrollBarVisible ? HorizontalScrollbarHeight : 0)
。 - Markus HütterCanContentScroll
为false
的情况。例如,在ListView
中,ScrollViewer
的视口高度将始终是ScrollViewer
可以显示的项目数。 - bitbonkListView
。 - Markus Hütter