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. C++之重载覆盖和隐藏

    继承体系下同名成员函数的三种关系 重载 在同一作用域内 函数名相同,参数列表不同(分三种情况:参数个数不同,参数类型不同,参数个数和类型都不同) 返回值类型可以相同也可以不同 重写(覆盖) 在不同作用 ...

  2. java超市购物管理系统

    一.概述 1.鹏哥前面有写过java项目超市管理系统项目,传送门 2.收到很多朋友私信给我,也很感谢老铁们的反馈和交流,前面这个项目只是对java基础知识和面向对象的思想练习,但是没有涉及到java如 ...

  3. Java 后台请求第三方系统接口详解

    //调用第三方系统接口 PrintWriter out = null; BufferedReader in = null; JSONObject jsonObject = null; Closeabl ...

  4. Linux系统:Centos7环境搭建Redis单台和哨兵集群环境

    本文源码:GitHub·点这里 || GitEE·点这里 一.环境和版本 Linux:centos7 三台 三台Linux服务 192.168.72.129 192.168.72.130 192.16 ...

  5. DockerFlie的基本命令解析

    DockerFlie官网文档:  https://docs.docker.com/engine/reference/builder/   DockerFile中文: http://www.docker ...

  6. Evaluation Warning : The document was created with Spire.PDF for .NET.

    由于使用  Spire.Pdf 生成的书签带有 Evaluation Warning : The document was created with Spire.PDF for .NET. 字样 但是 ...

  7. Python Web(二)

    Infi-chu: http://www.cnblogs.com/Infi-chu/ 一.Django-debug-toolbar django-debug-toolbar 是一组可配置的面板,可显示 ...

  8. dedecmsV5.7 arclist 如何调用副栏目的文章

    问题:用arclist 调用某个栏目下的文章的时候,发现无法调用出副栏目是这个栏目的文章. 然后就上百度搜了一番,记录一下我搜到的解决方法: 1.打开/include/taglib/arclist.l ...

  9. 文本处理工具awk

    目录 gawk:模式扫描和处理语言 awk语言 awk工作原理 print awk变量 自定义变量 printf命令 awk操作符 awk的条件判断 awk的循环 awk的数组 awk的函数 awk调 ...

  10. Rman Enhancements(增强) In Oracle 11g. (Doc ID 1115423.1)

    Rman Enhancements In Oracle 11g. (Doc ID 1115423.1) APPLIES TO: Oracle Database - Enterprise Edition ...