如何在WPF数据网格上实现自动滚动

41

我觉得我很蠢。我花了15分钟搜索,找到了几种不同的数据网格滚动解决方案,但似乎都不适用于我。

我正在使用带有.NET 3.5和WPF Toolkit DataGrid的WPF。我的网格在可观察集合更改时更新,运行得非常完美。现在,我的DataGrid位于一个普通的Grid内,如果DataGrid变得太大,就会出现滚动条。这也没问题...

现在来个百万美元的问题:

如何使datagrid滚动到最后一行? 这里:

  • 没有AutoScroll属性
  • 没有CurrentRowSelected索引
  • 有一个CurrentCell,但我找不到一个可用于CurrentCell = AllCells.Last的集合

有什么想法吗?我感觉真的很笨,这个问题看起来很奇怪。我错过了些什么?


2
WPF就像一辆没有轮子的汽车,它只有发动机和底盘。 - huang
17个回答

56

你应该使用 datagrid 方法

datagrid.ScrollIntoView(itemInRow);
或者
datagrid.ScrollIntoView(itemInRow, column);

这种方法不需要费力地寻找滚动查看器等。


3
如果您对这个答案有疑问,您应该提供详细信息说明为什么它不起作用,或者您的情况可能不同,需要提出另一个问题。也许自6年前回答此问题以来底层框架发生了一些变化。有时当网格被虚拟化时,我会遇到滚动的问题。从点赞数量来看,很明显这对很多人有效,所以也许问题出在您的代码而不是这个答案上。 - Aran Mulholland

54

;)

if (mainDataGrid.Items.Count > 0)
{
    var border = VisualTreeHelper.GetChild(mainDataGrid, 0) as Decorator;
    if (border != null)
    {
        var scroll = border.Child as ScrollViewer;
        if (scroll != null) scroll.ScrollToEnd();
    }
}

1
非常感谢,如果生活总是那么简单就好了 :-) - Christian Ruppert
1
优秀的代码,将其包装在ArgumentOutOfRange异常中,它就会完美无缺,以防列表框可能为空。 - wonea
我也想知道这段代码放在哪里(钩入了哪个事件)?我已经尝试过SizeChanged和LayoutUpdated等事件。最好的结果是我的DataGrid只能滚动到一半。我已经尝试过使用装饰器/ScrollToEnd版本和ScrollIntoView版本。 - e-holder
谢谢,这对我有用...从你的回答中看来,你是所有技术的大师 :) 太棒了 - C Sharper
5
使用 ((INotifyCollectionChanged)MyDataGrid.Items).CollectionChanged += Your_Event_Handler; 进行挂钩。 - maxp
C#7用户请注意,您可以使用模式匹配来实现此功能,例如,您可以将整个语句压缩为:if (VisualTreeHelper.GetChild(mainDataGrid, 0) is Decorator border),而不是使用边框声明和检查。 - Lauraducky

23

我为网格自动滚动编写了一个附加属性:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;

public static class DataGridBehavior
{
    public static readonly DependencyProperty AutoscrollProperty = DependencyProperty.RegisterAttached(
        "Autoscroll", typeof(bool), typeof(DataGridBehavior), new PropertyMetadata(default(bool), AutoscrollChangedCallback));

    private static readonly Dictionary<DataGrid, NotifyCollectionChangedEventHandler> handlersDict = new Dictionary<DataGrid, NotifyCollectionChangedEventHandler>();

    private static void AutoscrollChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
    {
        var dataGrid = dependencyObject as DataGrid;
        if (dataGrid == null)
        {
            throw new InvalidOperationException("Dependency object is not DataGrid.");
        }

        if ((bool)args.NewValue)
        {
            Subscribe(dataGrid);
            dataGrid.Unloaded += DataGridOnUnloaded;
            dataGrid.Loaded += DataGridOnLoaded;
        }
        else
        {
            Unsubscribe(dataGrid);
            dataGrid.Unloaded -= DataGridOnUnloaded;
            dataGrid.Loaded -= DataGridOnLoaded;
        }
    }

    private static void Subscribe(DataGrid dataGrid)
    {
        var handler = new NotifyCollectionChangedEventHandler((sender, eventArgs) => ScrollToEnd(dataGrid));
        handlersDict.Add(dataGrid, handler);
        ((INotifyCollectionChanged)dataGrid.Items).CollectionChanged += handler;
        ScrollToEnd(dataGrid);
    }

