WPF自定義控件之圖片控件 AsyncImage

AsyncImage 是一個封裝完善,使用簡便,功能齊全的WPF圖片控件,比直接使用Image相對來講更加方便,但它的內部仍然使用Image承載圖像,只不過在其基礎上進行了一次完善成熟的封裝git

AsyncImage解決了如下問題
1) 異步加載及等待提示
2) 緩存
3) 支持讀取多種形式的圖片路徑 (Local,Http,Resource)
4) 根據文件頭識別準確的圖片格式
5) 靜態圖支持設置解碼大小
6) 支持GIFgithub

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緩存字典
        }
    }

 

聲明成員async

  #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事件執行以後再加載ide

       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;
        }

 

加載靜態圖this

        /// <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解析url

博客園上的周銀輝老師也作過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="正在加載圖像請稍後"/>

 

相關文章
相關標籤/搜索