使用数据绑定填充 ListBox 很慢

3

以前我使用代码向我的ListBox手动添加项目,但是速度非常慢。我听说通过XAML进行数据绑定是提高性能的方法。

于是我设法让数据绑定工作起来(对绑定不太熟悉),但令我失望的是,性能并没有比我之前的非数据绑定方法更好。

我的想法是,我的ListBox包含一个图像,下面有一个名称。我进行了一些基准测试,54个项目需要8秒钟才能显示。这对于用户来说自然太长了。

源图像的最大尺寸为2100x1535像素,文件大小范围从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);
            }
        }
    }
}

刚刚用50张大小在300-900kb之间的图片进行了测试,显示几乎瞬间完成...不过,我不得不复制一些图片并将它们重命名,因为可用的测试材料不够。 - grek40
小图片不会,是大尺寸和详细的图片会导致它变得缓慢。 - PersuitOfPerfection
@PeterDuniho 哦,看起来是imgur在压缩它们或者做了某些处理。以下是下载所有图片的链接:http://s.imgur.com/a/jmbv6/zip - 我也会把它添加到原始帖子中。 - PersuitOfPerfection
@PeterDuniho 嗯,我也在这里验证了一下。好吧,那真是个打击。我比较了两张图片(我的源图像和一个来自压缩包的图像),发现有3MB的文件大小差异。看起来这些图片在上传到Imgur时被压缩了一定程度;/ - PersuitOfPerfection
@PeterDuniho 我需要将这些图片上传到我的网站,并提供直接下载链接,以保证图片的完整性。我现在就去做。 - PersuitOfPerfection
3个回答

2
现在我能够看到您使用的图片,我可以确认这里的主要问题就是加载大型图片的基本成本。使用这些图像文件,没有任何改进时间的方法。
您可以采取以下措施之一:异步加载图片,以便在用户等待所有图片加载完成时,程序的其余部分仍然可以响应;或者减小图片的大小,以便更快地加载。如果可能的话,我强烈推荐后者。
如果由于某种原因,部署和加载原始大尺寸格式的图片是必需的,则至少应该异步加载它们。有很多不同的方法可以实现这一点。
最简单的方法是在Image.Source绑定上设置Binding.IsAsync:
<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>

这种方法的主要缺点是在使用此方法时无法设置DecoderPixelWidthImage控件正在为您处理从路径到实际位图的转换,并且没有机制来设置各种选项。
鉴于该技术的简单性,我认为这是首选的方法,至少对我而言是如此。只要程序响应迅速并显示进度迹象,用户通常不会关心完全初始化所有数据所需的总时间。但是,需要注意的是,在此方案中未设置DecoderPixelWidth的情况下,加载所有图像所需的时间将增加近两倍(约7.5秒对比近14秒)。因此,您可能有兴趣自己异步加载图像。
这样做需要常规的异步编程技术,您可能已经熟悉。主要的“陷阱”是,默认情况下,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中复制到了我的项目目录中,因此我更改了图片路径循环以对应实际文件名,即1-60,而不是您原始示例中的1-49。 我也没有费心处理基于0的标签,而是将其设置为与文件名相同。

我确实做了一些搜索,看看是否有另一个问题直接解决了这个问题。 我没有找到一个我认为是完全重复的问题,但有一个非常广泛的问题:在C#中使用WPF异步加载BitmapImage,其中显示了许多技术,包括类似或相同于上述技术的技术。


1
非常棒的回答。非常感谢您提供的示例和解释。我也很感激您抽出时间测试我的示例并在此处提供答案。感谢您与我一起坚持,并帮助我将来提出更好的结构化问题。 - PersuitOfPerfection

1
this.listBoxItems.ItemsSource = this.ItemsCollection;这行代码移到方法末尾可能会有所帮助。
这里发生的情况是,每次执行this.data.Add(newItem)时,列表都试图更新其内容,这涉及大量I/O(读取磁盘文件并解码相当大的图像)。运行分析器应该可以证实这一点。
更好的方法是从较小的缩略图缓存中加载(这将需要较少的I/O),如果您的要求允许的话。
启用{{link2:VirtualizingStackPanel.IsVirtualizing}}将有助于保持内存要求低。

这里有一个关于这个话题的讨论,我认为你可能会觉得很有趣。


我认为VirtualizingStackPanel.IsVirtualizing="True"是隐式设置的,不需要手动设置。将itemsource行移动没有明显的区别。我会研究缩略图缓存。最让我困惑的部分是,你经常看到这种代码在使用中,但当涉及到数千个项目时,人们会使用更复杂的解决方案。但我们只谈论50个项目,所以我的示例代码中肯定有什么阴险之处,对吧? - PersuitOfPerfection
当然,我想我也可以调整图片大小并将它们保存为缩略图。我尝试了使用批量图像转换器调整图像大小,然后使用这些小图像,速度非常快。通过编程进行调整大小和磁盘保存需要很长时间吗?再次以50张图像作为示例数据池。谢谢。 - PersuitOfPerfection
@PersuitOfPerfection:我认为延迟很可能不是由于图像数量或ListBox的人口,而仅仅是读取和解码图像数据。除非使文件和图像本身更小,否则无法加快速度。您可以尝试异步加载文件,以便至少UI保持响应。这里有一篇帖子,您可能会发现有用:https://dev59.com/Xeo6XIcBkEYKwwoYQiXQ。那里没有百分之百的答案,但值得思考。 - Peter Duniho

1
  • ObservableCollectionList都保存相同的对象时,您不需要两者。删除data字段。

  • 您没有正确使用VirtualizingStackPanel。ListBox默认可视化其项目。我不明白为什么您将WrapPanel用作ItemsPanel,因为您禁用了HorizontalScrollBar。从最小更改开始。我的意思是,首先删除VirtualizingStackPanelItemsPanel,然后查看性能如何改变。您可以稍后更改ItemsPanel等。

  • 我不明白为什么您要使用Dispatcher.Invoke来填充ObservableCollection。您已在当前线程中创建了它。不需要这样做。虚拟化将负责加载图像。

如果有错误,请告诉我。


即使水平滚动条被禁用,除非使用Wrap Panel(根据我的测试),否则项不会并排显示。没有Wrap Panel,则它们以垂直列表的形式显示。每行一个项目。 - PersuitOfPerfection
目前,您正在错误的位置使用“VirtualizingStackPanel”。我的意思是从通常的ListBox开始,看看是否有性能问题。(我认为没有)。然后,您可以通过Google或提问来考虑正确使用WrapPanel、水平StackPanel等的方法。 - rmojab63

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