WPF 图片列表卡顿?一文教你实现丝滑异步加载

138 阅读5分钟

前言

在开发WPF应用程序时,图片的加载是一个常见但又容易被忽视的细节。尤其是在处理网络图片或大量本地图片时,如果直接绑定图片路径,界面很容易出现卡顿、假死,用户体验大打折扣。更理想的做法是:在图片加载完成前,显示一个"加载中"的动画,加载完成后平滑地展示图片,就像网页中的图片懒加载一样。

最近在做一个WPF项目,需求是以列表形式展示本地和网络图片。面对加载延迟问题,我深入研究并实现了一套异步加载方案,结合加载动画,最终达到了流畅的视觉效果。

本文将完整分享这一实现过程,包括数据绑定、异步下载、线程安全、动画控制等关键技术点,希望能为有类似需求的开发提供参考。

运行效果

34f7987897a35c1f93906bbf3d9fe938_201209161902259073.gif

需求分析

项目要求以列表形式展示图片,这些图片来源包括:

  • 本地磁盘文件(file:// 协议)

  • 网络图片(http://https:// 协议)

直接使用 Image.Source 绑定URI会导致UI线程阻塞,尤其是网络图片加载缓慢时,界面会"卡住"。

因此,必须采用异步加载机制,并在加载过程中显示"加载中"动画,提升用户体验。

1、数据源与ViewModel

首先,我们准备一个文本文件 list.txt,每行存储一个图片地址,支持本地路径和URL:

http://img11.360buyimg.com//n3/g2/M00/06/1D/rBEGEVAkffUIAAAAAAB54F55qh8AABWrQLxLr0AAHn4106.jpg
C:\Users\Soar\Pictures\lovewallpaper\18451,106.jpg
http://img12.360buyimg.com//n3/g1/M00/06/1D/rBEGDVAkffQIAAAAAAB0mDavAccAABWrQMCUdwAAHSw197.jpg
C:\Users\Soar\Pictures\lovewallpaper\367448,106.jpg
...

接着创建 MainViewModel.cs,读取文件内容并暴露为 Images 属性:

using System;
using System.Collections.Generic;
using System.IO;

namespace WebImageList
{
    public class MainViewModel
    {
        public MainViewModel()
        {
            using (var sr = new StreamReader("list.txt"))
            {
                this._Images = new List<String>();
                while (!sr.EndOfStream)
                {
                    this._Images.Add(sr.ReadLine());
                }
            }
        }
        private List<String> _Images;

        public List<String> Images
        {
            get { return _Images; }
            set { _Images = value; }
        }
    }
}

2、加载中动画控件

WPF 不支持GIF动画,因此我们使用PNG图片配合旋转动画来实现"加载中"效果。

创建用户控件 WaitingProgress.xaml

<UserControl x:Class="WebImageList.WaitingProgress"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <Storyboard x:Key="waiting" Name="waiting">
            <DoubleAnimation Storyboard.TargetName="SpinnerRotate" Storyboard.TargetProperty="(RotateTransform.Angle)" From="0" To="359" Duration="0:0:02" RepeatBehavior="Forever" />
        </Storyboard>
    </UserControl.Resources>
    <Image Name="image" Source="loading.png" RenderTransformOrigin="0.5,0.5" Stretch="None" Loaded="Image_Loaded_1">
        <Image.RenderTransform>
            <RotateTransform x:Name="SpinnerRotate" Angle="0" />
        </Image.RenderTransform>
    </Image>
</UserControl>

后台代码 WaitingProgress.xaml.cs 在图片加载完成后启动动画,并提供 Stop() 方法隐藏控件:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;

namespace WebImageList
{
    public partial class WaitingProgress : UserControl
    {
        private Storyboard story;
        public WaitingProgress()
        {
            InitializeComponent();
            this.story = (base.Resources["waiting"] as Storyboard);
        }
        private void Image_Loaded_1(object sender, RoutedEventArgs e)
        {
            this.story.Begin(this.image, true);
        }
        public void Stop()
        {
            base.Dispatcher.BeginInvoke(new Action(() => {
                this.story.Pause(this.image);
                base.Visibility = System.Windows.Visibility.Collapsed;
            }));
        }
    }
}

3、异步图片下载队列

核心是 ImageQueue.cs,使用后台线程从队列中取出图片URL,异步下载并转换为 BitmapImage,通过事件通知UI线程更新。

using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Controls;

namespace WebImageList
{
    public static class ImageQueue
    {
        private class ImageQueueInfo
        {
            public Image image { get; set; }
            public String url { get; set; }
        }

        public delegate void ComplateDelegate(Image i, string u, BitmapImage b);
        public static event ComplateDelegate OnComplate;

        private static AutoResetEvent autoEvent;
        private static Queue<ImageQueueInfo> Stacks;

        static ImageQueue()
        {
            ImageQueue.Stacks = new Queue<ImageQueueInfo>();
            autoEvent = new AutoResetEvent(true);
            Thread t = new Thread(new ThreadStart(ImageQueue.DownloadImage));
            t.Name = "下载图片";
            t.IsBackground = true;
            t.Start();
        }

        private static void DownloadImage()
        {
            while (true)
            {
                ImageQueueInfo t = null;
                lock (ImageQueue.Stacks)
                {
                    if (ImageQueue.Stacks.Count > 0)
                    {
                        t = ImageQueue.Stacks.Dequeue();
                    }
                }
                if (t != null)
                {
                    Uri uri = new Uri(t.url);
                    BitmapImage image = null;
                    try
                    {
                        if ("http".Equals(uri.Scheme, StringComparison.CurrentCultureIgnoreCase))
                        {
                            WebClient wc = new WebClient();
                            using (var ms = new MemoryStream(wc.DownloadData(uri)))
                            {
                                image = new BitmapImage();
                                image.BeginInit();
                                image.CacheOption = BitmapCacheOption.OnLoad;
                                image.StreamSource = ms;
                                image.EndInit();
                            }
                        }
                        else if ("file".Equals(uri.Scheme, StringComparison.CurrentCultureIgnoreCase))
                        {
                            using (var fs = new FileStream(t.url, FileMode.Open))
                            {
                                image = new BitmapImage();
                                image.BeginInit();
                                image.CacheOption = BitmapCacheOption.OnLoad;
                                image.StreamSource = fs;
                                image.EndInit();
                            }
                        }
                        if (image != null && image.CanFreeze) image.Freeze();

                        t.image.Dispatcher.BeginInvoke(new Action<ImageQueueInfo, BitmapImage>((i, bmp) => 
                        {
                            if (ImageQueue.OnComplate != null)
                            {
                                ImageQueue.OnComplate(i.image, i.url, bmp);
                            }
                        }), new Object[] { t, image });
                    }
                    catch (Exception e)
                    {
                        MessageBox.Show(e.Message);
                        continue;
                    }
                }
                if (ImageQueue.Stacks.Count > 0) continue;
                autoEvent.WaitOne();
            }
        }

        public static void Queue(Image img, String url)
        {
            if (String.IsNullOrEmpty(url)) return;
            lock (ImageQueue.Stacks)
            {
                ImageQueue.Stacks.Enqueue(new ImageQueueInfo { url = url, image = img });
                ImageQueue.autoEvent.Set();
            }
        }
    }
}

4、主界面与数据绑定

MainWindow.xaml 中,使用 ListBox 展示图片,每个项包含 WaitingProgressImage 控件:

<Window x:Class="WebImageList.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WebImageList"
        Title="MainWindow" Height="600" Width="600" WindowStartupLocation="CenterScreen">
    <StackPanel>
        <Button Content="载入图片" Click="Button_Click_1"></Button>
        <ListBox ItemsSource="{Binding Images}" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <local:WaitingProgress/>
                        <Image Stretch="UniformToFill" Width="130" Height="130" local:ImageDecoder.Source="{Binding}"/>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel Name="wrapPanel" HorizontalAlignment="Stretch" />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
    </StackPanel>
</Window>

5、依赖属性实现绑定

ImageDecoder.cs 定义附加属性 Source,当绑定值变化时,将图片加入下载队列,并在下载完成后更新 Image.Source,同时播放淡入动画并隐藏加载控件:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;

namespace WebImageList
{
    public static class ImageDecoder
    {
        public static readonly DependencyProperty SourceProperty;

        public static string GetSource(Image image) => (string)image.GetValue(SourceProperty);
        public static void SetSource(Image image, string value) => image.SetValue(SourceProperty, value);

        static ImageDecoder()
        {
            SourceProperty = DependencyProperty.RegisterAttached("Source", typeof(string), typeof(ImageDecoder), 
                new PropertyMetadata(new PropertyChangedCallback(OnSourceWithSourceChanged)));
            ImageQueue.OnComplate += ImageQueue_OnComplete;
        }

        private static void ImageQueue_OnComplete(Image i, string u, BitmapImage b)
        {
            string source = GetSource(i);
            if (source == u)
            {
                i.Source = b;
                var storyboard = new Storyboard();
                var doubleAnimation = new DoubleAnimation(0.0, 1.0, new Duration(TimeSpan.FromMilliseconds(500.0)));
                Storyboard.SetTarget(doubleAnimation, i);
                Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Opacity"));
                storyboard.Children.Add(doubleAnimation);
                storyboard.Begin();

                if (i.Parent is Grid grid)
                {
                    foreach (var c in grid.Children)
                    {
                        if (c is WaitingProgress wp)
                        {
                            wp.Stop();
                            break;
                        }
                    }
                }
            }
        }

        private static void OnSourceWithSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ImageQueue.Queue((Image)o, (string)e.NewValue);
        }
    }
}

6、启动加载

MainWindow.xaml.cs 中设置 DataContext 并绑定按钮事件:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainViewModel();
    }

    private void Button_Click_1(object sender, RoutedEventArgs e)
    {
        // 数据已在ViewModel中加载,绑定自动触发
    }
}

总结

通过上述实现,我们成功解决了WPF中异步加载本地和网络图片的问题。

关键点包括:

  • 使用 Queue<T> 和后台线程管理图片下载任务,避免阻塞UI。

  • 利用 AutoResetEvent 实现线程同步,提高资源利用率。

  • 通过附加属性和事件机制,实现MVVM模式下的异步数据绑定。

  • 添加加载动画和淡入效果,显著提升用户体验。

整个方案稳定、高效,适用于图片列表、壁纸应用、资源管理器等场景。

下载地址:files.cnblogs.com/Soar1991/We…

关键词

WPF、异步加载、图片加载、加载动画、BitmapImage、后台线程、依赖属性、MVVM、WinForm、控件

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:Soar、毅

出处:cnblogs.com/Soar1991/archive/2012/09/16/WebImageList.html

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!