    private static void Unsubscribe(DataGrid dataGrid)
    {
        NotifyCollectionChangedEventHandler handler;
        handlersDict.TryGetValue(dataGrid, out handler);
        if (handler == null)
        {
            return;
        }
        ((INotifyCollectionChanged)dataGrid.Items).CollectionChanged -= handler;
        handlersDict.Remove(dataGrid);
    }

    private static void DataGridOnLoaded(object sender, RoutedEventArgs routedEventArgs)
    {
        var dataGrid = (DataGrid)sender;
        if (GetAutoscroll(dataGrid))
        {
            Subscribe(dataGrid);
        }
    }

    private static void DataGridOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
    {
        var dataGrid = (DataGrid)sender;
        if (GetAutoscroll(dataGrid))
        {
            Unsubscribe(dataGrid);
        }
    }

    private static void ScrollToEnd(DataGrid datagrid)
    {
        if (datagrid.Items.Count == 0)
        {
            return;
        }
        datagrid.ScrollIntoView(datagrid.Items[datagrid.Items.Count - 1]);
    }

    public static void SetAutoscroll(DependencyObject element, bool value)
    {
        element.SetValue(AutoscrollProperty, value);
    }

    public static bool GetAutoscroll(DependencyObject element)
    {
        return (bool)element.GetValue(AutoscrollProperty);
    }
}

使用方法:

    <DataGrid c:DataGridBehavior.Autoscroll="{Binding AutoScroll}"/>

4
我认为访客需要对这段代码进行更多的解释...! - kamesh
非常非常好。谢谢你。 - Chris Mantle
1
我不得不对此进行了一些微小的修改,以便它绑定到监视整数属性的更改,而不是布尔值。很高兴报告它在修改后仍然有效。 - Krondorian
一直在尝试让DataGrid在遵循MVVM的同时自动滚动。我在我的视图模型中创建了AutoScroll属性,然后将复选框IsChecked绑定到AutoScroll属性,这样做非常有效。 - Moon Waxing
我不得不修改Subscribe()方法以确保字典中没有重复的键。除此之外,一切正常。 - SnowGroomer
显示剩余3条评论

8

要实现自动滚动到最后一个添加的元素:

YourDataGrid.ScrollIntoView(YourDataGrid.Items.GetItemAt(YourDataGrid.Items.Count-1));

希望这能有所帮助 :)

1
YourDataGrid.Items.Last() 可以让代码更简洁。 - Oskar
1
@Anas,“Items”没有“Last()”方法。 - marsh-wiggle

6
listbox.Add(foo);
listbox.SelectedIndex = count - 1;
listbox.ScrollIntoView(listbox.SelectedItem);
listbox.SelectedIndex = -1;

这对于将选择滚动到屏幕的“中间”非常有用(需要进行一些逻辑检查和更改)。 - DJ van Wyk

6

我知道这个回答有点晚了,但是对于那些正在搜索的人来说,我找到了最简单的方法来滚动到DataGrid的底部。在DataContextChanged事件中加入以下内容:

myDataGrid.ScrollIntoView(CollectionView.NewItemPlaceholder);

很容易吧?

这是为什么它可以工作的原因:在每个数据网格下方都有一个位置,您可以添加到与其绑定的列表中的新项目。那就是CollectionView.NewItemPlaceholder,在您的数据网格中只会有一个。所以你可以直接滚动到那里。


4
数据上下文只有在向视图添加新的视图模型时才会更改,因此这将仅在第一次滚动到底部,而不是每次向项源添加项目。 - Ignacio Soler Garcia
好的,它不会,但是数据上下文只是一个例子,在我的应用程序中它非常好用... - James Esh
2
另外,这不是问题中提到的。 - James Esh
OP正在询问如何将自动滚动功能添加到DataGrid控件,而不是如何向下滚动一次。 - Ignacio Soler Garcia
就像我说的那样,那只是一个例子,你可以在任何地方调用它。请移除-1。谢谢。 - James Esh

4
我发现最简单的方法是从ScrollViewer.ScrollChanged附加事件中调用ScrollIntoView方法。可以在XAML中设置如下:
<DataGrid
...
ScrollViewer.ScrollChanged="control_ScrollChanged">

