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

  • 相关阅读:
    【数据结构与算法】用go语言实现数组结构及其操作
    ElasticSearch搜索引擎
    【系统】Libevent库和Libev
    pod管理调度约束、与健康状态检查
    使用yaml配置文件管理资源
    Oracle中exists替代in语句
    【oracle】通过存储过程名查看package名
    解决Flink消费Kafka信息,结果存储在Mysql的重复消费问题
    利用Flink消费Kafka数据保证全局有序
    Oracle 字符集的查看和修改
  • 原文地址:https://www.cnblogs.com/ShenNan/p/10879084.html
Copyright © 2020-2023  润新知