在开发媒体助手应用时,遇到了一些性能问题。在之前的文章《用ListBox替换TreeView》中,解释了如何通过模拟一个看起来像树形视图的ListBox来支持虚拟化,因为TreeView在大约有20,000个节点时性能会非常慢。在添加缩略图视图以显示库中的电影时,也遇到了类似的问题。使用了ListBox,并更改了ItemsPanel为WrapPanel。使用WrapPanel后,ListBox失去了虚拟化能力,导致在库中显示5000部电影需要很长时间,因为WrapPanel不支持虚拟化。在本文中,将解释是如何实现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,它显示了带有图像和其他数据的详细电影信息。因此,本文不会解释这部分内容。
以下是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);
}