【问题标题】:Scroll WPF Listview to specific line将 WPF Listview 滚动到特定行
【发布时间】:2008-10-17 12:44:45
【问题描述】:

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 吗?很多事情都在那里解决了。
  • 是的,我运行了 3.5SP1,发现这不是一个错误。 ListViewItem 是虚拟化的,这没关系,但是我如何将它滚动到视图中呢?

标签: wpf listview scroll


【解决方案1】:

有人告诉我一种更好的滚动到特定行的方法,它既简单又像魅力一样。
简而言之:

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 forums中的较长版本:

【讨论】:

  • 对我来说唯一的问题是它无情地覆盖了用户可能已经做出的任何选择。不过,修改起来应该很容易,所以为您+1,先生,谢谢。
  • 哦,是的 - 对我来说,改变选择正是我想要的,但你是对的。
  • 遗憾的是,如果 ListBox 有 VirtualizingStackPanel.IsVirtualizing="True" =[,这不起作用。但是,接受的答案确实。如果.IsVirtualizing="False",这是默认值,这个答案可以正常工作。
  • 是的。确实像魅力一样工作!我在使用数据库的虚拟化列表视图方面遇到了很多困难。由于我启用了排序和搜索,我真的很难找到不会从数据库中加载所有项目的方法。我必须实现逻辑,它会告诉我在应用过滤器后,ListView 中的项目是什么索引,以及数据库中的项目实际上是哪一行。现在它可以工作了:)。不错!
【解决方案2】:

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

所以在这种情况下,您可能会为该项目获得null,是吗? (根据您的评论,您可以)

我找到了link on MSDN forums that suggest accessing the Scrollviewer directly 以便滚动。对我来说,那里提供的解决方案看起来很像 hack,但您可以自己决定。

这是来自link above的代码sn-p:

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);

【讨论】:

  • 尝试了代码,它可以工作。不好,但一个答案。我想清理我的问题以反映真正的问题,如果您想包含代码(链接可能会更改),我会将您标记为答案。
  • 既然它有效,我还是将你标记为答案 - 但我仍然认为如果将代码复制到这里会很好,以防链接发生变化。
【解决方案3】:

我对 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();
        }
    }

干杯

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

      感谢山姆的最后提示。我打开了一个对话框,这意味着每次对话框关闭时我的网格都会失去焦点。我用这个:

      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();
      }
      

      【讨论】:

        【解决方案6】:

        试试这个

        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();
        
        }
        

        【讨论】:

        • 这是一个完美的答案,因为您可以保持虚拟化并滚动到特定的行,而不必为原始选择而苦恼 - 即使对于可能具有不同项目高度的列表视图,如我的(可扩展项目) .谢谢!
        • 嗨什么是实体?
        【解决方案7】:

        如果您只想在创建新数据项后显示并聚焦最后一项,则此方法可能更好。与 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();
        }
        

        【讨论】:

        • 抱歉,第一个子句应该是:ObservableCollection items= lsv.ItemsSource as ObservableCollection;
        • 请编辑您的答案而不是对其发表评论。干杯,
        • ScrollToEnd 比 ScrollIntoView 效果更好,后者可能仅显示最后一个项目部分可见。感谢您分享此解决方案。
        • 嗨,如果 ListBox 有 VirtualizingStackPanel.IsVirtualizing="True",这不起作用
        【解决方案8】:

        我刚刚遇到了相同的问题,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

        空的&lt;ListBox.ItemsPanel&gt; 代码是关闭虚拟化的原因。为我解决问题的示例代码:

        数据模板:

        <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 的回答禁用了虚拟化!
        【解决方案9】:

        要克服虚拟化问题,但仍使用 ScrollIntoView 并且不要在 ListView 的内部乱窜,您还可以使用 ViewModel 对象来确定选择的内容。假设您的列表中有具有 IsSelected 属性的 ViewModel 对象。您可以像这样将项目链接到 XAML 中的 ListView:

        <ListView Name="PersonsListView" ItemsSource="{Binding PersonVMs}">
          <ListView.ItemContainerStyle>
            <Style TargetType="{x:Type ListViewItem}">
              <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            </Style>
          </ListView.ItemContainerStyle>
        </ListView>
        

        然后,代码隐藏方法可以滚动到第一个选定的项目:

        var firstSelected = PersonsListView.Items
            .OfType<TreeViewItemViewModel>().FirstOrDefault(x => x.IsSelected);
        if (firstSelected != null)
            CoObjectsListView.ScrollIntoView(firstSelected);
        

        如果所选项目完全不在视线范围内,这也有效。在我的实验中,PersonsListView.SelectedItem 属性是 null,但当然您的 ViewModel IsSelected 属性始终存在。请务必在所有绑定和加载完成后调用此方法(使用正确的DispatcherPriority)。

        使用ViewCommand 模式,您的 ViewModel 代码可能如下所示:

        PersonVMs.ForEach(vm => vm.IsSelected = false);
        PersonVMs.Add(newPersonVM);
        newPersonVM.IsSelected = true;
        ViewCommandManager.InvokeLoaded("ScrollToSelectedPerson");
        

        【讨论】:

          【解决方案10】:

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

          BooleanListView.ScrollIntoView(BooleanListView.SelectedItem);

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

          【讨论】:

            【解决方案11】:

            不确定这是否可行,但目前这对我使用 WPF、MVVM Light 和 .NET 3.5 有效

            我为 ListBox 添加了名为“lbPossibleError_SelectionChanged”的 SelectionChanged 事件

            然后在这个“lbPossibleError_SelectionChanged”事件后面,这里是代码

            为我工作。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-05-13
              • 1970-01-01
              • 2010-12-29
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-07-29
              • 1970-01-01
              相关资源
              最近更新 更多