在WPF中使用后台线程加载图像

18

这个网站和其他论坛上已经有了很多关于这个问题的讨论,但我还没有找到真正可行的解决方案。

以下是我的需求:

  • 在我的 WPF 应用程序中,我想要加载一张来自于网络上任意 URI 的图片;
  • 这张图片可以是任意格式的;
  • 如果我加载同一张图片超过一次,我希望使用标准的 Windows Internet 缓存;
  • 图像加载和解码应该同步进行,但不能在 UI 线程上执行;
  • 最终我需要得到一些东西,可以应用到<Image>的源属性上。

我尝试过以下方法:

  • 在 BackgroundWorker 中使用 WebClient.OpenRead()。虽然能正常工作,但不使用缓存。WebClient.CachePolicy 只影响特定的 WebClient 实例。
  • 在 Backgroundworker 上使用 WebRequest 而不是 WebClient,并设置 WebRequest.DefaultCachePolicy。这种方法可以正确使用缓存,但我没有看到一个示例可以避免出现看起来损坏的图像。
  • 在 BackgroundWorker 中创建一个 BitmapImage,设置 BitmapImage.UriSource 并尝试处理 BitmapImage.DownloadCompleted。如果设置了 BitmapImage.CacheOption,好像它会使用缓存,但由于 BackgroundWorker 立即返回,因此似乎没有办法处理 DownloadCompleted 事件。

我已经苦苦挣扎了几个月,但还是无法解决这个问题。我开始觉得这是不可能的,但你可能比我聪明。你怎么看?


你能发布一下你的BackgroundWorker代码吗? - Rusty
2个回答

14

我已经采用了几种方法来解决这个问题,包括使用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);

我在一个简单的项目中测试过,它可以正常工作。我不确定它是否正在访问缓存,但从我从MSDN、其他论坛问题和对PresentationCore的反射中得出的结论来看,它应该正在访问缓存。WebClient包装了WebRequest,后者包装了HTTPWebRequest,以此类推,并且缓存设置会传递到每个层级。
BitmapImage的BeginInit/EndInit配对确保您可以同时设置所需的设置,然后在EndInit期间执行。如果您需要设置任何其他属性,您应该使用空构造函数并编写像上面那样的BeginInit/EndInit配对,在调用EndInit之前设置所需内容。
我通常还设置此选项,强制在EndInit期间将图像加载到内存中:
image.CacheOption = BitmapCacheOption.OnLoad;

这将以更好的运行时性能为代价来换取可能更高的内存使用。如果您这样做,那么除非BitmapImage需要通过URL进行异步下载,否则它将在EndInit中同步加载。
进一步说明:
如果UriSource是绝对Uri并且是http或https方案,则BitmapImage将会异步下载。您可以在EndInit后检查BitmapImage.IsDownloading属性来判断它是否正在下载。有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代理或其他可能有助于更好性能的东西。建议使用上面的第一个示例。


如果你真的想要使用BackgroundWorker,你可以这样做,但是你可能需要延迟工作线程返回直到图像下载完成并分派回调。我一直在尝试这样做,但完全无法让它工作。IsDownloading为true,但是DownloadCompleted事件从未触发。 - H.B.
@H.B. 我添加了一个例子,并重新组织了我的答案以反映进一步的测试和研究。 - Joshua Blake
这真是令人惊讶的复杂(我需要阅读一些调度程序相关的内容才能真正理解它),但最终它终于起作用了,谢谢! - H.B.
@H.B. 我同意,这似乎相当复杂,但它类似于WPF在内部System.Windows.Media.Imaging.BitmapDownload类中所做的工作。 (实际上,BitmapDownload使用WebRequest,但WebClient无论如何都会包装WebRequest。)如果您找到更好的方法,请告诉我。 - Joshua Blake
DoEvents技术是我尝试解决这个问题的众多方法之一,它确实有效,但在加载图像时使用了近100%的CPU,因此我不建议使用它。 - Josh Santangelo
以上的代码似乎比我之前写的要好一些,因为它更频繁地使用缓存,但并非总是如此。然而,当尝试解码某些图像时,我也遇到了奇怪的“未找到合适的成像组件”错误,这些URL如果在UriSource中设置则可以正常工作。 - Josh Santangelo

8
< p > 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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接