在WPF中的后台线程中加载图像

时间:2011-03-25 23:09:47

标签: wpf

在本网站和其他论坛上已经有很多关于此的问题,但我还没有找到真正有效的解决方案。

这就是我想要做的事情:

  • 在我的WPF应用中,我想加载图片。
  • 图片来自网络上的任意URI。
  • 图片可以是任何格式。
  • 如果我多次加载相同的图像,我想使用标准的Windows互联网缓存。
  • 图像加载和解码应该同步进行,但不能在UI线程上进行。
  • 最后,我最终会得到一些可以应用于< Image>的源属性的内容。

我尝试过的事情:

  • 在BackgroundWorker上使用WebClient.OpenRead()。工作正常,但不使用缓存。 WebClient.CachePolicy仅影响该特定WebClient实例。
  • 在Backgroundworker上使用WebRequest而不是WebClient,并设置WebRequest.DefaultCachePolicy。这正确地使用了缓存,但是我没有看到一个例子,它没有给我一半时间看到损坏的图像。
  • 在BackgroundWorker中创建BitmapImage,设置BitmapImage.UriSource并尝试处理BitmapImage.DownloadCompleted。如果设置了BitmapImage.CacheOption,这似乎使用了缓存,但是由于BackgroundWorker立即返回,因此似乎没有办法处理DownloadCompleted。

我几个月来一直在苦苦挣扎,我开始认为这是不可能的,但你可能比我更聪明。你觉得怎么样?

2 个答案:

答案 0 :(得分:13)

我已经通过多种方式解决了这个问题,包括WebClient和BitmapImage。

编辑:最初的建议是使用BitmapImage(Uri, RequestCachePolicy)构造函数,但我意识到我测试此方法的项目只使用本地文件,而不是web。更改指南以使用我的其他测试网络技术。

您应该在后台线程上运行下载和解码,因为在加载期间,无论是同步还是在下载图像之后,解码图像所需的时间很短但很长。如果要加载许多图像,则可能导致UI线程停止。 (这里有一些其他复杂的东西,比如DelayCreation,但它们不适用于你的问题。)

有几种方法可以加载图像,但我发现在BackgroundWorker中从Web加载时,您需要使用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下载异步。

补充说明:

如果UriSource是绝对Uri并且是http或https方案,则BitmapImage将异步下载。您可以通过在EndInit之后检查BitmapImage.IsDownloading属性来判断它是否正在下载。有DownloadCompleted,DownloadFailed和DownloadProgress事件,但是你必须要特别棘手才能在后台线程上触发它们。由于BitmapImage仅公开异步方法,因此您必须使用等效于DoEvents()的WPF添加while循环,以使该线程保持活动状态,直到下载完成为止。 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代理或其他可能有助于提高性能的内容。建议使用上面的第一个示例。

答案 1 :(得分: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);