Подтвердить что ты не робот

Загрузка изображения в фоновом потоке в 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 немедленно возвращается.

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

4b9b3361

Ответ 1

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

Ответ 2

BitmapImage нуждается в поддержке async для всех своих событий и внутренних компонентов. Вызов 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);