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="正在加载图像请稍后"/>
推荐阅读
-
[WPF自定义控件库] 模仿UWP的ProgressRing
-
Android自定义控件之可拖动控制的圆环控制条实例代码
-
如何在双向绑定的Image控件上绘制自定义标记(wpf)
-
[WPF自定义控件库] 给WPF一个HyperlinkButton
-
[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用
-
C# Winfrom 自定义控件——带图片的TextBox
-
android之视频播放系统VideoView和自定义VideoView控件的应用
-
Android自定义控件之圆形、圆角ImageView
-
ASP.NET2.0服务器控件之自定义状态管理
-
WPF自定义实现IP地址输入控件