简体   繁体   English

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

[英]Loading an image in a background thread in WPF

There are a bunch of questions about this already on this site and other forums, but I've yet to find a solution that actually works. 在这个网站和其他论坛上已经有很多关于这方面的问题,但我还没有找到真正有效的解决方案。

Here's what I want to do: 这就是我想要做的事情:

  • In my WPF app, I want to load an image. 在我的WPF应用程序中,我想加载一个图像。
  • The image is from an arbitrary URI on the web. 图像来自Web上的任意URI。
  • The image could be in any format. 图像可以是任何格式。
  • If I load the same image more than once, I want to use the standard windows internet cache. 如果我不止一次加载相同的图像,我想使用标准的Windows互联网缓存。
  • Image loading and decoding should happen synchronously, but not on the UI Thread. 图像加载和解码应该同步进行,但不能在UI线程上进行。
  • In the end I should end up with something that I can apply to an <Image>'s source property. 最后,我应该得到一些我可以应用于<Image>的源属性的东西。

Things I have tried: 我尝试过的事情:

  • Using WebClient.OpenRead() on a BackgroundWorker. 在BackgroundWorker上使用WebClient.OpenRead()。 Works fine, but doesn't use the cache. 工作正常,但不使用缓存。 WebClient.CachePolicy only affects that particular WebClient instance. WebClient.CachePolicy仅影响该特定WebClient实例。
  • Using WebRequest on the Backgroundworker instead of WebClient, and setting WebRequest.DefaultCachePolicy. 在Backgroundworker上使用WebRequest而不是WebClient,并设置WebRequest.DefaultCachePolicy。 This uses the cache properly, but I've not seen an example that doesn't give me corrupted-looking images half the time. 这正确地使用了缓存,但我没有看到一个例子,它没有给我一半时间看起来像图像的图像。
  • Creating a BitmapImage in a BackgroundWorker, setting BitmapImage.UriSource and trying to handle BitmapImage.DownloadCompleted. 在BackgroundWorker中创建BitmapImage,设置BitmapImage.UriSource并尝试处理BitmapImage.DownloadCompleted。 This seems to use the cache if BitmapImage.CacheOption is set, but there doesn't seem to be away to handle DownloadCompleted since the BackgroundWorker returns immediately. 如果设置了BitmapImage.CacheOption,这似乎使用了缓存,但是由于BackgroundWorker立即返回,因此似乎没有办法处理DownloadCompleted。

I've been struggling with this off-and-on for literally months and I'm starting to think it's impossible, but you're probably smarter than me. 几个月来我一直在努力解决这个问题,我开始认为这是不可能的,但你可能比我更聪明。 What do you think? 你怎么看?

I have approached this problem in several ways, including with WebClient and just with BitmapImage. 我已经通过多种方式解决了这个问题,包括WebClient和BitmapImage。

EDIT: Original suggestion was to use the BitmapImage(Uri, RequestCachePolicy) constructor, but I realized my project where I tested this method was only using local files, not web. 编辑:原始建议是使用BitmapImage(Uri,RequestCachePolicy)构造函数,但我意识到我测试此方法的项目只使用本地文件,而不是web。 Changing guidance to use my other tested web technique. 更改指南以使用我的其他测试网络技术。

