【问题标题】:Why does a listbox take forever to load when I override its ItemsPanel to wrappanel?当我将其 ItemsPanel 覆盖为 wrappanel 时,为什么列表框需要永远加载?
【发布时间】:2014-02-05 12:59:50
【问题描述】:

我正在尝试将项目(图像)添加到 ListBox,最好使用 WrapPanel 类型的布局。很简单。

我遇到的问题是,当我将 ListBox.ItemsPanel 模板定义为使用包装面板时,加载图像所花费的时间变得难以忍受。我目前正在添加大约 70 张图片,但我希望它能够支持数千张。

如果我不自定义列表框,加载速度会非常快。我在下面的 XAML 中提到了我要更改的部分。

我不明白的是

  1. 为什么看似很小的 xaml 更改会导致应用阻塞
  2. 我如何仍然可以获得具有不错性能的 wrappanel 类型布局。

    <Grid>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>
    
    <Button Grid.Row=0 Name="ImageButton" Click="ImageButton_Click">GET IMAGES</Button> 
    
    <ListBox Grid.Row="1" Name="ImageCollection" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
        <!-- Items added to list panel using Wrap Panel Template. --> 
        <!-- By removing the below, things go pretty fast --> 
        <!-- When the ListBox.ItemsPanel uses a wrappanel as below, loading up the images is SLOW. If I delete the below, loading is FAST, but isn't what I want.  -->
    
        <ListBox.ItemsPanel>                
            <ItemsPanelTemplate>
                <WrapPanel/>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
    </Grid>
    

我的代码如下所示。我在这里没有更改任何内容,只是将其包含为信息。

    public AsyncGet()
    {
        InitializeComponent();                        
    }

    private List<String> images = new List<string>(
        Directory.GetFiles(@"C:\Users\me\Pictures\wallpapers")
    );

    private void ImageButton_Click(object sender, RoutedEventArgs e)
    {
        foreach (string s in images)
        { 
            Image image = new Image();
            BitmapImage bi = new BitmapImage();
            bi.BeginInit();
            bi.UriSource = new Uri(s);
            bi.EndInit();
            image.Source = bi;
            image.Height = 200;
            image.Width = 200;
            ImageCollection.Items.Add(image);
        }
    }

编辑

在尝试了一些我不太满意的 VirtualWrapPanel 实现之后,我决定坚持使用 WrapPanel。我做了一些似乎使主线程保持响应的事情:

  1. 保持 BitMapImage 解码尺寸和图像尺寸为 200
  2. 通过为每个项目添加默认图像开始加载。预期路径存储在 Images 标记中。
  3. 遍历每一项更新源,使用 async/await 生成 bitmapImage。

我不知道我正在按照预期的方式使用 async/await。但它似乎允许 UI 保持响应。图像被循环,最终所有默认值都被替换,它可能需要比其他方式更长的时间,但我更喜欢仍然接受用户输入的能力。

我希望我能找到一种方法来对它进行数据绑定,我可以稍后再试。

private List<String> images = new List<string>(
    Directory.GetFiles(@"C:\Users\me\Pictures\wallpapers")
);

private Image defaultImage;

public AsyncGet()
{
    InitializeComponent();
    DataContext = this;
    defaultImage = new Image();
    defaultImage.Source = MakeBitmapImage( @"C:\Users\me\Pictures\MeowShawn_Lynch.jpg");
    defaultImage.Height = 200;
    defaultImage.Width = 200;
}

private async void ImageButton_Click(object sender, RoutedEventArgs e)
{
    ImageCollection.Items.Clear();

    foreach(string s in images)
    {
        Image image = new Image();
        image.Source = defaultImage.Source;
        image.Height = defaultImage.Height;
        image.Width = defaultImage.Width;
        ImageCollection.Items.Add(image);
        image.Tag = s;
    }

    foreach (Image image in ImageCollection.Items)
    {
        string path = image.Tag.ToString();
        Task<BitmapImage> task = new Task<BitmapImage>(() => MakeBitmapImage(path, true));
        task.Start();

        image.Source = await task;
    }         
}

    private BitmapImage MakeBitmapImage(string path, Boolean freeze = false)
    {
        BitmapImage bi = new BitmapImage();

        bi.CacheOption = BitmapCacheOption.OnLoad;            
        bi.BeginInit();
        bi.UriSource = new Uri(path);
        bi.DecodePixelHeight = 200;

        bi.EndInit();
        if (freeze)
        {
            bi.Freeze();
        }
        return bi;
    }

