WPF自定义控件之图片控件 AsyncImage
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 = 0, Local = 1, Http = 2, Resources = 3 } public enum ImageType { Invalid = 0, Gif = 7173, Jpg = 255216, Png = 13780, Bmp = 6677 } /// <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, 0, 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 != 0) { 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(100); var metadataDelay = metadata.GetQueryOrDefault("/grctlext/Delay", 10); //显示时长 if (metadataDelay != 0) delay = TimeSpan.FromMilliseconds(metadataDelay * 10); var disposalMethod = (FrameDisposalMethod)metadata.GetQueryOrDefault("/grctlext/Disposal", 0); //显示方式 var frameMetadata = new FrameMetadata { Left = metadata.GetQueryOrDefault("/imgdesc/Left", 0), Top = metadata.GetQueryOrDefault("/imgdesc/Top", 0), 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="50" /> <local:AsyncImage UrlSource="{Binding Url}" LoadingText="正在加载图像请稍后"/>