2011-03-25 56 views
18

關於這個網站和其他論壇上已經存在的一些問題,但我還沒有找到一個實際可行的解決方案。在WPF中的後臺線程中加載圖像

這就是我想做的事:

  • 在我的WPF應用程序,我想加載圖像。
  • 該圖像來自Web上的任意URI。
  • 圖像可以是任何格式。
  • 如果我多次加載相同的圖像,我想使用標準的Windows互聯網緩存。
  • 圖像加載和解碼應該同步發生,但不是在UI線程上。
  • 最後,我應該結束一些事情,我可以適用於<圖片>的源屬性。

事情我已經嘗試:

  • 上一個BackgroundWorker使用WebClient.OpenRead()。工作正常,但不使用緩存。 WebClient.CachePolicy僅影響特定的WebClient實例。
  • 在Backgroundworker上使用WebRequest而不是WebClient,並設置WebRequest.DefaultCachePolicy。這正確地使用了緩存,但是我還沒有看到一個例子,一半時間都不會給我看起來破損的圖像。
  • 在BackgroundWorker中創建BitmapImage,設置BitmapImage.UriSource並嘗試處理BitmapImage.DownloadCompleted。這似乎使用緩存如果BitmapImage.CacheOption設置,但似乎沒有去處理DownloadCompleted,因爲BackgroundWorker立即返回。

我一直都在爲這個零零散散的月份而苦苦掙扎,我開始認爲這是不可能的,但你可能比我聰明。你怎麼看?

+0

您可以發佈您的BackgroundWorker代碼? – Rusty 2011-03-26 00:05:46

回答

13

我以幾種方式解決了這個問題,包括WebClient和BitmapImage。

編輯:最初的建議是使用BitmapImage(Uri, RequestCachePolicy)的構造函數,但我意識到我的項目,我測試這種方法只使用本地文件,而不是網絡。改變指導以使用我的其他測試網絡技術。

您應該在後臺線程上運行下載和解碼,因爲在加載過程中,無論是同步還是下載圖像後,解碼圖像都需要很短的時間。如果您正在加載許多圖像,這可能導致UI線程停頓。 (這裏還有其他一些複雜的問題,如DelayCreation,但它們不適用於您的問題。)

有幾種方法可以加載圖像,但我發現在BackgroundWorker中從網絡加載時,您需要使用WebClient或類似的類自行下載數據。

請注意,BitmapImage在內部使用WebClient,另外它還有很多錯誤處理和憑證設置以及其他我們必須針對不同情況設置的其他事項。我提供了這個片段,但它只在有限的幾種情況下進行了測試。如果您正在處理代理,憑證或其他場景,您將不得不對此進行一些處理。

BackgroundWorker worker = new BackgroundWorker(); 
worker.DoWork += (s, e) => 
{ 
    Uri uri = e.Argument as Uri; 

    using (WebClient webClient = new WebClient()) 
    { 
     webClient.Proxy = null; //avoids dynamic proxy discovery delay 
     webClient.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Default); 
     try 
     { 
      byte[] imageBytes = null; 

      imageBytes = webClient.DownloadData(uri); 

      if (imageBytes == null) 
      { 
       e.Result = null; 
       return; 
      } 
      MemoryStream imageStream = new MemoryStream(imageBytes); 
      BitmapImage image = new BitmapImage(); 

      image.BeginInit(); 
      image.StreamSource = imageStream; 
      image.CacheOption = BitmapCacheOption.OnLoad; 
      image.EndInit(); 

      image.Freeze(); 
      imageStream.Close(); 

      e.Result = image; 
     } 
     catch (WebException ex) 
     { 
      //do something to report the exception 
      e.Result = ex; 
     } 
    } 
}; 

worker.RunWorkerCompleted += (s, e) => 
    { 
     BitmapImage bitmapImage = e.Result as BitmapImage; 
     if (bitmapImage != null) 
     { 
      myImage.Source = bitmapImage; 
     } 
     worker.Dispose(); 
    }; 

worker.RunWorkerAsync(imageUri); 

我測試了這個在一個簡單的項目,它工作正常。我並不是100%關於它是否打到緩存,而是從MSDN可以告訴我的其他論壇問題,以及Reflectoring into PresentationCore中,它應該打到緩存。 WebClient包裝了WebRequest,它封裝了HTTPWebRequest等等,並且緩存設置在每一層傳遞。

BitmapImage BeginInit/EndInit對確保您可以同時設置所需的設置,然後在執行EndInit期間設置。如果你需要設置任何其他屬性,你應該使用空的構造函數,並寫出如上所示的BeginInit/EndInit對,在調用EndInit之前設置你需要的。

