带有ListBox的WPF ListBox - UI虚拟化和滚动

时间:2009-12-30 01:18:55

标签: wpf user-interface listbox scroll virtualization

我的原型显示包含“页面”的“文档” 由缩略图图像表示。每个文件都可以 任意数量的页面。例如,可能有 1000个文档,每个5页,或5个文档,1000页 每个,或介于两者之间。文件不包含其他文件。 在我的xaml标记中,我有一个ListBox,其ItemsTemplate 引用也有ListBox的innerItemsTemplate。我想要 2级选定的项目,以便我可以执行各种操作 在文档或页面上(删除,合并,移动到新位置等)。 innerItemsTemplate ListBox使用WrapPanel作为ItemsPanelTemplate

对于我有大量文档的场景 每个页面(例如,10000个文档,每个5页),滚动 由于VirtualizingStackPanel的UI虚拟化,效果很好。 但是,如果我有大量的页面,我会遇到问题。一个文件 1000个页面一次只显示大约50个(无论在屏幕上适合什么),当我向下滚动时,外部ListBox移动到下一个文档,跳过950 页面左右不可见。除此之外,没有 VirtualzingWrapPanel所以app内存确实增加了。

我想知道我是否正确地走这条路,特别是 因为它有点难以解释!我希望能够展示 10000个文档,每个1000页(仅显示适合屏幕的任何内容), 使用UI虚拟化,还可以平滑滚动。

如何确保滚动移动到文档中的所有页面 在它显示下一个文档之前,仍然保持UI虚拟化? 滚动条似乎只移动到下一个文档。

表示“文件”和“页面”似乎合乎逻辑 - 我目前在ListBox中使用ListBox的方法?

我非常感谢你的任何想法。 谢谢。

5 个答案:

答案 0 :(得分:42)

如果您准备使用反射来访问VirtualizingStackPanel的私有功能,则可以在WPF 4.0中实现平滑滚动VirtualizingStackPanel而不会牺牲虚拟化。您所要做的就是将VirtualizingStackPanel的私有IsPixelBased属性设置为true。

请注意,在.Net 4.5中,不需要这个黑客,因为你可以设置VirtualizingPanel.ScrollUnit =" Pixel"。

为了让它变得非常简单,请参考以下代码:

public static class PixelBasedScrollingBehavior 
{
    public static bool GetIsEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior), new UIPropertyMetadata(false, HandleIsEnabledChanged));

    private static void HandleIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var vsp = d as VirtualizingStackPanel;
        if (vsp == null)
        {
            return;
        }

        var property = typeof(VirtualizingStackPanel).GetProperty("IsPixelBased",
                                                                     BindingFlags.NonPublic | BindingFlags.Instance);

        if (property == null)
        {
            throw new InvalidOperationException("Pixel-based scrolling behaviour hack no longer works!");
        }

        if ((bool)e.NewValue == true)
        {
            property.SetValue(vsp, true, new object[0]);
        }
        else
        {
            property.SetValue(vsp, false, new object[0]);
        }
    }
}

例如,要在ListBox上使用它,您可以执行以下操作:

<ListBox>
   <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
         <VirtualizingStackPanel PixelBasedScrollingBehavior.IsEnabled="True">
          </VirtualizingStackPanel>
       </ItemsPanelTemplate>
   </ListBox.ItemsPanel>
</ListBox>

答案 1 :(得分:25)

这里的答案令人惊讶:

  • 如果您使用ItemsControlListBox,您将获得您遇到的行为,其中控件按“项目”滚动,以便您一次跳过整个文档,但
  • 如果您使用TreeView,控件将平滑滚动,以便您可以滚动浏览文档并进入下一个文档,但它仍然可以进行虚拟化。

我认为WPF团队选择此行为的原因是TreeView通常包含的项目大于可见区域,而ListBox通常不会。

在任何情况下,通过简单地修改TreeView,在WPF中使ListBox看起来像ItemsControlItemContainerStyle一样是微不足道的。这非常简单。您可以自己滚动,也可以只从系统主题文件中复制相应的模板。

所以你会有这样的事情:

<TreeView ItemsSource="{Binding documents}">
  <TreeView.ItemsPanel>
    <ItemsPanelTemplate>
      <VirtualizingStackPanel />
    </ItemsPanelTemplate>
  </TreeView.ItemsPanel>
  <TreeView.ItemContainerStyle>
    <Style TargetType="{x:Type TreeViewItem}">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type TreeViewItem}">
            <ContentPresenter /> <!-- put your desired container style here  with a ContentPresenter inside -->
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </TreeView.ItemContainerStyle>
  <TreeView.ItemTemplate>
    <DataTemplate TargetType="{x:Type my:Document}">
      <Border BorderThickness="2"> <!-- your document frame will be more complicated than this -->
        <ItemsControl ItemsSource="{Binding pages}">
          ...
        </ItemsControl>
      </Border>
    </DataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

让基于像素的滚动和ListBox风格的多选项协同工作

如果使用此技术来获取基于像素的滚动,则显示文档的外部ItemsControl不能是ListBox(因为ListBox不是TreeView或TreeViewItem的子类)。因此,您将失去所有ListBox的多选支持。据我所知,没有办法将这两个功能结合使用,而不包括一些功能的代码或其他功能。

如果您需要同一控件中的两组功能,基本上有几个选项:

  1. 在TreeViewItem的子类中自己实现多选。使用TreeViewItem而不是TreeView作为外部控件,因为它允许选择多个子项。在ItemsContainerStyle中的模板中:在ContentPresenter周围添加一个CheckBox,模板将CheckBox绑定到IsSelected,并使用控件模板为CheckBox设置样式以获得所需的外观。然后添加自己的鼠标事件处理程序来处理Ctrl-Click和Shift-Click以进行多选。

  2. 自己在VirtualizingPanel的子类中实现像素滚动虚拟化。这相对简单,因为VirtualizingStackPanel的大部分复杂性与非像素滚动和容器回收有关。 Dan Crevier's Blog对于了解VirtualizingPanel有一些有用的信息。

答案 2 :(得分:16)

.NET 4.5现在具有VirtualizingPanel.ScrollUnit="ScrollUnit"属性。我只是将我的一个TreeView转换为ListBox,性能明显更好。

此处提供更多信息:http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingpanel.scrollunit(v=vs.110).aspx

答案 3 :(得分:7)

这对我有用。似乎有几个简单的属性可以做到(.NET 4.5)

<ListBox            
    ItemsSource="{Binding MyItems}"
    VirtualizingStackPanel.IsVirtualizing="True"
    VirtualizingStackPanel.ScrollUnit="Pixel"/>

答案 4 :(得分:0)

请允许我在此答案前加上一个问题:用户是否必须始终查看列表中每个项目中的每个缩略图?

如果该问题的答案为“否”,那么限制内部项目模板中可见页面的数量可能是可行的(假设您已指示滚动效果很好,例如5页)和使用更大的单独“选定项目”模板并显示该文档的所有页面? Billy Hollis解释了如何在dnrtv episode 115

列表框中“弹出”所选项目