使WPF图像异步加载

7
我想从后台代码加载Gravatar图像并将其设置为WPF Image-Control。所以代码看起来像这样:
imgGravatar.Source = GetGravatarImage(email);

GetGravatarImage是以下样子:

BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri( GravatarImage.GetURL( "http://www.gravatar.com/avatar.php?gravatar_id=" + email) , UriKind.Absolute );
bi.EndInit();
return bi;

不幸的是,当网络连接缓慢时,这会锁定GUI。有没有一种方法可以分配图像源并在后台加载图像而不阻塞UI?

谢谢!

2个回答

26

我建议您在XAML中为imgGravatar使用绑定。将IsAsync=true设置为它,WPF将自动利用线程池中的一个线程来获取图像。您可以将解析逻辑封装到IValueConverter中,并将电子邮件简单地绑定为Source。

XAML中:

<Window.Resouces>
    <local:ImgConverter x:Key="imgConverter" />
</Window.Resource>

...


<Image x:Name="imgGravatar" 
       Source="{Binding Path=Email, 
                        Converter={StaticResource imgConverter}, 
                        IsAsync=true}" />

在代码中:

public class ImgConverter : IValueConverter
{
    public override object Convert(object value, ...)
    {
        if (value != null)
        {
             BitmapImage bi = new BitmapImage();
             bi.BeginInit();
             bi.UriSource = new Uri( 
                 GravatarImage.GetURL(
                     "http://www.gravatar.com/avatar.php?gravatar_id=" + 
                      value.ToString()) , UriKind.Absolute 
                 );
             bi.EndInit();
             return bi;                
        }
        else
        {
            return null;
        }

    }
}

2
BitmapImage也有一个带有Uri参数的构造函数,这将节省您BeginInit/EndInit调用。 - Clemens
+1 我不知道这个,很好的提示。我只是从作者那里复制了代码,没有考虑如何优化实际下载,而是专注于以优雅的方式解决 UI 阻塞问题(当然,如果代码不是异步的话,它会阻塞)。我认为对于重复使用,转换器也可以从绑定中获取完整的 URL,以使其更具可重用性,而不是电子邮件。我不喜欢使用 ThreadPool,因为你必须一直管理调度。这使得对于一个纯 MVVM 应用程序来说,它对我来说不够优雅。 - JanW
你能解释一下为什么创建BitmapImage时不异步调用会阻塞吗?那么它为什么有“IsDownloading”属性和“DownloadCompleted”事件呢?如果你在XAML中将一个Image控件的“Source”属性简单地设置为引用Web上大图像的URL,UI不会被阻塞。相反,WPF会在后台下载图像,并在下载完成后立即显示它。 - Clemens
我非常确定这是由BitmapImage执行的,因为(来自MSDN)“BitmapImage主要存在于支持可扩展应用程序标记语言(XAML)语法,并引入了用于位图加载的其他属性,这些属性未由BitmapSource定义”。 - Clemens
我对一个巨大的图像(14MB)进行了一些测试,你是完全正确的。 BitmapImage 在后台线程上自动执行下载。 UI 仅在将图像数据加载到 UI 时挂起。 对于混淆,我感到抱歉。 下载是在将 BitmapImage 分配为 Source 而不是使用 BeginInit() 后开始的。 因此,原始作者的代码应该可以正常工作。 所以也许与他在 UI 上使用的图像数量有关。 - JanW
显示剩余3条评论

10
我不明白为什么你的代码会阻塞UI,因为BitmapImage支持在后台下载图像数据。这就是它具有IsDownloading属性和DownloadCompleted事件的原因。
无论如何,以下代码展示了一种直接从线程池中创建并下载图像的简单方法。它使用一个WebClient实例来下载整个图像缓冲区,然后从该缓冲区创建一个BitmapImage。创建BitmapImage后,调用Freeze使其可以从UI线程访问。最后,通过Dispatcher.BeginInvoke调用,在UI线程中分配Image控件的Source属性。
ThreadPool.QueueUserWorkItem(
    o =>
    {
        var url = GravatarImage.GetURL(
           "http://www.gravatar.com/avatar.php?gravatar_id=" + email);
        var webClient = new WebClient();
        var buffer = webClient.DownloadData(url);
        var bitmapImage = new BitmapImage();

        using (var stream = new MemoryStream(buffer))
        {
            bitmapImage.BeginInit();
            bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
            bitmapImage.StreamSource = stream;
            bitmapImage.EndInit();
            bitmapImage.Freeze();
        }

        Dispatcher.BeginInvoke((Action)(() => image.Source = bitmapImage));
    });

编辑:今天你只需要使用异步方法:

var url = GravatarImage.GetURL(
    "http://www.gravatar.com/avatar.php?gravatar_id=" + email);
var httpClient = new HttpClient();
var responseStream = await httpClient.GetStreamAsync(url);
var bitmapImage = new BitmapImage();

using (var memoryStream = new MemoryStream())
{
    await responseStream.CopyToAsync(memoryStream);

    bitmapImage.BeginInit();
    bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
    bitmapImage.StreamSource = memoryStream;
    bitmapImage.EndInit();
    bitmapImage.Freeze();
}

image.Source = bitmapImage;

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