我通常還會設置這個選項,這迫使它EndInit期間將圖像加載到內存中:

image.CacheOption = BitmapCacheOption.OnLoad; 

這將權衡可能的更高的內存使用情況更好的運行時性能。如果你這樣做,那麼BitmapImage將在EndInit中同步加載,除非BitmapImage需要從URL下載異步。

其它注意事項:

的BitmapImage將異步下載,如果UriSource是絕對URI是HTTP或HTTPS方案。您可以通過在EndInit之後檢查BitmapImage.IsDownloading屬性來判斷它是否正在下載。有DownloadCompleted,DownloadFailed和DownloadProgress事件,但你必須特別棘手才能讓它們在後臺線程中觸發。由於BitmapImage只公開了一種異步方法,因此您必須添加一個while循環與WPF等效的DoEvents()以使線程保持活動狀態,直到下載完成。爲的DoEvents,在這個片段中工作This thread顯示代碼:

worker.DoWork += (s, e) => 
    { 
     Uri uri = e.Argument as Uri; 
     BitmapImage image = new BitmapImage(); 

     image.BeginInit(); 
     image.UriSource = uri; 
     image.CacheOption = BitmapCacheOption.OnLoad; 
     image.UriCachePolicy = new RequestCachePolicy(RequestCacheLevel.Default); 
     image.EndInit(); 

     while (image.IsDownloading) 
     { 
      DoEvents(); //Method from thread linked above 
     } 
     image.Freeze(); 
     e.Result = image; 
    }; 

雖然上述方法工作,它有一個代碼味道,因爲的DoEvents()的,它不會讓你配置Web客戶端代理或其他東西,可能有助於提高性能。上面的第一個例子推薦使用這個例子。

+0

「如果您真的想使用BackgroundWorker,您可以,但您可能需要延遲工作線程返回,直到圖像下載併發送回調。」我一直在試圖做到這一點,但根本無法完成。 IsDownloading是true,但DownloadCompleted事件從不觸發。 – 2011-03-26 01:40:46

+0

@ H.B。我將編輯添加一個示例。 – 2011-03-26 01:47:06

+0

@ H.B。我添加了一個例子,並重新組織了我的答案以反映進一步的測試和研究。 – 2011-03-26 05:14:41

8

BitmapImage需要所有事件和內部的異步支持。調用後臺線程上的Dispatcher.Run()將...很好地運行該線程的調度程序。 (BitmapImage從DispatcherObject繼承,所以它需要一個調度器,如果創建BitmapImage的線程還沒有調度器,那麼會根據需要創建一個新的。)。

重要安全提示:如果BitmapImage從緩存(老鼠)中提取數據,則不會引發任何事件。

這一直對我非常好....

 var worker = new BackgroundWorker() { WorkerReportsProgress = true }; 

    // DoWork runs on a brackground thread...no thouchy uiy. 
    worker.DoWork += (sender, args) => 
    { 
     var uri = args.Argument as Uri; 
     var image = new BitmapImage(); 

     image.BeginInit(); 
     image.DownloadProgress += (s, e) => worker.ReportProgress(e.Progress); 
     image.DownloadFailed += (s, e) => Dispatcher.CurrentDispatcher.InvokeShutdown(); 
     image.DecodeFailed += (s, e) => Dispatcher.CurrentDispatcher.InvokeShutdown(); 
     image.DownloadCompleted += (s, e) => 
     { 
      image.Freeze(); 
      args.Result = image; 
      Dispatcher.CurrentDispatcher.InvokeShutdown(); 
     }; 
     image.UriSource = uri; 
     image.EndInit(); 

     // !!! if IsDownloading == false the image is cached and NO events will fire !!! 

     if (image.IsDownloading == false) 
     { 
      image.Freeze(); 
      args.Result = image; 
     } 
     else 
     { 
      // block until InvokeShutdown() is called. 
      Dispatcher.Run(); 
     } 
    }; 

    // ProgressChanged runs on the UI thread 
    worker.ProgressChanged += (s, args) => progressBar.Value = args.ProgressPercentage; 

    // RunWorkerCompleted runs on the UI thread 
    worker.RunWorkerCompleted += (s, args) => 
    { 
     if (args.Error == null) 
     { 
      uiImage.Source = args.Result as BitmapImage; 
     } 
    }; 

    var imageUri = new Uri(@"http://farm6.static.flickr.com/5204/5275574073_1c5b004117_b.jpg"); 

    worker.RunWorkerAsync(imageUri);