using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Http; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Schedulers; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Resources; using dw = System.Drawing; namespace SJ.Controls { public class BAsyncImage : Control { #region DependencyProperty public static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register("DecodePixelWidth", typeof(double), typeof(BAsyncImage), new PropertyMetadata(0.0)); public static readonly DependencyProperty LoadingTextProperty = DependencyProperty.Register("LoadingText", typeof(string), typeof(BAsyncImage), new PropertyMetadata("Loading...")); public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register("IsLoading", typeof(bool), typeof(BAsyncImage), new PropertyMetadata(false)); public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(BAsyncImage)); public static readonly DependencyProperty DefaultUrlSourceProperty = DependencyProperty.Register("DefaultUrlSource", typeof(string), typeof(BAsyncImage), new PropertyMetadata("")); public static readonly DependencyProperty UrlSourceProperty = DependencyProperty.Register("UrlSource", typeof(string), typeof(BAsyncImage), new PropertyMetadata(string.Empty, new PropertyChangedCallback((s, e) => { var asyncImg = s as BAsyncImage; if (asyncImg.LoadEventFlag) { asyncImg.Load(); } }))); public static readonly DependencyProperty FailUrlSourceProperty = DependencyProperty.Register("FailUrlSource", typeof(string), typeof(BAsyncImage), new PropertyMetadata(string.Empty)); public static readonly DependencyProperty IsCacheProperty = DependencyProperty.Register("IsCache", typeof(bool), typeof(BAsyncImage), new PropertyMetadata(true)); public static readonly DependencyProperty StretchProperty = DependencyProperty.Register("Stretch", typeof(Stretch), typeof(BAsyncImage), new PropertyMetadata(Stretch.Uniform)); public static readonly DependencyProperty CacheGroupProperty = DependencyProperty.Register("CacheGroup", typeof(string), typeof(BAsyncImage), new PropertyMetadata("QLAsyncImage_Default")); #endregion #region Property private static readonly HttpClient httpClient = new HttpClient(); public double StaticImageActualPixelWidth { get; set; } = 0; public double StaticImageActualPixelHeight { get; set; } = 0; public const string LocalRegex = @"^([C-J]):\\([^:&]+\\)*([^:&]+).(jpg|jpeg|png|gif)$"; public const string HttpRegex = @"^((https|http):\/\/)?([^\\*+@]+)$"; private Image _image; private dw.Bitmap gifBitmap; private bool LoadEventFlag = false; private static ConcurrentDictionary ImageCacheList; private static ConcurrentDictionary GifImageCacheList; private static LimitedConcurrencyLevelTaskScheduler httpTaskScheduler; public double DecodePixelWidth { get { return (double)GetValue(DecodePixelWidthProperty); } set { SetValue(DecodePixelWidthProperty, value); } } public string LoadingText { get { return GetValue(LoadingTextProperty) as string; } set { SetValue(LoadingTextProperty, value); } } public bool IsLoading { get { return (bool)GetValue(IsLoadingProperty); } set { SetValue(IsLoadingProperty, value); } } public string UrlSource { get { return GetValue(UrlSourceProperty) as string; } set { SetValue(UrlSourceProperty, value); } } /// /// 仅限Resourcesl路径 /// public string FailUrlSource { get { return GetValue(FailUrlSourceProperty) as string; } set { SetValue(FailUrlSourceProperty, value); } } public ImageSource ImageSource { get { return GetValue(ImageSourceProperty) as ImageSource; } set { SetValue(ImageSourceProperty, value); } } public bool IsCache { get { return (bool)GetValue(IsCacheProperty); } set { SetValue(IsCacheProperty, value); } } public Stretch Stretch { get { return (Stretch)GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } public string CacheGroup { get { return GetValue(CacheGroupProperty) as string; } set { SetValue(CacheGroupProperty, value); } } #endregion #region RouteEvent public delegate void QLAsyncImageLoadCompleteHandler(object sender, QLAsyncImageLoadCompleteEventArgs e); public static readonly RoutedEvent OnLoadCompleteEvent = EventManager.RegisterRoutedEvent("OnLoadComplete", RoutingStrategy.Bubble, typeof(QLAsyncImageLoadCompleteHandler), typeof(BAsyncImage)); public event QLAsyncImageLoadCompleteHandler OnLoadComplete { add { AddHandler(OnLoadCompleteEvent, value); } remove { RemoveHandler(OnLoadCompleteEvent, value); } } #endregion #region Method static BAsyncImage() { DefaultStyleKeyProperty.OverrideMetadata(typeof(BAsyncImage), new FrameworkPropertyMetadata(typeof(BAsyncImage))); ImageCacheList = new ConcurrentDictionary(); GifImageCacheList = new ConcurrentDictionary(); httpTaskScheduler = new LimitedConcurrencyLevelTaskScheduler(10); } public BAsyncImage() { Loaded += QLAsyncImage_Loaded; Unloaded += QLAsyncImage_Unloaded; } private void QLAsyncImage_Unloaded(object sender, RoutedEventArgs e) { Reset(); LoadEventFlag = false; } private void QLAsyncImage_Loaded(object sender, RoutedEventArgs e) { Init(); } public override void OnApplyTemplate() { Init(); } private void Init([CallerMemberName] string eventName = "") { if (LoadEventFlag) return; _image = GetTemplateChild("image") as Image; if (_image == null) return; LoadEventFlag = true; Load(); } /// /// Delete local bitmap resource /// Reference: http://msdn.microsoft.com/en-us/library/dd183539(VS.85).aspx /// [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool DeleteObject(IntPtr hObject); /// /// 重置图像 /// private void Reset() { //停止播放Gif动画 if (gifBitmap != null) StopGif(); SetSource(null); } private void Load() { if (_image == null) return; Reset(); if (!string.IsNullOrEmpty(UrlSource)) { var url = UrlSource; var failUrl = FailUrlSource; var pixelWidth = (int)DecodePixelWidth; var isCache = IsCache; var cacheKey = string.Format("{0}_{1}", CacheGroup, url); IsLoading = !ImageCacheList.ContainsKey(cacheKey) && !GifImageCacheList.ContainsKey(cacheKey); #region 读取缓存 if (ImageCacheList.ContainsKey(cacheKey)) { var source = ImageCacheList[cacheKey]; SetSource(source); SetStaticImageActualPixelSize(source.Width, source.Height); LoadComplete(string.Empty, url); return; } else if (GifImageCacheList.ContainsKey(cacheKey)) { PlayGif(GifImageCacheList[cacheKey]); LoadComplete(string.Empty, url); return; } #endregion this.Load(url, failUrl, cacheKey, isCache, pixelWidth); } } private void Load(string url, string failUrl, string cacheKey, bool isCache, int pixelWidth) { var errorMessage = string.Empty; //解析路径类型 var pathType = ValidatePathType(url); if (pathType == PathType.Invalid) { LoadFail(failUrl); LoadComplete(errorMessage, url); return; } if (pathType == PathType.Http) { ////先加载默认图 //if (!string.IsNullOrEmpty(defaultUrl)) // LoadLocal(defaultUrl, failUrl, PathType.Resources, defaultUrl, true, 0, excuteComplete: false); //默认图不触发加载完毕事件 LoadHttp(url, failUrl, cacheKey, isCache, pixelWidth); } else LoadLocal(url, failUrl, pathType, cacheKey, isCache, pixelWidth); } /// /// 加载失败图像 /// /// private void LoadFail(string failUrl) { byte[] imgBytes = null; string errorMessage = string.Empty; var pathType = ValidatePathType(failUrl); if (pathType == PathType.Invalid) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"LoadFail 无效的路径 {failUrl}"); Console.ResetColor(); return; } if (pathType == PathType.Local) imgBytes = LoadBytesFromLocal(failUrl, out errorMessage); else if (pathType == PathType.Resources) imgBytes = LoadBytesFromApplicationResource(failUrl, out errorMessage); if (string.IsNullOrEmpty(errorMessage) && imgBytes != null) AnalysisBytes(imgBytes, failUrl, true, 0, out errorMessage); if (string.IsNullOrEmpty(errorMessage)) return; Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"LoadFail {errorMessage} {failUrl}"); Console.ResetColor(); } private void LoadLocal(string url, string failUrl, PathType pathType, string cacheKey, bool isCache, int pixelWidth, bool excuteComplete = true) { byte[] imgBytes = null; var errorMessage = string.Empty; if (pathType == PathType.Local) imgBytes = LoadBytesFromLocal(url, out errorMessage); else if (pathType == PathType.Resources) imgBytes = LoadBytesFromApplicationResource(url, out errorMessage); if (string.IsNullOrEmpty(errorMessage) && imgBytes != null) AnalysisBytes(imgBytes, cacheKey, isCache, pixelWidth, out errorMessage); if (!string.IsNullOrEmpty(errorMessage)) { LoadFail(failUrl); Console.ForegroundColor = ConsoleColor.Red; } Console.WriteLine($"LoadLocal {errorMessage} {url}"); Console.ResetColor(); if (excuteComplete) LoadComplete(errorMessage, url); return; } private void LoadHttp(string url, string failUrl, string cacheKey, bool isCache, int pixelWidth) { Task.Factory.StartNew(() => { //Thread.Sleep(2000); Console.WriteLine($"LoadHttp Start {url}"); var errorMessage = string.Empty; var imgBytes = LoadBytesFromHttp(url, out errorMessage); if (string.IsNullOrEmpty(errorMessage) && imgBytes != null) AnalysisBytes(imgBytes, cacheKey, isCache, pixelWidth, out errorMessage); if (!string.IsNullOrEmpty(errorMessage)) { LoadFail(failUrl); Console.ForegroundColor = ConsoleColor.Red; } Console.WriteLine($"LoadHttp Completed {errorMessage} {url}"); Console.ResetColor(); LoadComplete(errorMessage, url); return; }, CancellationToken.None, TaskCreationOptions.None, httpTaskScheduler); } private void AnalysisBytes(byte[] imgBytes, string cacheKey, bool isCache, int pixelWidth, out string errorMessage) { errorMessage = string.Empty; #region 读取文件类型 var imgType = GetImageType(imgBytes); if (imgType == ImageType.Invalid) { imgBytes = null; errorMessage = "Invalid ImageFile"; return; } #endregion #region 加载图像 if (imgType != ImageType.Gif) { //加载静态图像 var imgSource = LoadStaticImage(cacheKey, imgBytes, pixelWidth, isCache, out errorMessage); if (imgSource == null) return; SetStaticImageActualPixelSize(imgSource.Width, imgSource.Height); SetSource(imgSource); } else { var frameCount = 0; using (var memoryStream = new System.IO.MemoryStream(imgBytes)) { //读取gif帧数 var decoder = BitmapDecoder.Create(memoryStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad); frameCount = decoder.Frames.Count; decoder = null; } if (frameCount > 1) { CacheGifBytes(cacheKey, imgBytes, isCache); PlayGif(imgBytes); } else { //gif只有1帧,视为静态图处理 var imgSource = LoadStaticImage(cacheKey, imgBytes, pixelWidth, isCache, out errorMessage); if (imgSource == null) return; SetStaticImageActualPixelSize(imgSource.Width, imgSource.Height); SetSource(imgSource); } } #endregion } /// /// 加载静态图像 /// /// /// /// /// /// private ImageSource LoadStaticImage(string cacheKey, byte[] imgBytes, int pixelWidth, bool isCache, out string errorMessage) { errorMessage = string.Empty; if (ImageCacheList.ContainsKey(cacheKey)) return ImageCacheList[cacheKey]; var bit = new BitmapImage() { CacheOption = BitmapCacheOption.OnLoad }; try { bit.BeginInit(); if (pixelWidth != 0) { bit.DecodePixelWidth = pixelWidth; } bit.StreamSource = new System.IO.MemoryStream(imgBytes); bit.EndInit(); bit.Freeze(); if (isCache && !ImageCacheList.ContainsKey(cacheKey)) ImageCacheList.TryAdd(cacheKey, bit); } catch (Exception ex) { errorMessage = $"LoadStaticImage Error {ex.Message}"; bit = null; } return bit; } /// /// 加载Gif图像动画 /// /// /// /// /// /// private void CacheGifBytes(string cacheKey, byte[] imgBytes, bool isCache) { if (isCache && !GifImageCacheList.ContainsKey(cacheKey)) GifImageCacheList.TryAdd(cacheKey, imgBytes); } private byte[] LoadBytesFromHttp(string url, out string errorMessage) { errorMessage = string.Empty; try { return httpClient.GetByteArrayAsync(url).Result; } catch (Exception ex) { errorMessage = $"Dowdload Error {ex.Message}"; } return null; } private byte[] LoadBytesFromLocal(string path, out string errorMessage) { errorMessage = string.Empty; if (!System.IO.File.Exists(path)) { errorMessage = "File No Exists"; return null; } try { return System.IO.File.ReadAllBytes(path); } catch (Exception ex) { errorMessage = $"Load Local Error {ex.Message}"; return null; } } private byte[] LoadBytesFromApplicationResource(string path, out string errorMessage) { errorMessage = string.Empty; 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) { errorMessage = $"Load Resource Error {ex.Message}"; return null; } return null; } private void SetSource(ImageSource source) { Dispatcher.BeginInvoke((Action)delegate { if (_image != null) _image.Source = source; }); } /// /// 更新图像实际像素 /// /// private void SetStaticImageActualPixelSize(double pixelWidth, double pixelHeight) { Dispatcher.Invoke(() => { StaticImageActualPixelWidth = pixelWidth; StaticImageActualPixelHeight = pixelHeight; }); } private void PlayGif(byte[] imgBytes) { gifBitmap = new dw.Bitmap(new System.IO.MemoryStream(imgBytes)); if (dw.ImageAnimator.CanAnimate(gifBitmap)) { SetStaticImageActualPixelSize(gifBitmap.Width, gifBitmap.Height); dw.ImageAnimator.Animate(gifBitmap, OnGifFrameChanged); } else { gifBitmap.Dispose(); } } private void StopGif() { dw.ImageAnimator.StopAnimate(gifBitmap, OnGifFrameChanged); gifBitmap.Dispose(); } private void OnGifFrameChanged(object sender, EventArgs e) { dw.ImageAnimator.UpdateFrames(); var currentFrameImageSource = GetBitmapSource(); if (currentFrameImageSource != null) currentFrameImageSource.Freeze(); SetSource(currentFrameImageSource); } /// /// 加载完成 /// /// /// private void LoadComplete(string errorMessage, string url) { this.Dispatcher.BeginInvoke((Action)delegate { IsLoading = false; var args = new QLAsyncImageLoadCompleteEventArgs(OnLoadCompleteEvent, this) { IsSuccess = string.IsNullOrEmpty(errorMessage), UrlSource = url, ErrorMessage = errorMessage, StaticImageActualPixelWidth = StaticImageActualPixelWidth, StaticImageActualPixelHeight = StaticImageActualPixelHeight }; this.RaiseEvent(args); }); } /// /// 从System.Drawing.Bitmap中获得当前帧图像的BitmapSource /// /// private ImageSource GetBitmapSource() { IntPtr handle = IntPtr.Zero; try { handle = gifBitmap.GetHbitmap(); return Imaging.CreateBitmapSourceFromHBitmap(handle, IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } catch { return null; } finally { if (handle != IntPtr.Zero) { DeleteObject(handle); } } } public void Refresh() { Load(); } private PathType ValidatePathType(string path) { if (path.StartsWith("pack://")) return PathType.Resources; else if (Regex.IsMatch(path, BAsyncImage.LocalRegex, RegexOptions.IgnoreCase)) return PathType.Local; else if (Regex.IsMatch(path, BAsyncImage.HttpRegex, RegexOptions.IgnoreCase)) return PathType.Http; else return PathType.Invalid; } 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; } else { type = (ImageType)fileHead; } } catch (Exception ex) { type = ImageType.Invalid; Console.WriteLine($"获取图片类型失败 {ex.Message}"); } return type; } /// /// 清楚缓存 /// /// 缓存Key,格式: CacheGroup_UrlSource public static void ClearCache(string cacheKey = "") { if (string.IsNullOrEmpty(cacheKey)) { ImageCacheList.Clear(); GifImageCacheList.Clear(); return; } ImageCacheList.Remove(cacheKey, out _); GifImageCacheList.Remove(cacheKey, out _); } public static ImageSource GetImageCache(string cacheKey) { if (ImageCacheList.ContainsKey(cacheKey)) return ImageCacheList[cacheKey]; return null; } #endregion } public enum PathType { Invalid = 0, Local = 1, Http = 2, Resources = 3 } public enum ImageType { Invalid = 0, Gif = 7173, Jpg = 255216, Png = 13780, Bmp = 6677 } public class QLAsyncImageLoadCompleteEventArgs : RoutedEventArgs { public QLAsyncImageLoadCompleteEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) { } public bool IsSuccess { get; set; } public string UrlSource { get; set; } public string ErrorMessage { get; set; } /// /// 当加载静态图时的实际像素宽度 /// public double StaticImageActualPixelWidth { get; set; } /// /// 当加载静态图时的实际像素高度 /// public double StaticImageActualPixelHeight { get; set; } } }