【问题讨论】:

    标签: c# wpf listbox


    【解决方案1】:

    默认情况下ListBox 将使用VirtualizingStackPanel,它不会渲染当前不在视图中的元素。

    “虚拟化”一词是指一种技术,通过该技术,用户界面 (UI) 元素的子集根据屏幕上可见的数据项从大量数据项中生成。当屏幕上可能只有几个元素时生成许多 UI 元素会对应用程序的性能产生不利影响。 VirtualizingStackPanel 计算可见项的数量,并与 ItemsControl(例如 ListBox 或 ListView)中的 ItemContainerGenerator 一起为可见项创建 UI 元素。

    WrapPanel 没有该功能,所有项目都被视为可见。你可以试试这个Virtualizing WrapPanel控件。

    对于普通ListBox,默认ItemsPanel,可以通过设置VirtualizingStackPanel.IsVirtualizing附加属性来控制虚拟化:

    <ListBox VirtualizingStackPanel.IsVirtualizing="True" 
    

    【讨论】:

    • 如果你能摆脱它,那么我还建议将 VirtualizingPanel.VirtualizationMode 设置为“回收”,以便在用户滚动列表时重复使用创建的任何项目。
    • 谢谢。我仍然在这里玩一些想法并且对结果并不满意,但我认为这是“为什么包装面板很慢”的最佳答案。我尝试了 VirtualizingWrapPanel,但它似乎有点……笨重。滚动不稳定,列表末尾会发生奇怪的事情。
    • 你是对的。我用过几次,它远非完美。您的问题没有简单的答案,因为没有打开/关闭的选项。你可以玩线程。如果您要使用绑定,您可以使用Binding.IsAsync。如果它会加载所有图像,至少在加载时不要阻塞 UI。
    • 同样为了优化内存使用,如果原图很大,可以使用BitmapImage.DecodePixelWidth/BitmapImage.DecodePixelHeight来限制图像在内存中解码的分辨率
    • Binding.IsAsync 是一件棘手的事情。我认为这是一个很好的例子,它的帮助微乎其微,但不是灵丹妙药。我感觉 I/O 部分(IsAsync 有帮助)实际上非常快,而且 CPU 成本比我估计的要高。有道理,处理这些像素一定很昂贵。由于这是 UI 的东西,我们必须在主线程上执行此操作并让 UI 挂起。我管理的最好的是在另一个线程上创建 bitMapImage 时的异步/等待。 DecodePixelHeight 也有帮助。数量级比我开始的要好。我会尽快更新最终结果。
    【解决方案2】:

    建议:与其使用 UIElement 的名称 (ImageCollection) 填充图像集合,不如实现 INotifyPropertyChanged 并将图像列表公开为表单的属性,而不是使用绑定(即 WPF - 执行此类操作的方式)并最后但同样重要的是检查启用/禁用虚拟化是否对您的问题有任何影响...

    【讨论】:

      【解决方案3】:

      如果您想支持数千个,请考虑一个仅按需加载 BitmapImage 的类
      此示例仅在虚拟化请求时加载图像

      ListBox 的 Image 与 itemsource 绑定到 BitmapImage

      Showing items as images in a WPF ListView

      public class ImageDefer
      {
         private string source;
         private BitmapImage bi;
         public BitmapImage BI 
         {
            get 
            {
               if (bi is null)
               {
                  bi = new BitmapImage();
                  bi.BeginInit();
                  bi.UriSource = new Uri(source);
                  bi.EndInit();
               }
               return bi;
            }
          }
          public  ImageDefer (string Source) { source = Source; }   
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多