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