【.net深呼吸】WPF异步加载大批量图像

如何在WPF中加载大批量数据,而且不会阻塞UI线程,尤为是加载大量图片时,这活儿一直是不少朋友都至关关注的。世上没有最完美的解决之道,我们但求相对较优的方案。异步

通过一些试验和对比,老周找到了一种算是不错的方案,重点是这个方案比较简单,无须闯五关斩六将,只要你对数据绑定有些基础就行了。函数

好,F话少扯,我们开始吧。性能

老周手里没有那么多照片,那就用同一张图片作测试吧。假设我要在应用程序运行时加载 2 万张图片,我想2W张应该能够了,没见过谁会傻到要加载100W张那么变态。测试

大体状况是:数据源集合是一个 ObservableCollection<Uri>, 也就是说集合中放的是图像的URI,为何不放BitmapSource 呢,由于 DependencyObject 是不能跨线程操做的,只能在UI线程上建立。默认状况下,ObservableCollection<T>也不能在非UI线程上操做,不过,我能够经过调用如下方法来让它能够跨线程操做:spa

public static void EnableCollectionSynchronization(IEnumerable collection, object lockObject)

这个方法是 BindingOperations 类公开的静态方法,能够在窗口的构造函数中调用它,并且必定要在操做集合以前调用。调用时,把 ObservableCollection 集合传递给 collection 参数,第二个参数lockObject 是一个自定义对象,它指的是能够在线程间同步时引用的对象,在异步代码中,能够把这个对象写在一个 lock 语句块中。主要用途是防止UI访问集合的过程当中,集合被其余线程意外修改。线程

下面代码开启跨线程访问集合支持:code

            images = new ObservableCollection<Uri>();
            ……
            lbImages.SetBinding(ItemsControl.ItemsSourceProperty, b);

            // 这一句很关键,开启集合的异步访问支持
            BindingOperations.EnableCollectionSynchronization(images, lockobj);

 

而后在窗口的构造函数中,执行一个新 Task,用一个新线程来加载数据。对象

            Task.Run(() =>
            {
                // 代码写在 lock 块中
                lock (lockobj)
                {
                    for (int i = 0; i < 20000; i++)
                    {
                        Uri u = new Uri("0.jpg", UriKind.Relative);
                        images.Add(u);
                    }
                }
            });

开始一个新Task是为了让主线程不受阻止,能够继续响应UI操做。blog

 

因为集合中都是 URI,而界面上显示的是图像,能够弄一个自定义的数据转换器,转换为位图。图片

    public sealed class UriToBitmapConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            Uri uri = (Uri)value;
            BitmapImage bmp = new BitmapImage();
            bmp.DecodePixelHeight = 250; // 肯定解码高度,宽度不一样时设置
            bmp.BeginInit();
            // 延迟,必要时建立
            bmp.CreateOptions = BitmapCreateOptions.DelayCreation;
            bmp.CacheOption = BitmapCacheOption.OnLoad;
            bmp.UriSource = uri;
            bmp.EndInit(); //结束初始化
            return bmp;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }

由于是单向转换,因此ConvertBack就免了。

注意,在实例化BitmapImage时,DecodePixelHeight 和 DecodePixelWidth 属性只能设置任意一个,不要同时设置,否则图片的比例会变形。若是咱们界面用的图不须要很大,就设一个小的值,好比200像素,这样能够节约性能。

还能够把 CreateOptions 属性设为 DelayCreation ,这样只在图像须要时才会建立,也省了一些性能。

 

为了让这个转换器能在XAML代码中访问,须要把它的实例声明在UI的资源列表中。

        <Grid.Resources>
            <local:UriToBitmapConverter x:Key="tobmpcvt"/>
        </Grid.Resources>

 

接下来就是用Binding了,实现界面绑定。

        <ListBox Name="lbImages" ScrollViewer.IsDeferredScrollingEnabled="False"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Image Height="200" Width="200" Source="{Binding IsAsync=True,Converter={StaticResource tobmpcvt}}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>

 

使用 Binding 时,把 IsAsync 属性设为 True,这样容许界面使用辅助线程来绑定数据,记得,记得。

 

这样就完成了,而后咱们能够运行,让程序加载 2万个图像。这时候会发现,程序运行后不会卡住了,并且把滚动往下拖动时,会自动加载数据。

 

如何?这效果不错吧。

 

示例源代码下载地址

相关文章
相关标签/搜索