AsyncImage 是一个封装完善,使用简便,功能齐全的WPF图片控件,比直接使用Image相对来说更加方便,但它的内部仍然使用Image承载图像,只不过在其基础上进行了一次完善成熟的封装

AsyncImage解决了以下问题
1) 异步加载及等待提示
2) 缓存
3) 支持读取多种形式的图片路径 (Local,Http,Resource)
4) 根据文件头识别准确的图片格式
5) 静态图支持设置解码大小
6) 支持GIF

AsyncImage的工作流程


开始创建

首先声明一个自定义控件

    public class AsyncImage : Control
{
static AsyncImage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(AsyncImage), new FrameworkPropertyMetadata(typeof(AsyncImage)));
ImageCacheList = new ConcurrentDictionary<string, ImageSource>(); //初始化静态图缓存字典
GifImageCacheList = new ConcurrentDictionary<string, ObjectAnimationUsingKeyFrames>(); //初始化gif缓存字典
}
}

声明成员

  #region DependencyProperty
public static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register("DecodePixelWidth",
typeof(double), typeof(AsyncImage), new PropertyMetadata(0.0)); public static readonly DependencyProperty LoadingTextProperty =
DependencyProperty.Register("LoadingText", typeof(string), typeof(AsyncImage), new PropertyMetadata("Loading")); public static readonly DependencyProperty IsLoadingProperty =
DependencyProperty.Register("IsLoading", typeof(bool), typeof(AsyncImage), new PropertyMetadata(false)); public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(AsyncImage)); public static readonly DependencyProperty UrlSourceProperty =
DependencyProperty.Register("UrlSource", typeof(string), typeof(AsyncImage), new PropertyMetadata(string.Empty, new PropertyChangedCallback((s, e) =>
{
var asyncImg = s as AsyncImage;
if (asyncImg.LoadEventFlag)
{
Console.WriteLine("Load By UrlSourceProperty Changed");
asyncImg.Load();
}
}))); public static readonly DependencyProperty IsCacheProperty = DependencyProperty.Register("IsCache", typeof(bool), typeof(AsyncImage), new PropertyMetadata(true)); public static readonly DependencyProperty StretchProperty = DependencyProperty.Register("Stretch", typeof(Stretch), typeof(AsyncImage), new PropertyMetadata(Stretch.Uniform)); public static readonly DependencyProperty CacheGroupProperty = DependencyProperty.Register("CacheGroup", typeof(string), typeof(AsyncImage), new PropertyMetadata("AsyncImage_Default"));
#endregion #region Property
/// <summary>
/// 本地路径正则
/// </summary>
private const string LocalRegex = @"^([C-J]):\\([^:&]+\\)*([^:&]+).(jpg|jpeg|png|gif)$"; /// <summary>
/// 网络路径正则
/// </summary>
private const string HttpRegex = @"^(https|http):\/\/[^*+@!]+$"; private Image _image; /// <summary>
/// 是否允许加载图像
/// </summary>
private bool LoadEventFlag; /// <summary>
/// 静态图缓存
/// </summary>
private static IDictionary<string, ImageSource> ImageCacheList; /// <summary>
/// 动态图缓存
/// </summary>
private static IDictionary<string, ObjectAnimationUsingKeyFrames> GifImageCacheList; /// <summary>
/// 动画播放控制类
/// </summary>
private ImageAnimationController gifController; /// <summary>
/// 解码宽度
/// </summary>
public double DecodePixelWidth
{
get { return (double)GetValue(DecodePixelWidthProperty); }
set { SetValue(DecodePixelWidthProperty, value); }
} /// <summary>
/// 异步加载时的文字提醒
/// </summary>
public string LoadingText
{
get { return GetValue(LoadingTextProperty) as string; }
set { SetValue(LoadingTextProperty, value); }
} /// <summary>
/// 加载状态
/// </summary>
public bool IsLoading
{
get { return (bool)GetValue(IsLoadingProperty); }
set { SetValue(IsLoadingProperty, value); }
} /// <summary>
/// 图片路径
/// </summary>
public string UrlSource
{
get { return GetValue(UrlSourceProperty) as string; }
set { SetValue(UrlSourceProperty, value); }
} /// <summary>
/// 图像源
/// </summary>
public ImageSource ImageSource
{
get { return GetValue(ImageSourceProperty) as ImageSource; }
set { SetValue(ImageSourceProperty, value); }
} /// <summary>
/// 是否启用缓存
/// </summary> public bool IsCache
{
get { return (bool)GetValue(IsCacheProperty); }
set { SetValue(IsCacheProperty, value); }
} /// <summary>
/// 图像填充类型
/// </summary>
public Stretch Stretch
{
get { return (Stretch)GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
} /// <summary>
/// 缓存分组标识
/// </summary>
public string CacheGroup
{
get { return GetValue(CacheGroupProperty) as string; }
set { SetValue(CacheGroupProperty, value); }
}
#endregion

需要注意的是,当UrlSource发生改变时,也许AsyncImage本身并未加载完成,这个时候获取模板中的Image对象是获取不到的,所以要在其PropertyChanged事件中判断一下load状态,已经load过才能触发加载,否则就等待控件的load事件执行之后再加载

       public static readonly DependencyProperty UrlSourceProperty =
DependencyProperty.Register("UrlSource", typeof(string), typeof(AsyncImage), new PropertyMetadata(string.Empty, new PropertyChangedCallback((s, e) =>
{
var asyncImg = s as AsyncImage;
if (asyncImg.LoadEventFlag) //判断控件自身加载状态
{
Console.WriteLine("Load By UrlSourceProperty Changed");
asyncImg.Load();
}
}))); private void AsyncImage_Loaded(object sender, RoutedEventArgs e)
{
_image = this.GetTemplateChild("image") as Image; //获取模板中的Image
Console.WriteLine("Load By LoadedEvent");
this.Load();
this.LoadEventFlag = true; //设置控件加载状态
}
        private void Load()
{
if (_image == null)
return; Reset();
var url = this.UrlSource;
if (!string.IsNullOrEmpty(url))
{
var pixelWidth = (int)this.DecodePixelWidth;
var isCache = this.IsCache;
var cacheKey = string.Format("{0}_{1}", CacheGroup, url);
this.IsLoading = !ImageCacheList.ContainsKey(cacheKey) && !GifImageCacheList.ContainsKey(cacheKey); Task.Factory.StartNew(() =>
{
#region 读取缓存
if (ImageCacheList.ContainsKey(cacheKey))
{
this.SetSource(ImageCacheList[cacheKey]);
return;
}
else if (GifImageCacheList.ContainsKey(cacheKey))
{
this.Dispatcher.BeginInvoke((Action)delegate
{
var animation = GifImageCacheList[cacheKey];
PlayGif(animation);
});
return;
}
#endregion #region 解析路径类型
var pathType = ValidatePathType(url);
Console.WriteLine(pathType);
if (pathType == PathType.Invalid)
{
Console.WriteLine("invalid path");
return;
}
#endregion #region 读取图片字节
byte[] imgBytes = null;
Stopwatch sw = new Stopwatch();
sw.Start();
if (pathType == PathType.Local)
imgBytes = LoadFromLocal(url);
else if (pathType == PathType.Http)
imgBytes = LoadFromHttp(url);
else if (pathType == PathType.Resources)
imgBytes = LoadFromApplicationResource(url);
sw.Stop();
Console.WriteLine("read time : {0}", sw.ElapsedMilliseconds); if (imgBytes == null)
{
Console.WriteLine("imgBytes is null,can't load the image");
return;
}
#endregion #region 读取文件类型
var imgType = GetImageType(imgBytes);
if (imgType == ImageType.Invalid)
{
imgBytes = null;
Console.WriteLine("无效的图片文件");
return;
}
Console.WriteLine(imgType);
#endregion #region 加载图像
if (imgType != ImageType.Gif)
{
//加载静态图像
var imgSource = LoadStaticImage(cacheKey, imgBytes, pixelWidth, isCache);
this.SetSource(imgSource);
}
else
{
//加载gif图像
this.Dispatcher.BeginInvoke((Action)delegate
{
var animation = LoadGifImageAnimation(cacheKey, imgBytes, isCache);
PlayGif(animation);
});
}
#endregion }).ContinueWith(r =>
{
this.Dispatcher.BeginInvoke((Action)delegate
{
this.IsLoading = false;
});
});
}
}

判断路径,判断文件格式,读取图片字节

    public enum PathType
{
Invalid = , Local = , Http = , Resources =
} public enum ImageType
{
Invalid = , Gif = , Jpg = , Png = , Bmp =
} /// <summary>
/// 验证路径类型
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
private PathType ValidatePathType(string path)
{
if (path.StartsWith("pack://"))
return PathType.Resources;
else if (Regex.IsMatch(path, AsyncImage.LocalRegex, RegexOptions.IgnoreCase))
return PathType.Local;
else if (Regex.IsMatch(path, AsyncImage.HttpRegex, RegexOptions.IgnoreCase))
return PathType.Http;
else
return PathType.Invalid;
} /// <summary>
/// 根据文件头判断格式图片
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
private ImageType GetImageType(byte[] bytes)
{
var type = ImageType.Invalid;
try
{
var fileHead = Convert.ToInt32($"{bytes[0]}{bytes[1]}");
if (!Enum.IsDefined(typeof(ImageType), fileHead))
{
type = ImageType.Invalid;
Console.WriteLine($"获取图片类型失败 fileHead:{fileHead}");
}
else
{
type = (ImageType)fileHead;
}
}
catch (Exception ex)
{
type = ImageType.Invalid;
Console.WriteLine($"获取图片类型失败 {ex.Message}");
}
return type;
} private byte[] LoadFromHttp(string url)
{
try
{
using (WebClient wc = new WebClient() { Proxy = null })
{
return wc.DownloadData(url);
}
}
catch (Exception ex)
{
Console.WriteLine("network error:{0} url:{1}", ex.Message, url);
}
return null;
} private byte[] LoadFromLocal(string path)
{
if (!System.IO.File.Exists(path))
{
return null;
}
try
{
return System.IO.File.ReadAllBytes(path);
}
catch (Exception ex)
{
Console.WriteLine("Read Local Failed : {0}", ex.Message);
return null;
}
} private byte[] LoadFromApplicationResource(string path)
{
try
{
StreamResourceInfo streamInfo = Application.GetResourceStream(new Uri(path, UriKind.RelativeOrAbsolute));
if (streamInfo.Stream.CanRead)
{
using (streamInfo.Stream)
{
var bytes = new byte[streamInfo.Stream.Length];
streamInfo.Stream.Read(bytes, , bytes.Length);
return bytes;
}
}
}
catch (Exception ex)
{
Console.WriteLine("Read Resource Failed : {0}", ex.Message);
return null;
}
return null;
}

加载静态图

        /// <summary>
/// 加载静态图像
/// </summary>
/// <param name="cacheKey"></param>
/// <param name="imgBytes"></param>
/// <param name="pixelWidth"></param>
/// <param name="isCache"></param>
/// <returns></returns>
private ImageSource LoadStaticImage(string cacheKey, byte[] imgBytes, int pixelWidth, bool isCache)
{
if (ImageCacheList.ContainsKey(cacheKey))
return ImageCacheList[cacheKey];
var bit = new BitmapImage() { CacheOption = BitmapCacheOption.OnLoad };
bit.BeginInit();
if (pixelWidth != )
{
bit.DecodePixelWidth = pixelWidth; //设置解码大小
}
bit.StreamSource = new System.IO.MemoryStream(imgBytes);
bit.EndInit();
bit.Freeze();
try
{
if (isCache && !ImageCacheList.ContainsKey(cacheKey))
ImageCacheList.Add(cacheKey, bit);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return bit;
}
return bit;
}

关于GIF解析

博客园上的周银辉老师也做过Image支持GIF的功能,但我个人认为他的解析GIF部分代码不太友好,由于直接操作文件字节,导致如果阅读者没有研究过gif的文件格式,将晦涩难懂。几经周折我找到github上一个大神写的成熟的WPF播放GIF项目,源码参考https://github.com/XamlAnimatedGif/WpfAnimatedGif

解析GIF的核心代码,从图片帧的元数据中使用路径表达式获取当前帧的详细信息 (大小/边距/显示时长/显示方式)

        /// <summary>
/// 解析帧详细信息
/// </summary>
/// <param name="frame">当前帧</param>
/// <returns></returns>
private static FrameMetadata GetFrameMetadata(BitmapFrame frame)
{
var metadata = (BitmapMetadata)frame.Metadata;
var delay = TimeSpan.FromMilliseconds();
var metadataDelay = metadata.GetQueryOrDefault("/grctlext/Delay", ); //显示时长
if (metadataDelay != )
delay = TimeSpan.FromMilliseconds(metadataDelay * );
var disposalMethod = (FrameDisposalMethod)metadata.GetQueryOrDefault("/grctlext/Disposal", ); //显示方式
var frameMetadata = new FrameMetadata
{
Left = metadata.GetQueryOrDefault("/imgdesc/Left", ),
Top = metadata.GetQueryOrDefault("/imgdesc/Top", ),
Width = metadata.GetQueryOrDefault("/imgdesc/Width", frame.PixelWidth),
Height = metadata.GetQueryOrDefault("/imgdesc/Height", frame.PixelHeight),
Delay = delay,
DisposalMethod = disposalMethod
};
return frameMetadata;
}

创建WPF动画播放对象

        /// <summary>
/// 加载Gif图像动画
/// </summary>
/// <param name="cacheKey"></param>
/// <param name="imgBytes"></param>
/// <param name="pixelWidth"></param>
/// <param name="isCache"></param>
/// <returns></returns>
private ObjectAnimationUsingKeyFrames LoadGifImageAnimation(string cacheKey, byte[] imgBytes, bool isCache)
{
var gifInfo = GifParser.Parse(imgBytes);
var animation = new ObjectAnimationUsingKeyFrames();
foreach (var frame in gifInfo.FrameList)
{
var keyFrame = new DiscreteObjectKeyFrame(frame.Source, frame.Delay);
animation.KeyFrames.Add(keyFrame);
}
animation.Duration = gifInfo.TotalDelay;
animation.RepeatBehavior = RepeatBehavior.Forever;
//animation.RepeatBehavior = new RepeatBehavior(3);
if (isCache && !GifImageCacheList.ContainsKey(cacheKey))
{
GifImageCacheList.Add(cacheKey, animation);
}
return animation;
}

GIF动画的播放

创建动画控制器ImageAnimationController,使用动画时钟控制器AnimationClock ,为控制器指定需要作用的控件属性

        private readonly Image _image;
private readonly ObjectAnimationUsingKeyFrames _animation;
private readonly AnimationClock _clock;
private readonly ClockController _clockController; public ImageAnimationController(Image image, ObjectAnimationUsingKeyFrames animation, bool autoStart)
{
_image = image;
try
{
_animation = animation;
//_animation.Completed += AnimationCompleted;
_clock = _animation.CreateClock();
_clockController = _clock.Controller;
_sourceDescriptor.AddValueChanged(image, ImageSourceChanged); // ReSharper disable once PossibleNullReferenceException
_clockController.Pause(); //暂停动画 _image.ApplyAnimationClock(Image.SourceProperty, _clock); //将动画作用于该控件的指定属性 if (autoStart)
_clockController.Resume(); //播放动画
}
catch (Exception)
{ } }

定义外观

<Style TargetType="{x:Type local:AsyncImage}">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:AsyncImage}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<Grid>
<Image x:Name="image"
Stretch="{TemplateBinding Stretch}"
RenderOptions.BitmapScalingMode="HighQuality"/>
<TextBlock Text="{TemplateBinding LoadingText}"
FontSize="{TemplateBinding FontSize}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
x:Name="txtLoading"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsLoading" Value="False">
<Setter Property="Visibility" Value="Collapsed" TargetName="txtLoading"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

调用示例

   <local:AsyncImage UrlSource="{Binding Url}"/>
<local:AsyncImage UrlSource="{Binding Url}" IsCache="False"/>
<local:AsyncImage UrlSource="{Binding Url}" DecodePixelWidth="" />
<local:AsyncImage UrlSource="{Binding Url}" LoadingText="正在加载图像请稍后"/>

WPF自定义控件之图片控件 AsyncImage的更多相关文章

  1. 解决WPF两个图片控件显示相同图片因线程占用,其中一个显示不全的问题

    在做项目的过程中遇到这样一个问题,下面提出一种解决方法,主要思想是图片的Copy,如还有其他方法,欢迎交流. 在前端图片控件绑定显示时,使用转换器进行转义绑定   (1)转换器: public cla ...

  2. WPF自定义控件二:Border控件与TextBlock控件轮播动画

    需求:实现Border轮播动画与TextBlock动画 XAML代码如下: <Window.Resources> <Storyboard x:Key="OnLoaded1& ...

  3. WPF 后台C#设置控件背景图片

    原文:WPF 后台C#设置控件背景图片 以前的程序中有做过,当时只是记得uri很长一大段就没怎么记.今天有人问了也就写下来.   这是一个Button,设置了Background后的效果. 前台的设置 ...

  4. 【WPF/C#】拖拽Image图片控件

    需求:使得Image图片控件能够被拖动. 思路:关键是重写Image控件的几个鼠标事件,实现控制. 前台: <Image Source="C:\Users\Administrator\ ...

  5. WPF后台设置xaml控件的样式System.Windows.Style

    WPF后台设置xaml控件的样式System.Windows.Style 摘-自 :感谢 作者: IT小兵   http://3w.suchso.com/projecteac-tual/wpf-zhi ...

  6. WPF中的image控件的Source赋值

    WPF中的Image控件Source的设置 1.XAML中 简单的方式(Source="haha.png"); image控件的Source设置为相对路径后(Source=&quo ...

  7. Android自定义控件1--自定义控件介绍

    Android控件基本介绍 Android本身提供了很多控件比如我们常用的有文本控件TextView和EditText:按钮控件Button和ImageButton状态开关按钮ToggleButton ...

  8. WPF Step By Step 控件介绍

    WPF Step By Step 控件介绍 回顾 上一篇,我们主要讨论了WPF的几个重点的基本知识的介绍,本篇,我们将会简单的介绍几个基本控件的简单用法,本文会举几个项目中的具体的例子,结合这些 例子 ...

  9. WPF编程,将控件所呈现的内容保存成图像的一种方法。

    原文:WPF编程,将控件所呈现的内容保存成图像的一种方法. 版权声明:我不生产代码,我只是代码的搬运工. https://blog.csdn.net/qq_43307934/article/detai ...

随机推荐

  1. 花了三个月终于把所有的 Python 库全部整理了!可以说很全面了

    库名称简介 Chardet字符编码探测器,可以自动检测文本.网页.xml的编码. colorama主要用来给文本添加各种颜色,并且非常简单易用. Prettytable主要用于在终端或浏览器端构建格式 ...

  2. python中list的运算,操作及实例

    在操作list的时候,经常用到对列表的操作运算,比如说,列表添加,删除操作,其实,这里面经常回遇到这样一个问题,就是列表的操作容易被混淆了. 有人做了一个总结,这个很清晰,我就不多做阐述了: 1.ap ...

  3. 使用脚本进行ansible批量主机的免密配置

    应用场景: 在应用ansible的实际情况中,有一个很现实的问题,ansible是需要对主机做ssh免密登陆的,而挨个对主机做免密是非常的繁琐的,挨个敲IP不仅非常的繁琐而且容易出错,为解决这个问题, ...

  4. 经验之谈-switch结构常见错误的分析与处理

    1.缺少break语句 本来只想输出“出任武林盟主”可输出结果为 错误分析:在 switch结构中,每一个case语句块后面如果不写 break语句, switch就会 直接往下面的case语句块运行 ...

  5. sublime插件开发教程4

    写几个简单的例子详解下 import sublime import sublime_plugin class ExampleCommand(sublime_plugin.TextCommand): d ...

  6. 巧妙利用label标签实现input file上传文件自定义样式

    提到上传文件,一般会想到用input file属性来实现,简单便捷,一行代码即可    但input file原生提供的默认样式大多情况下都不符合需求,且在不同浏览器上呈现的样式也不尽相同   我们往 ...

  7. 隐藏Nginx软件版本号信息

    为了提高我们web服务器的安全性,我们应当尽可能的隐藏服务器的信息以防止他人通过这些信息找到漏洞侵入我们的服务器,对于Nginx而言,我们安装好Nginx后最好隐藏Nginx的版本号,以防止通过该版本 ...

  8. IDEA中安装EasyCode插件并连接数据库生成代码

    场景 EasyCode是基于IntelliJ IDEA开发的代码生成插件,支持自定义任意模板(Java,html,js,xml).只要是与数据库相关的代码都可以通过自定义模板来生成.支持数据库类型与j ...

  9. kvm2

    kvm虚拟机的桥接网络 默认的虚拟机网络是NAT模式,网段192.168.122.0/24 1:创建桥接网卡 创建桥接网卡命令 virsh iface-bridge eth0 br0 取消桥接网卡命令 ...

  10. FCC---Create a Graphic Using CSS---新月图形

    By manipulating different selectors and properties, you can make interesting shapes. One of the easi ...