2011-03-25 2 views
18

Есть несколько вопросов об этом уже на этом сайте и на других форумах, но я еще не нашел решение, которое действительно работает.Загрузка изображения в фоновом потоке в WPF

Вот что я хочу сделать:

  • В моем WPF приложения, я хочу, чтобы загрузить изображение.
  • Изображение из произвольного URI в Интернете.
  • Изображение может быть в любом формате.
  • Если я загружаю одно и то же изображение более одного раза, я хочу использовать стандартный интернет-кеш Windows.
  • Загрузка и декодирование изображений должны происходить синхронно, но не в потоке пользовательского интерфейса.
  • В конце концов я должен закончить тем, что я могу применить к исходному свойству объекта Image >.

вещи я попытался:

  • Использование WebClient.OpenRead() на BackgroundWorker. Прекрасно работает, но не использует кеш. WebClient.CachePolicy влияет только на конкретный экземпляр WebClient.
  • Использование WebRequest на рабочем столе вместо WebClient и настройка WebRequest.DefaultCachePolicy. Это правильно использует кеш, но я не видел примера, который не дает мне испорченных изображений в полтора раза.
  • Создание BitmapImage в BackgroundWorker, настройка BitmapImage.UriSource и попытка обработки BitmapImage.DownloadCompleted. Кажется, что этот кеш использует кеш, если установлен BitmapImage.CacheOption, но, похоже, не удается обработать DownloadCompleted, поскольку BackgroundWorker немедленно возвращается.

Я боролся с этим без проблем в течение нескольких месяцев, и я начинаю думать, что это невозможно, но вы, вероятно, умнее меня. Как вы думаете?

+0

Вы можете разместить свой код BackgroundWorker? – Rusty

ответ

13

Я подошел к этой проблеме несколькими способами, в том числе с WebClient и только с BitmapImage.

EDIT: Первоначальное предложение состояло в том, чтобы использовать конструктор BitmapImage(Uri, RequestCachePolicy), но я понял, что мой проект, в котором я тестировал этот метод, использовал только локальные файлы, а не веб. Изменение руководства для использования моей другой проверенной веб-техники.

Вы должны запустить загрузку и декодирование в фоновом потоке, потому что во время загрузки, будь то синхронно или после загрузки изображения, требуется небольшое, но значительное время, необходимое для декодирования изображения. Если вы загружаете много изображений, это может привести к остановке потока пользовательского интерфейса. (Здесь есть несколько других тонкостей, таких как 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 в 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 схема. Вы можете определить, загружается ли она, проверяя свойство BitmapImage.IsDownloading после EndInit. Есть события DownloadCompleted, DownloadFailed и DownloadProgress, но вы должны быть слишком сложными, чтобы заставить их запускать фоновый поток. Поскольку BitmapImage предоставляет только асинхронный подход, вам нужно будет добавить цикл while с эквивалентом WPF DoEvents(), чтобы поддерживать поток до тех пор, пока загрузка не будет завершена. This thread показывает код DoEvents, который работает в этом фрагменте:

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(), и это не позволяет вам настроить прокси-WebClient или другие вещи, которые может помочь с лучшей производительностью. Первый пример выше рекомендуется для этого.

+0

«Если вы действительно хотите использовать BackgroundWorker, вы можете, но вам может потребоваться отложить рабочий поток от возврата, пока изображение не будет загружено, и вы отправите обратный вызов». Я пытался это сделать, но не могу заставить его работать вообще. IsDownloading истинно, но событие DownloadCompleted никогда не срабатывает. –

+0

@ H.B. Я буду редактировать, чтобы добавить пример. –

+0

@ H.B. Я добавил пример, а также реорганизовал свой ответ, чтобы отразить дальнейшие испытания и исследования. –

8

BitmapImage нуждается в асинхронной поддержке всех своих событий и внутренних компонентов. Вызов Dispatcher.Run() в фоновом потоке будет ... хорошо запускать диспетчер для потока. (BitmapImage наследует от DispatcherObject, поэтому ему нужен диспетчер. Если в потоке, который создал BitmapImage, еще нет диспетчера, новый будет создан по требованию. Cool.).

Важная рекомендация: 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); 
Смежные вопросы