前言
在开发WPF应用程序时,图片的加载是一个常见但又容易被忽视的细节。尤其是在处理网络图片或大量本地图片时,如果直接绑定图片路径,界面很容易出现卡顿、假死,用户体验大打折扣。更理想的做法是:在图片加载完成前,显示一个"加载中"的动画,加载完成后平滑地展示图片,就像网页中的图片懒加载一样。
最近在做一个WPF项目,需求是以列表形式展示本地和网络图片。面对加载延迟问题,我深入研究并实现了一套异步加载方案,结合加载动画,最终达到了流畅的视觉效果。
本文将完整分享这一实现过程,包括数据绑定、异步下载、线程安全、动画控制等关键技术点,希望能为有类似需求的开发提供参考。
运行效果
需求分析
项目要求以列表形式展示图片,这些图片来源包括:
-
本地磁盘文件(
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 展示图片,每个项包含 WaitingProgress 和 Image 控件:
<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
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!