You should run the download and decoding on a background thread because during loading, whether synchronous or after download the image, there is a small but significant time required to decode the image. 您应该在后台线程上运行下载和解码,因为在加载期间,无论是同步还是在下载图像之后,解码图像所需的时间很短但很长。 If you are loading many images, this can cause the UI thread to stall. 如果要加载许多图像,则可能导致UI线程停止。 (There are a few other intricacies here like DelayCreation but they don't apply to your question.) (这里有一些其他复杂的东西,比如DelayCreation,但它们不适用于你的问题。)

There are a couple ways to load an image, but I've found for loading from the web in a BackgroundWorker, you'll need to download the data yourself using WebClient or a similar class. 有几种方法可以加载图像,但我发现在BackgroundWorker中从Web加载时,您需要使用WebClient或类似的类自行下载数据。

Note that BitmapImage internally uses a WebClient, plus it has a lot of error handling and settings of credentials and other things that we'd have to figure out for different situations. 请注意,BitmapImage内部使用WebClient,而且它有很多错误处理和凭据设置以及我们必须针对不同情况确定的其他事项。 I'm providing this snippet but it has only been tested in a limited number of situations. 我提供此代码片段,但它仅在有限的情况下进行了测试。 If you are dealing with proxies, credentials, or other scenarios you'll have to massage this a bit. 如果您正在处理代理,凭证或其他方案,您将不得不按摩这一点。

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

I tested this in a simple project and it works fine. 我在一个简单的项目中测试了它,它工作正常。 I'm not 100% about whether it is hitting the cache, but from what I could tell from MSDN, other forum questions, and Reflectoring into PresentationCore it should be hitting the cache. 我不是100%关于它是否正在达到缓存,而是从我从MSDN可以看出的,其他论坛问题,以及Reflectoring到PresentationCore它应该打到缓存。 WebClient wraps WebRequest, which wraps HTTPWebRequest, and so on, and the cache settings are passed down each layer. WebClient包装WebRequest,它包装HTTPWebRequest,依此类推,缓存设置在每一层传递下来。

The BitmapImage BeginInit/EndInit pair ensures that you can set the settings you need at the same time and then during EndInit it executes. BitmapImage BeginInit / EndInit对确保您可以同时设置所需的设置,然后在执行EndInit期间。 If you need to set any other properties, you should use the empty constructor and write out the BeginInit/EndInit pair like above, setting what you need before calling EndInit. 如果你需要设置任何其他属性,你应该使用空构造函数并写出如上所述的BeginInit / EndInit对,在调用EndInit之前设置你需要的东西。

I typically also set this option, which forces it to load the image into memory during EndInit: 我通常还设置此选项,强制它在EndInit期间将图像加载到内存中:

image.CacheOption = BitmapCacheOption.OnLoad;

This will trade off possible higher memory usage for better runtime performance. 这将牺牲可能的更高内存使用率以获得更好的运行时性能。 If you do this, then the BitmapImage will be loaded synchronously within EndInit, unless the BitmapImage requires async downloading from a URL. 如果这样做,那么BitmapImage将在EndInit中同步加载,除非BitmapImage需要从URL下载异步。

Further notes: 附加说明:

BitmapImage will async download if the UriSource is an absolute Uri and is an http or https scheme. 如果UriSource是绝对Uri并且是http或https方案,则BitmapImage将异步下载。 You can tell whether it is downloading by checking the BitmapImage.IsDownloading property after EndInit. 您可以通过在EndInit之后检查BitmapImage.IsDownloading属性来判断它是否正在下载。 There are DownloadCompleted, DownloadFailed, and DownloadProgress events, but you have to be extra tricky to get them to fire on the background thread. 有DownloadCompleted,DownloadFailed和DownloadProgress事件,但是你必须要特别棘手才能在后台线程上触发它们。 Since BitmapImage only exposes an asynchronous approach, you would have to add a while loop with the WPF equivalent of DoEvents() to keep the thread alive until the download is complete. 由于BitmapImage仅公开异步方法,因此您必须使用等效于DoEvents()的WPF添加while循环,以使该线程保持活动状态,直到下载完成为止。 This thread shows code for DoEvents that works in this snippet: 此线程显示适用于此代码段的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;
    };

While the above approach works, it has a code smell because of DoEvents(), and it doesn't let you configure the WebClient proxy or other things that might help with better performance. 虽然上述方法有效,但由于DoEvents(),它具有代码味道,并且它不允许您配置WebClient代理或其他可能有助于提高性能的事物。 The first example above is recommended over this one. 建议使用上面的第一个示例。

The BitmapImage needs async support for all of its events and internals. BitmapImage需要对其所有事件和内部的异步支持。 Calling Dispatcher.Run() on the background thread will...well run the dispatcher for the thread. 在后台线程上调用Dispatcher.Run()将...运行该线程的调度程序。 (BitmapImage inherits from DispatcherObject so it needs a dispatcher. If the thread that created the BitmapImage doesn't already have a dispatcher a new one will be created on demand. cool.). (BitmapImage继承自DispatcherObject,因此它需要一个调度程序。如果创建BitmapImage的线程还没有调度程序,则将根据需要创建一个新的调度程序。很酷。)。

Important safety tip: The BitmapImage will NOT raise any events if it is pulling data from cache (rats). 重要安全提示:如果BitmapImage从缓存(大鼠)中提取数据,则不会引发任何事件。

This has been working very well for me.... 这对我来说非常好....

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM