将WPF Listview滚动到特定行

24

WPF,类似浏览器的应用程序。
我有一个包含ListView的页面。调用PageFunction后,我向ListView添加一行,并希望将新行滚动到视图中:

  ListViewItem item = ItemContainerGenerator.ContainerFromIndex(index) as ListViewItem;
  if (item != null)
    ScrollIntoView(item);

只要新行在视图中可见,它就能正常工作并获得焦点。

问题是,当该行不可见时,情况变得不同。如果该行不可见,将不会生成该行的ListViewItem,因此ItemContainerGenerator.ContainerFromIndex返回null。

但是如果没有这个项,如何将该行滚动到视图中?有没有办法在不需要ListViewItem的情况下滚动到最后一行(或任何地方)?


我已经使用了ListBox和DataGrid的ScrollIntoView(),它们没有出现这个问题。这是一个愚蠢的问题,但你是否在运行3.5 SP1?很多问题都在那里得到了解决。 - Bob King
是的,我在3.5SP1上运行,并发现这不是一个错误。 ListViewItem是虚拟化的,这是可以接受的,但是我该如何将其滚动到视图中呢? - Sam
11个回答

42

有人告诉我另一种更好的滚动到特定行的方法,它简单易用且效果显著。
简而言之:

public void ScrollToLastItem()
{
  lv.SelectedItem = lv.Items.GetItemAt(rows.Count - 1);
  lv.ScrollIntoView(lv.SelectedItem);
  ListViewItem item = lv.ItemContainerGenerator.ContainerFromItem(lv.SelectedItem) as ListViewItem;
  item.Focus();
}

MSDN论坛中的更长版本:


对我来说,唯一的问题是它无情地覆盖了用户可能已经做出的任何选择。不过,这应该是一个足够简单的修改,所以加一分给你,先生,谢谢。 - metao
哦,是的 - 对我来说,改变选择正是我想要的,但你是对的。 - Sam
1
遗憾的是,如果ListBox具有VirtualizingStackPanel.IsVirtualizing="True",则此方法无法正常工作 =[。然而,被接受的答案可以正常工作。如果.IsVirtualizing="False"(默认值),则此答案可以正常工作。 - newfurniturey
是的,确实像魅力一样工作!我在使用虚拟化列表视图和数据库时遇到了很多困难。由于启用了排序和搜索功能,我真的很难找到不会从数据库加载所有项目的方法。我不得不实现逻辑,告诉我应用过滤器后我的ListView中的项目将位于哪个索引位置,以及实际上该项目在数据库中的行号是多少。现在它可以正常工作了:)不错! - VinayChoudhary99

12

我认为这里的问题是如果行不可见,ListViewItem尚未创建。WPF按需创建Visible。

所以在这种情况下,您可能会得到null的项目,对吗?(根据您的评论,确实如此)

我在MSDN论坛上找到了一个链接,建议直接访问ScrollViewer以进行滚动。对我而言,这里呈现的解决方案非常像是一种hack,但您可以自己决定。

这是来自上述链接的代码片段:

VirtualizingStackPanel vsp =  
  (VirtualizingStackPanel)typeof(ItemsControl).InvokeMember("_itemsHost",
   BindingFlags.Instance | BindingFlags.GetField | BindingFlags.NonPublic, null, 
   _listView, null);

double scrollHeight = vsp.ScrollOwner.ScrollableHeight;

// itemIndex_ is index of the item which we want to show in the middle of the view
double offset = scrollHeight * itemIndex_ / _listView.Items.Count;

vsp.SetVerticalOffset(offset);

尝试了代码,它可以工作。虽然不太好看,但是是一个答案。如果您想包含代码(链接可能会更改),我想整理一下我的问题以反映真正的问题,我会将您标记为答案。 - Sam
既然它能工作,我还是将您标记为答案 - 但我仍然认为如果代码被复制到这里会更好,以防链接发生变化。 - Sam

5

我对Sam的答案进行了一些修改。请注意,我想要滚动到最后一行。不幸的是,ListView有时候只显示最后一行(即使上面有100行),所以我解决了这个问题:

    public void ScrollToLastItem()
    {
        if (_mainViewModel.DisplayedList.Count > 0)
        {
            var listView = myListView;
            listView.SelectedItem = listView.Items.GetItemAt(_mainViewModel.DisplayedList.Count - 1);
            listView.ScrollIntoView(listView.Items[0]);
            listView.ScrollIntoView(listView.SelectedItem);
            //item.Focus();
        }
    }

干杯

3

解决这个问题的一个方法是更改ListView的ItemsPanel。默认面板是VirtualizingStackPanel,它只在第一次可见时创建ListBoxItem。如果您的列表中没有太多项,这应该不是问题。

<ListView>
   ...
   <ListView.ItemsPanel>
      <ItemsPanelTemplate>
         <StackPanel/>
      </ItemsPanelTemplate>
   </ListView.ItemsPanel>
</ListView>

2
如果您只想在创建新数据项后显示和关注最后一项,则此方法可能更好。与ScrollIntoView相比,ScrollViewer的ScrollToEnd在我的测试中更可靠。在使用ListView的ScrollIntoView方法进行上述测试时,有些测试失败了,我不知道原因。但是使用ScrollViewer滚动到最后一个可以起作用。
void FocusLastOne(ListView lsv)
{
   ObservableCollection<object> items= sender as ObservableCollection<object>;

   Decorator d = VisualTreeHelper.GetChild(lsv, 0) as Decorator;
   ScrollViewer v = d.Child as ScrollViewer;
   v.ScrollToEnd();

   lsv.SelectedItem = lsv.Items.GetItemAt(items.Count - 1);
   ListViewItem lvi = lsv.ItemContainerGenerator.ContainerFromIndex(items.Count - 1) as ListViewItem;
   lvi.Focus();
}

2
对不起,第一个条件应该是: ObservableCollection<object> items = lsv.ItemsSource as ObservableCollection<object>; - Willi
请编辑您的答案,而不是在评论中回复。谢谢。 - Anders R. Bystrup
ScrollToEnd比ScrollIntoView更好用,后者可能会将最后一个项目仅显示部分可见。感谢分享这个解决方案。 - Califf
嗨,如果ListBox具有VirtualizingStackPanel.IsVirtualizing="True",这将无法正常工作。 - luka

2

试试这个

private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            ScrollViewer scrollViewer = GetScrollViewer(lstVw) as ScrollViewer;
            scrollViewer.ScrollToHorizontalOffset(dataRowToFocus.RowIndex);
            if (dataRowToFocus.RowIndex < 2)
                lstVw.ScrollIntoView((Entity)lstVw.Items[0]);
            else
                lstVw.ScrollIntoView(e.AddedItems[0]);
        } 

 public static DependencyObject GetScrollViewer(DependencyObject o)
        {
            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;
        } 

private void Focus()
{
 lstVw.SelectedIndex = dataRowToFocus.RowIndex;
 lstVw.SelectedItem = (Entity)dataRowToFocus.Row;

 ListViewItem lvi = (ListViewItem)lstVw.ItemContainerGenerator.ContainerFromItem(lstVw.SelectedItem);
ContentPresenter contentPresenter = FindVisualChild<ContentPresenter>(lvi);
contentPresenter.Focus();
contentPresenter.BringIntoView();

}

这是完美的答案,因为您可以保持虚拟化并滚动到特定行,而不必为原始选择而苦恼 - 即使对于可能具有不同项高度(如可展开项)的列表视图也是如此。谢谢! - lunatix
嗨,什么是实体? - luka

2

感谢你上次给的提示,Sam。我有一个对话框会打开,每次关闭时网格就失去焦点。我使用了以下代码:

if(currentRow >= 0 && currentRow < lstGrid.Items.Count) {
    lstGrid.SelectedIndex = currentRow;
    lstGrid.ScrollIntoView(lstGrid.SelectedItem);
    if(shouldFocusGrid) {
        ListViewItem item = lstGrid.ItemContainerGenerator.ContainerFromItem(lstGrid.SelectedItem) as ListViewItem;
        item.Focus();
    }
} else if(shouldFocusGrid) {
    lstGrid.Focus();
}

1

我刚遇到了与ItemContainerGenerator.ContainerFromItem()和ItemContainerGenerator.ContainerFromIndex()返回null的问题,尽管这些项在列表框中明显存在。Decasteljau是正确的,但我不得不进行一些挖掘才能弄清楚他的意思。以下是详细说明,以便为下一个人节省一些功夫。

长话短说,如果ListBoxItems不在视图内,它们将被销毁。因此,ContainerFromItem()和ContainerFromIndex()返回null,因为ListBoxItems不存在。这显然是一种内存/性能节省功能,详细信息请参见:http://blogs.msdn.com/b/oren/archive/2010/11/08/wp7-silverlight-perf-demo-1-virtualizingstackpanel-vs-stackpanel-as-a-listbox-itemspanel.aspx

空的<ListBox.ItemsPanel>代码是关闭虚拟化的方法。以下是为我解决问题的示例代码:

数据模板:

<phone:PhoneApplicationPage.Resources>
    <DataTemplate x:Key="StoryViewModelTemplate">
        <StackPanel>
          <your datatemplated stuff here/>
        </StackPanel>
    </DataTemplate>
</phone:PhoneApplicationPage.Resources>

主体内容:

<Grid x:Name="ContentPanel">
    <ListBox Name="lbResults" ItemsSource="{Binding SearchResults}" ItemTemplate="{StaticResource StoryViewModelTemplate}">
        <ListBox.ItemsPanel>
             <ItemsPanelTemplate>
                 <StackPanel>
                 </StackPanel>
             </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
</Grid>

请注意,Ray的答案禁用了虚拟化! - user1458416

0
在我的项目中,我需要向用户显示从ListView选择的索引行,因此我将所选项分配给ListView控件。这段代码将滚动滚动条并显示所选的项。

BooleanListView.ScrollIntoView(BooleanListView.SelectedItem);

或者

var listView = BooleanListView; listView.SelectedItem = listView.Items.GetItemAt(BooleanListView.SelectedIndex); listView.ScrollIntoView(listView.Items[0]); listView.ScrollIntoView(listView.SelectedItem);


0

不确定这是否是正确的方法,但我目前在使用WPF、MVVM Light和.NET 3.5时可以工作。

我为ListBox添加了SelectionChanged事件,称为“lbPossibleError_SelectionChangedI added the SelectionChanged event for ListBox

然后在这个“lbPossibleError_SelectionChanged”事件后面,这是代码 enter image description here

对我来说正常工作。


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