WPF数据表格:CanContentScroll属性引起奇怪的行为

14

我有一个解决方案,可以根据用户的条件生成一个DataGrid(或多个实例)。每个网格通过ObservableCollection不断地接收数据。

我的问题在于滚动条行为很奇怪。它很卡顿,而且在滚动时滚动条会自动调整大小。

然后我找到了CanContentScroll属性!它完全解决了奇怪的滚动行为,带给我临时的幸福和快乐。

但是,它会导致两个不幸的副作用:

  1. 每当我重新创建DataGrid实例并将其绑定到我的ObservableCollection时,整个窗口都会冻结5秒钟。当我的DataGrid增长到非常大的大小时,这种延迟可能会持续30秒。

  2. 当我调用TradeGrid.ScrollIntoView(TradeGrid.Items(TradeGrid.Items.Count - 1))来滚动到底部时,它会跳到底部,然后回到顶部。

是否有其他方法可以实现流畅的滚动?

2个回答

39

您正在遇到物理滚动和逻辑滚动之间的差异。

正如您发现的那样,它们各有利弊。

物理滚动

物理滚动(CanContentScroll=false)只按像素移动,因此:

  • 视口始终表示滚动范围的完全相同部分,为您提供流畅的滚动体验,并且

但是

  • 必须对DataGrid的整个内容应用所有模板,并测量和排列以确定滚动条的大小,从而导致加载时间过长和高RAM使用率,并且
  • 它实际上不会滚动项,因此对ScrollIntoView的理解不佳

逻辑滚动

逻辑滚动(CanContentScroll=true)通过项目而不是像素计算其滚动视口和范围,因此:

  • 视口在不同时间可能显示不同数量的项目,这意味着视口中的项目数与范围中的项目数不同,导致滚动条长度发生变化,并且

  • 滚动从一个项目移动到下一个项目,而不是在两个项目之间移动,导致“颠簸”的滚动

但是

  • 只要在底层使用VirtualizingStackPanel,它只需要应用模板并测量和排列当前实际可见的项即可,而且

  • 由于ScrollIntoView只需要将正确的项目索引放入视图中,因此它更简单

选择它们之间的差异

这是WPF提供的唯一两种滚动方式。您必须根据上述利弊来选择它们。通常,对于中到大型数据集,逻辑滚动最好,而对于小型数据集,物理滚动最好。

加快物理滚动加载速度的一个技巧是,将您的项包装在自定义的Decorator中,该Decorator具有固定的大小,并在不可见时将其子元素的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)。

使用不同的Panel 需要更大的工作量:VirtualizingStackPanel 是唯一支持虚拟化的 Panel,只有它和 StackPanel 支持逻辑滚动。由于你正在利用 VirtualizingStackPanel 的虚拟化能力,你需要重新实现所有这些功能以及所有的 IScrollInfo 函数和普通的 Panel 函数。我可以做类似的事情,但是我将花费几天甚至很多天来完善它。我建议你不要尝试它。


有没有什么我可以做的来使逻辑滚动中的滚动条大小更加稳定?也许使用不同的滚动容器等。非常感谢。 - Sonic Soul
1
你有三种选择来改进这个问题:调整你的项目高度,重新设计ListBox/ScrollViewer/ScrollBar以使用自定义Track控件,或者编写你自己的虚拟化面板。我已经在答案中详细介绍了这三种方法。 - Ray Burns
翻译:糟糕..我试图再次赞同您的答案,但那只是取消了我的初始赞同..如果您编辑您的答案,我将能够重新应用它 :) - Sonic Soul
有没有办法将物理滚动的处理至少从UI线程卸载出来?它会占用整个应用程序。 - Sonic Soul
1
是的,请查看“在它们之间进行选择”的第二段。这将实现类似虚拟化的加载(但没有回收),使您可以通过UI锁定。渐进式改进是将调度程序回调设置为SystemIdle优先级,触发第一个未加载的项目进行加载,然后在设置下一个回调后返回。只要每个单独的项目快速加载,这将为您提供敏捷的性能和看起来像是后台加载。 - Ray Burns

2

我也遇到了DataGrid的同样问题,最终我做了以下操作:

ScrollViewer.CanContentScroll="True"   
EnableRowVirtualization="True"
VirtualizingPanel.VirtualizationMode="Standard"

现在我的DataGrid一切正常工作。


我发现在不设置VirtualizingPanel.VirtualizationMode的情况下性能更好。将其设置为“Recycling”会导致滚动不流畅(强制滚动条物理滚动),而将其设置为“Standard”则会以某种方式导致滚动变慢,这很奇怪,因为我本以为默认值是Standard,但给它一个显式设置却使滚动延迟。不理解原因。 - KMC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接