ScrollChangedEventArgs对象具有各种属性,可帮助计算布局和滚动位置(Extent、Offset、Viewport)。请注意,在使用默认DataGrid虚拟化设置时,这些通常以行/列的数量来衡量。

以下是一个示例实现,它在向DataGrid添加新项时将底部项保持在视图中,除非用户移动滚动条以查看网格中更高的项。

    private void control_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        // If the entire contents fit on the screen, ignore this event
        if (e.ExtentHeight < e.ViewportHeight)
            return;

        // If no items are available to display, ignore this event
        if (this.Items.Count <= 0)
            return;

        // If the ExtentHeight and ViewportHeight haven't changed, ignore this event
        if (e.ExtentHeightChange == 0.0 && e.ViewportHeightChange == 0.0)
            return;

        // If we were close to the bottom when a new item appeared,
        // scroll the new item into view.  We pick a threshold of 5
        // items since issues were seen when resizing the window with
        // smaller threshold values.
        var oldExtentHeight = e.ExtentHeight - e.ExtentHeightChange;
        var oldVerticalOffset = e.VerticalOffset - e.VerticalChange;
        var oldViewportHeight = e.ViewportHeight - e.ViewportHeightChange;
        if (oldVerticalOffset + oldViewportHeight + 5 >= oldExtentHeight)
            this.ScrollIntoView(this.Items[this.Items.Count - 1]);
    }

3
如果大数据 datagrid.ScrollIntoView(itemInRow, column); 不起作用,那么我们只能使用下面的方法:
if (mainDataGrid.Items.Count > 0) 
        { 
            var border = VisualTreeHelper.GetChild(mainDataGrid, 0) as Decorator; 
            if (border != null) 
            { 
                var scroll = border.Child as ScrollViewer; 
                if (scroll != null) scroll.ScrollToEnd(); 
            } 
        } 

1

实际上...

当我在学习关于WPF中的Collection Views和DataContext时,我也遇到了同样的问题。

我也面临着一个任务,需要编写一个WPF程序,通过按钮编程地在DataGrid上下移动,因为我需要将它放在仅供生产建筑师使用的电阻式触摸屏上,他们没有鼠标或键盘可以使用。

但是,正如此帖子中先前提到的那样,使用ScrollIntoView方法,这个例子对我有效。

    private void OnMoveUp(object sender, RoutedEventArgs e)
    {
        ICollectionView myCollectView = CollectionViewSource.GetDefaultView(Orders);
        if (myCollectView.CurrentPosition > 0)
            myCollectView.MoveCurrentToPrevious();

        if (myCollectView.CurrentItem != null)
            theDataGrid.ScrollIntoView(myCollectView.CurrentItem);
    }

    private void OnMoveDown(object sender, RoutedEventArgs e)
    {
        ICollectionView  myCollectView = CollectionViewSource.GetDefaultView(Orders);
        if (myCollectView.CurrentPosition < Orders.Count)
            myCollectView.MoveCurrentToNext();

        if (myCollectView.CurrentItem !=null)
            theDataGrid.ScrollIntoView(myCollectView.CurrentItem);
    }

其中 Orders 是一个 List<T> 集合

XAML 中:

    <StackPanel Grid.Row="1"
        Orientation="Horizontal">
            <Button Click="OnMoveUp">
                <Image Source="Up.jpg" />
            </Button>
            <Button Click="OnMoveDown">
                <Image Source="Down.jpg" />
              </Button>
    </StackPanel>

    <DataGrid Grid.Row="2"
              x:Name="theDataGrid"
              ItemSource="{Binding Orders}"
              ScrollViewer.CanContentScroll="True"
              ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0,0,0,5">

    << code >>


    </DataGrid>

请遵循之前的建议,将DataGrid单独放置,而不是在StackPanel中。对于DataGrid的行定义(在此情况下为第三行),我将高度设置为150,滚动条可以正常工作。

1
这是另一个优秀的解决方案。
public sealed class CustomDataGrid : DataGrid
{
    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        base.OnItemsSourceChanged(oldValue, newValue);
    }
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);
        if (this.Items.Count > 0) this.ScrollIntoView(this.Items[this.Items.Count - 1]);
    }
}

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