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="正在加载图像请稍后"/>

 

posted on 2019-06-02 18:38  #山鸡  阅读(3414)  评论(5编辑  收藏  举报

导航