实现虚拟化的WrapPanel

在开发媒体助手应用时,遇到了一些性能问题。在之前的文章《用ListBox替换TreeView》中,解释了如何通过模拟一个看起来像树形视图的ListBox来支持虚拟化,因为TreeView在大约有20,000个节点时性能会非常慢。在添加缩略图视图以显示库中的电影时,也遇到了类似的问题。使用了ListBox,并更改了ItemsPanel为WrapPanel。使用WrapPanel后,ListBox失去了虚拟化能力,导致在库中显示5000部电影需要很长时间,因为WrapPanel不支持虚拟化。在本文中,将解释是如何实现WrapPanel的虚拟化的。

缩略图视图ListBox与WrapPanel

将ListBox的ItemsPanel更改为WrapPanel,以显示缩略图视图。以下是XML代码示例:

<ListBox x:Name="thumbnailListBox" ItemsSource="{Binding DataSource.ResultMovies}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectedItem="{Binding DataSource.SelectedMovie}" ScrollViewer.IsDeferredScrollingEnabled="True" ItemTemplate="{StaticResource NormalThumbnailTemplate}" ScrollViewer.ScrollChanged="HandleScrollChanged"> <ListBox.ContextMenu> <ContextMenu> <MenuItem Header="播放" Command="{Binding DataSource.PlayMovieCommand}"/> <Separator/> <MenuItem Header="在Windows资源管理器中显示" Command="{Binding DataSource.ShowMovieInWindowsExplorerCommand}"/> </ContextMenu> </ListBox.ContextMenu> <ListBox.ItemsPanel> <ItemsPanelTemplate> <WrapPanel/> </ItemsPanelTemplate> </ListBox.ItemsPanel> </ListBox>

使用WrapPanel后,ListBox以缩略图视图显示电影,但不支持虚拟化。找不到任何解决方案来虚拟化WrapPanel。因此,调查了代码中哪部分最耗时。然后发现,由于使用了图像,每个缩略图项都需要一些时间来渲染,还使用了转换器等。尝试了一个简单的边框,没有任何内容的WrapPanel,它显示得非常快。因此,尝试只为可见区域的项目使用带有图像和其他信息的详细电影视图,所有其他不可见区域的项目使用简单的边框。不断在用户滚动时更改视图。

缩略图模板

以下是XML代码示例,它定义了一个DataTemplate,当项目可见时,会切换到缩略图视图:

<DataTemplate x:Key="NormalThumbnailTemplate"> <Border Width="300" Height="200" BorderThickness="1" Margin="5" BorderBrush="Gray"> <ContentControl x:Name="content" Content="{Binding}" ContentTemplate="{x:Null}"/> </Border> <DataTemplate.Triggers> <DataTrigger Binding="{Binding IsVisible}" Value="True"> <Setter TargetName="content" Value="{StaticResource NormalThumbnail}" Property="ContentTemplate"/> </DataTrigger> </DataTemplate.Triggers> </DataTemplate>

在电影数据中,使用了一个属性IsVisible,它在DataTrigger中用于切换到缩略图视图。NormalThumbnail是DataTemplate,它显示了带有图像和其他数据的详细电影信息。因此,本文不会解释这部分内容。

如何确定哪些项目在ListBox的可见区域内

以下是C#代码示例,它处理滚动事件,并使用ItemTemplate在普通边框和详细电影视图之间切换:

private void HandleScrollChanged(object sender, ScrollChangedEventArgs e) { ShowVisibleItems(sender); } private void ShowVisibleItems(object sender) { var scrollViewer = (FrameworkElement)sender; var visibleAreaEntered = false; var visibleAreaLeft = false; var invisibleItemDisplayed = 0; foreach (Movie item in thumbnailListBox.Items) { if (item.IsVisible) continue; var listBoxItem = (FrameworkElement)thumbnailListBox.ItemContainerGenerator.ContainerFromItem(item); if (!visibleAreaLeft && IsFullyOrPartiallyVisible(listBoxItem, scrollViewer)) { visibleAreaEntered = true; } else if (visibleAreaEntered) { visibleAreaLeft = true; } if (visibleAreaEntered) { if (visibleAreaLeft && ++invisibleItemDisplayed > 10) break; var job = new Job(MakeVisible); job.Store.Add(item); job.Start(); } } }

这段代码简单地找到那些完全或部分可见的项目,并将其标记为可见。使用了一个Job类,在不同的线程中标记项目为可见,只是为了延迟触发。但在大多数情况下,这是不必要的。所以,忽略这部分。

以下是C#代码示例,它确定项目是否在滚动视图器的可见区域内:

protected bool IsFullyOrPartiallyVisible(FrameworkElement child, FrameworkElement scrollViewer) { var childTransform = child.TransformToAncestor(scrollViewer); var childRectangle = childTransform.TransformBounds(new Rect(new Point(0, 0), child.RenderSize)); var ownerRectangle = new Rect(new Point(0, 0), scrollViewer.RenderSize); return ownerRectangle.IntersectsWith(childRectangle); }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485