【.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萬個圖像。這時候會發現,程序運行後不會卡住了,並且把滾動往下拖動時,會自動加載數據。

 

如何?這效果不錯吧。

 

示例源代碼下載地址

相關文章
相關標籤/搜索