Загрузка изображения в фоновом потоке в WPF - PullRequest
18 голосов
/ 26 марта 2011

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

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

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

То, что я пробовал:

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

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

Ответы [ 2 ]

13 голосов
/ 26 марта 2011

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

РЕДАКТИРОВАНИЕ: Первоначально было предложено использовать конструктор 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, других вопросов на форуме и рефлексии в 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 (), чтобы сохранить поток работоспособным до завершения загрузки. В этой теме показан код 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 или другие вещи, которые могут помочь в повышении производительности. Первый пример выше рекомендуется поверх этого.

8 голосов
/ 26 марта 2011

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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...