在WPF的ListView中实现平滑滚动是否可能?

50

是否可以在 WPF listview 中实现类似 Firefox 中平滑滚动的效果?
当 Firefox 浏览器包含了所有的 listview 项,并且你按住鼠标中键(但不释放),然后拖动它,它应该会平滑地滚动 listview 的项。当你释放它时,它应该停止。

看起来在 WinForms 中似乎不可能,但我想知道在 WPF 中是否可行?

5个回答

90
你可以实现平滑滚动,但会失去项虚拟化的功能,所以基本上只有在列表中元素很少的情况下才应该使用这种技术:
信息在这里: 列表框上的平滑滚动

你尝试设置了吗:

ScrollViewer.CanContentScroll="False"

在列表框上吗?

这样做可以通过面板来处理滚动,而不是列表框...不过如果你有大量内容的话,你会失去虚拟化,所以速度可能会变慢。


2
谢谢,Pop。什么是虚拟化? - Joan Venge
14
虚拟化是指位于VirtualizingStackPanel内(默认为ListBox等的ItemsPanel)的内容在项可见之前不会实际呈现布局。因此,对于众多或视觉复杂的项,它可以提供显着的性能提升,因为它每次仅会生成少部分项的UI。更多信息请参考:http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel.aspx - rmoore
1
这样做是从逻辑滚动切换到物理滚动(假设您正在使用实现IScrollInfo的StackPanel)。如果您仍然想要逻辑滚动但希望使其平滑,这并没有帮助。 - Jack Ukleja
感谢ScrollViewer.CanContentScroll="False"非常有用。 - ahmedsafan86

11

确实可以做到您所要求的,不过需要相当量的自定义代码。

通常,在WPF中,ScrollViewer使用的是所谓的逻辑滚动(Logical Scrolling),这意味着它会按项(item)而不是按偏移量(offset)进行滚动。其他答案涵盖了一些将逻辑滚动行为改变为物理滚动(Physical Scrolling)的方法。另一种方法是利用ScrollViewer和IScrollInfo两者公开的ScrollToVerticalOffset和ScrollToHorizontalOffset方法。

为了实现较大的部分,即按下鼠标滚轮时的滚动,我们需要利用MouseDown和MouseMove事件。

<ListView x:Name="uiListView"
          Mouse.MouseDown="OnListViewMouseDown"
          Mouse.MouseMove="OnListViewMouseMove"
          ScrollViewer.CanContentScroll="False">
    ....
</ListView>

在MouseDown事件中,我们将记录当前鼠标位置,作为相对点以确定滚动方向。在MouseMove事件中,我们将获取ListView的ScrollViewer组件,然后相应地滚动。

private Point myMousePlacementPoint;

private void OnListViewMouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.MiddleButton == MouseButtonState.Pressed)
    {
        myMousePlacementPoint = this.PointToScreen(Mouse.GetPosition(this));
    }
}

private void OnListViewMouseMove(object sender, MouseEventArgs e)
{
    ScrollViewer scrollViewer = ScrollHelper.GetScrollViewer(uiListView) as ScrollViewer;

    if (e.MiddleButton == MouseButtonState.Pressed)
    {
        var currentPoint = this.PointToScreen(Mouse.GetPosition(this));

        if (currentPoint.Y < myMousePlacementPoint.Y)
        {
            scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - 3);
        }
        else if (currentPoint.Y > myMousePlacementPoint.Y)
        {
            scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + 3);
        }

        if (currentPoint.X < myMousePlacementPoint.X)
        {
            scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset - 3);
        }
        else if (currentPoint.X > myMousePlacementPoint.X)
        {
            scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + 3);
        }
    }
}

public static DependencyObject GetScrollViewer(DependencyObject o)
{
    // Return the DependencyObject if it is a ScrollViewer
    if (o is ScrollViewer)
    { return o; }

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++)
    {
        var child = VisualTreeHelper.GetChild(o, i);

        var result = GetScrollViewer(child);
        if (result == null)
        {
            continue;
        }
        else
        {
            return result;
        }
    }
    return null;
}

虽然这只是一个概念验证,但它在某些方面还有所欠缺,但肯定可以帮助你朝着正确的方向开始。要使它一直滚动,直到鼠标移开初始MouseDown点,滚动逻辑可以放入DispatcherTimer或类似的东西。


9

我知道这篇文章已经13年了,但这仍然是人们想要做的事情。 在较新版本的.Net中,您可以设置VirtualizingPanel.ScrollUnit="Pixel" 这样您就不会失去虚拟化,并且您可以按像素而不是按项滚动。


5
谢谢,已测试并且在WPF .Net 7上运行正常! - VT Chiew

5
尝试在ListView上设置ScrollViewer.CanContentScroll附加属性为false。但是像Pop Catalin所说,您将失去项目虚拟化功能,这意味着列表中的所有项目一次性加载和填充,而不是在需要显示一组项目时进行加载 - 因此,如果列表很大,则可能会导致一些内存和性能问题。

谢谢 Eddie。是的,名字确实很奇怪。所以当启用该属性时,滚动条就可以工作了,对吗? - Joan Venge
2
看看这个相关的问题/答案 - 看起来你需要手动处理鼠标以达到你想要的效果,但这也会让你控制滚动的平滑程度/像素数量:https://dev59.com/1nNA5IYBdhLWcg3wVcFx - Eddie
1
这就是它的样子。CanContentScroll属性似乎是一个快速解决方案,但会有副作用(没有虚拟化)。但是抓取实际的ScrollViewer似乎可以给你更多的控制权 - 更多的代码,但可以让你做你想做的事情。它可能仍然存在虚拟化问题。我不确定 - 我很难看到如何自定义滚动仍然允许一次只加载所需的项目到内存中。这里有另一篇很棒的文章:https://dev59.com/wkfRa4cB1Zd3GeqP-qCu#973500 - Eddie
@Eddie:使用这些方法不会干扰虚拟化,测试的好方法是在顶部放置一个非常宽的项目,然后向下滚动,水平滚动条将消失。(或者如果方向翻转,您可以使垂直滚动条消失)@Joan Venge由于虚拟化的这种行为,如果预期项目具有不同的高度和宽度,则最好禁用虚拟化。 - rmoore
谢谢rmoore。项目大小相同,至少高度一定。宽度非常相似。 - Joan Venge
显示剩余4条评论

-1
尝试将ListView的高度设置为自动,并将其包装在滚动查看器中。
<ScrollViewer IsTabStop="True" VerticalScrollBarVisibility="Auto">
     <ListView></ListView>
</ScrollViewer>

不要忘记提及 ScrollViewer 的高度。 希望这可以帮到你。

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