欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

WPF自定义控件之图片控件 AsyncImage

程序员文章站 2022-05-15 12:32:23
AsyncImage 是一个封装完善,使用简便,功能齐全的WPF图片控件,比直接使用Image相对来说更加方便,但它的内部仍然使用Image承载图像,只不过在其基础上进行了一次完善成熟的封装 AsyncImage解决了以下问题1) 异步加载及等待提示2) 缓存3) 支持读取多种形式的图片路径 (Lo ......

asyncimage 是一个封装完善,使用简便,功能齐全的wpf图片控件,比直接使用image相对来说更加方便,但它的内部仍然使用image承载图像,只不过在其基础上进行了一次完善成熟的封装

asyncimage解决了以下问题
1) 异步加载及等待提示
2) 缓存
3) 支持读取多种形式的图片路径 (local,http,resource)
4) 根据文件头识别准确的图片格式
5) 静态图支持设置解码大小
6) 支持gif

asyncimage的工作流程

WPF自定义控件之图片控件 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="正在加载图像请稍后"/>

 

WPF自定义控件之图片控件 AsyncImage