如何在WPF中加載大批量數據,並且不會阻塞UI線程,尤其是加載大量圖片時,這活兒一直是很多朋友都相當關注的。世上沒有最完美的解決之道,咱們但求相對較優的方案。
經過一些試驗和對比,老周找到了一種算是不錯的方案,重點是這個方案比較簡單,無須闖五關斬六將,只要你對數據綁定有些基礎就好了。
好,F話少扯,咱們開始吧。
老周手里沒有那么多照片,那就用同一張圖片做測試吧。假設我要在應用程序運行時加載 2 萬張圖片,我想2W張應該可以了,沒見過誰會傻到要加載100W張那么變態。
大致情況是:數據源集合是一個 ObservableCollection<Uri>, 也就是說集合中放的是圖像的URI,為什么不放BitmapSource 呢,因為 DependencyObject 是不能跨線程操作的,只能在UI線程上創建。默認情況下,ObservableCollection<T>也不能在非UI線程上操作,不過,我可以通過調用以下方法來讓它可以跨線程操作:
public static void EnableCollectionSynchronization(IEnumerable collection, object lockObject)
這個方法是 BindingOperations 類公開的靜態方法,可以在窗口的構造函數中調用它,而且一定要在操作集合之前調用。調用時,把 ObservableCollection 集合傳遞給 collection 參數,第二個參數lockObject 是一個自定義對象,它指的是可以在線程間同步時引用的對象,在異步代碼中,可以把這個對象寫在一個 lock 語句塊中。主要用途是防止UI訪問集合的過程中,集合被其他線程意外修改。
下面代碼開啟跨線程訪問集合支持:
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操作。
由於集合中都是 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萬個圖像。這時候會發現,程序運行后不會卡住了,而且把滾動往下拖動時,會自動加載數據。
如何?這效果不錯吧。