我以前使用后面的代码手动将项目添加到我的ListBox,但它非常慢。我听说在性能方面,通过XAML进行数据绑定是最佳选择。
所以我设法让数据绑定工作(新的绑定),但令我沮丧的是,性能并不比我以前的非数据绑定方法好。
我的想法是我的ListBox包含一个名称在它下面的Image。我做了一些基准测试,54个项目需要整整8秒才能显示。对于用户来说,这自然太长了。
源图像的最大值为2100x1535px,每个文件的范围为400kb> 4mb。
重现此问题所需的图像可在此处找到:已删除链接,因为问题已得到解答,我的服务器没有很大的带宽限额。其他图片来源:https://imgur.com/a/jmbv6
我已经在下面提出了一个可重现的问题示例。我做错了什么让这么慢?
谢谢。
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800" WindowState="Maximized">
<Grid>
<ListBox x:Name="listBoxItems" ItemsSource="{Binding ItemsCollection}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<VirtualizingStackPanel>
<Image Width="278" Height="178">
<Image.Source>
<BitmapImage DecodePixelWidth="278" UriSource="{Binding ImagePath}" CreateOptions="IgnoreColorProfile" />
</Image.Source>
</Image>
<TextBlock Text="{Binding Name}" FontSize="16" VerticalAlignment="Bottom" HorizontalAlignment="Center" />
</VirtualizingStackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
背后的代码:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Threading;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
internal class Item : INotifyPropertyChanged
{
public Item(string name = null)
{
this.Name = name;
}
public string Name { get; set; }
public string ImagePath { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ObservableCollection<Item> ItemsCollection;
List<Item> data;
public MainWindow()
{
InitializeComponent();
this.data = new List<Item>();
this.ItemsCollection = new ObservableCollection<Item>();
this.listBoxItems.ItemsSource = this.ItemsCollection;
for (int i = 0; i < 49; i ++)
{
Item newItem = new Item
{
ImagePath = String.Format(@"Images/{0}.jpg", i + 1),
Name = "Item: " + i
};
this.data.Add(newItem);
}
foreach (var item in this.data.Select((value, i) => new { i, value }))
{
Dispatcher.Invoke(new Action(() =>
{
this.ItemsCollection.Add(item.value);
}), DispatcherPriority.Background);
}
}
}
}
答案 0 :(得分:1)
现在我能够看到您正在使用的图像,我可以确认这里的主要问题只是加载大图像的基本成本。使用这些图像文件时,根本没有办法改进。
您可以做的是异步加载图像,以便至少程序的其余部分在用户等待加载所有图像时响应,或者减小图像的大小,以便加载快点。如果可能的话,我强烈推荐后者。
如果出于某种原因需要以原始的大尺寸格式部署和加载图像,那么至少应该异步加载它们。有很多不同的方法可以实现这一目标。
最简单的方法是在Binding.IsAsync
绑定上设置Image.Source
:
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Width="278" Height="178" Source="{Binding ImagePath, IsAsync=True}"/>
<TextBlock Text="{Binding Name}" FontSize="16"
VerticalAlignment="Bottom" HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
这种方法的主要缺点是,在使用此方法时,您无法设置DecoderPixelWidth
。 Image
控件正在处理从路径到实际位图的转换,并且没有设置各种选项的机制。
鉴于该技术的简单性,我认为这是首选方法,至少对我而言。只要程序响应并显示进度迹象,用户通常不会关心完全初始化所有数据的总时间。但是,我注意到在这种情况下没有设置DecoderPixelWidth
,加载所有图像所花费的时间几乎是两倍(大约7.5秒,而差不多14秒)。所以你可能有兴趣自己异步加载图像。
这样做需要正常的异步编程技术,您可能已经熟悉了这些技术。主要&#34;陷阱&#34;默认情况下,WPF位图处理类将推迟实际加载位图,直到实际需要它为止。除非您可以强制立即加载数据,否则异步创建位图并不会有所帮助。
幸运的是,你可以。只需将CacheOption
属性设置为BitmapCacheOption.OnLoad
即可。
我冒昧地清理原始示例,创建正确的视图模型数据结构,并实现图像的异步加载。通过这种方式,我获得了8秒以下的加载时间,但是在加载过程中UI仍然保持响应。我包含了几个计时器:一个显示自程序启动以来经过的时间,主要用于说明UI的响应性,另一个用于显示实际加载位图图像所花费的时间。
<强> XAML:强>
<Window x:Class="TestSO42639506PopulateListBoxImages.MainWindow"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO42639506PopulateListBoxImages"
mc:Ignorable="d"
WindowState="Maximized"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<TextBlock Text="{Binding TotalSeconds, StringFormat=Total seconds: {0:0}}"/>
<TextBlock Text="{Binding LoadSeconds, StringFormat=Load seconds: {0:0.000}}"/>
</StackPanel>
<ListBox x:Name="listBoxItems" ItemsSource="{Binding Data}"
Grid.Row="1"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Width="278" Height="178" Source="{Binding Bitmap}"/>
<TextBlock Text="{Binding Name}" FontSize="16"
VerticalAlignment="Bottom" HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
<强> C#:强>
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdatePropertyField<T>(
ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return;
}
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class Item : NotifyPropertyChangedBase
{
private string _name;
private string _imagePath;
private BitmapSource _bitmap;
public string Name
{
get { return _name; }
set { _UpdatePropertyField(ref _name, value); }
}
public string ImagePath
{
get { return _imagePath; }
set { _UpdatePropertyField(ref _imagePath, value); }
}
public BitmapSource Bitmap
{
get { return _bitmap; }
set { _UpdatePropertyField(ref _bitmap, value); }
}
}
class MainWindowModel : NotifyPropertyChangedBase
{
public MainWindowModel()
{
_RunTimer();
}
private async void _RunTimer()
{
Stopwatch sw = Stopwatch.StartNew();
while (true)
{
await Task.Delay(1000);
TotalSeconds = sw.Elapsed.TotalSeconds;
}
}
private ObservableCollection<Item> _data = new ObservableCollection<Item>();
public ObservableCollection<Item> Data
{
get { return _data; }
}
private double _totalSeconds;
public double TotalSeconds
{
get { return _totalSeconds; }
set { _UpdatePropertyField(ref _totalSeconds, value); }
}
private double _loadSeconds;
public double LoadSeconds
{
get { return _loadSeconds; }
set { _UpdatePropertyField(ref _loadSeconds, value); }
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
partial class MainWindow : Window
{
private readonly MainWindowModel _model = new MainWindowModel();
public MainWindow()
{
DataContext = _model;
InitializeComponent();
_LoadItems();
}
private async void _LoadItems()
{
foreach (Item item in _GetItems())
{
_model.Data.Add(item);
}
foreach (Item item in _model.Data)
{
BitmapSource itemBitmap = await Task.Run(() =>
{
Stopwatch sw = Stopwatch.StartNew();
BitmapImage bitmap = new BitmapImage();
bitmap.BeginInit();
// forces immediate load on EndInit() call
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.UriSource = new Uri(item.ImagePath, UriKind.Relative);
bitmap.DecodePixelWidth = 278;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
bitmap.EndInit();
bitmap.Freeze();
sw.Stop();
_model.LoadSeconds += sw.Elapsed.TotalSeconds;
return bitmap;
});
item.Bitmap = itemBitmap;
}
}
private static IEnumerable<Item> _GetItems()
{
for (int i = 1; i <= 60; i++)
{
Item newItem = new Item
{
ImagePath = String.Format(@"Images/{0}.jpg", i),
Name = "Item: " + i
};
yield return newItem;
}
}
}
由于我只是将文件从.zip直接复制到我的项目目录中,因此我更改了image-path循环以对应于那里的实际文件名,即1-60,而不是原始示例中的1-49 。我也没有打扰基于0的标签,而只是使它与文件名相同。
我确实做了一些调查,看看是否有另一个问题直接在这里解决了你的问题。我没有找到一个我认为完全重复的,但是有一个非常宽泛的,asynchronously loading a BitmapImage in C# using WPF,它显示了许多技术,包括与上述类似或相同的技术。
答案 1 :(得分:0)
this.listBoxItems.ItemsSource = this.ItemsCollection;
移动到方法的末尾应该有所帮助。 this.data.Add(newItem)
时,列表都会尝试更新其内容,这些内容涉及大量I / O(读取磁盘文件并解码相当大的图像)。运行探查器应该确认这一点。VirtualizingStackPanel.IsVirtualizing
将有助于降低内存要求Here是关于这个主题的一个讨论,我想你可能会感兴趣。
答案 2 :(得分:0)
当两者保持相同的对象时,您不需要ObservableCollection
和List
。删除data
字段。
您没有正确使用VirtualizingStackPanel
。 ListBox默认情况下可视化其项目。我无法理解为什么使用WrapPanel作为ItemsPanel,因为您将HorizontalScrollBar设置为禁用。从最小的变化开始。我的意思是,首先删除VirtualizingStackPanel
和ItemsPanel
,看看效果如何变化。您可以稍后更改ItemsPanel等。
我无法理解您使用Dispatcher.Invoke
填充ObservableCollection
的原因。您已在当前线程中创建它。没必要。虚拟化将负责加载图像。
如果出现问题,请告诉我。