WPF ListBox with a ListBox - UI 虚拟化和滚动

30
我的原型显示包含“页面”的“文档”,而这些页面则用缩略图表示。每个文档可以有任意数量的页面。例如,可能有1000个文档,每个文档有5页,或者有5个文档,每个文档有1000页,或者介于两者之间。文档不包含其他文档。在我的xaml标记中,我有一个ListBox,它的ItemsTemplate引用了一个也有ListBox的innerItemsTemplate。我想要获得2级所选项,以便我可以对文档或页面执行各种操作(删除、合并、移动到新位置等)。内部ListBox使用一个WrapPanel作为ItemsPanelTemplate
对于我有大量有少数页面的文档的情况(例如,10000个文档每个文档有5页),由于VirtualizingStackPanel的UI虚拟化,滚动效果非常好。但是,如果页面很多,就会出现问题。具有1000页的文档一次只能显示大约50页(适合屏幕的任何内容),当我向下滚动时,外部的ListBox移到下一个文档,跳过了未显示的950页左右的页面。除此之外,没有VirtualzingWrapPanel,所以应用程序的内存会大幅增加。
我想知道我是否走了正确的路。我希望能够使用UI虚拟化和平滑滚动,在屏幕上仅显示适合的内容,从而显示10000个文档,每个文档有1000页。如何确保滚动在显示下一个文档之前经过文档中的所有页面,并仍然保持UI虚拟化?滚动条似乎只能移到下一个文档。使用我的当前方法(在ListBox内部使用一个ListBox)来表示“文档”和“页面”是否合乎逻辑?如果您有任何想法,我将不胜感激。谢谢!
5个回答

56

如果你愿意使用反射来访问VirtualizingStackPanel的私有功能,并将VirtualizingStackPanel的私有属性IsPixelBased设置为true,那么在WPF 4.0中实现平滑滚动的VirtualizingStackPanels就不需要放弃虚拟化。

请注意,在.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>

27
非常有帮助,点赞。对于使用 .NET 4.5 的未来访问者,您需要在 ListBox 上设置 "VirtualizingPanel.ScrollUnit="Pixel"",而不是在容纳内容的 VirtualizingStackPanel 上设置。 - Seth Carnegie
@ViktorLaCroix:你的电脑上安装了.NET 4.5吗?因为它是.NET 4.0的就地更新,我认为它会破坏这个黑客技巧。 - Samuel Jack
是的...我已经安装了.NET4.5,但程序是针对.NET4.0开发的。我自己制作了一个自定义控件来解决这个问题。不过还是谢谢你。 - Kapitán Mlíko
这会产生奇怪的副作用。首先,在加载几千个项目并滚动到末尾后,所有项目都不可见,直到列表再次更改或我向上滚动一点。其次,滚动部分地移动视图整个项目高度和部分分数,因此非常不一致。总的来说,这对我不起作用。(Win7,VS2010,.NET 4.0) - ygoe
不知怎么的,我不小心给这个回答点了个踩,现在我无法撤销它。好的回答,谢谢你提供这个信息。 - Eric Scherrer

27

这里的答案有些出人意料:

  • 如果您使用ItemsControlListBox,您将会遇到您正在经历的行为,即控件按“项目”滚动,因此您一次跳过整个文档,但是
  • 如果您改用TreeView,则该控件将平稳地滚动,因此您可以滚动文档并进入下一个文档,但它仍然能够虚拟化。

我认为WPF团队选择这种行为的原因是TreeView通常具有比可见区域更大的项,而通常ListBox没有。

无论如何,在WPF中使TreeView看起来和像ListBoxItemsControl一样行事非常容易,只需简单修改ItemContainerStyle即可。 这非常简单。 您可以自己编写模板,也可以从系统主题文件中复制适当的模板。

因此,您将会得到类似于以下内容:

<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的博客 中有一些有用的信息,可帮助您理解VirtualizingPanel。


这种方法对于UI虚拟化确实对我有用。现在我只需要获得类似于ListBox的选择项目(在此情况下为页面或文档)的行为。我如何获得类似于ListBox的多选和扩展选择模式? - Rob Buhler
另外,我正在将ItemsPanelTemplate设置为WrapPanel,并将其放置在ItemsControl中,但当我调整应用程序大小时,它似乎并没有自动换行,而是更像StackPanel。总体来说,我觉得Ray上面的答案确实让我朝着正确的方向前进了一步。 - Rob Buhler
我很高兴偶然发现了这篇文章,它让我避免了把所有头发都拔光的痛苦。 - Bijington
哇!你开启了我的眼界!非常感谢你的回答! - Mikolaytis

21

14

这对我行得通。好像只需要几个简单的属性就可以了(.NET 4.5)

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

VirtualizingStackPanel 没有这些属性。您是不是想说 VirtualizingPanel - Herohtar
距离我的回复已经三年了,但我很抱歉我不确定。VS没有对VirtualizingStackPanel提出任何抱怨——但是当我将鼠标悬停在其上时,提示显示的是VirtualizingPanel而不是前者。我的猜测是前者是后者的一个备用别名。 - rmirabelle
2
VirtualizingStackPanelVirtualizingPanel 派生而来。 IsVirtualizingPropertyScrollUnitProperty 在基类上被定义。你可以通过子类型引用它们,但也许更清晰的方法是直接从声明属性的类型引用它们。 - Drew Noakes

0

请允许我以一个问题开头回答:用户是否必须始终看到列表中每个项目中的每个缩略图?

如果答案是“否”,那么或许可以通过限制内部项模板中可见页面的数量(假设您已经表明滚动功能与5页一起很好用),并使用一个单独的“选定项目”模板,该模板更大,显示该文档的所有页面。Billy Hollis解释了如何在dnrtv上的列表框中“弹出”所选项目 episode 115


用户不必始终查看列表中每个项目中的每个缩略图 - 他们只需要滚动即可访问其他项目。 - Rob Buhler

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