[WPF自定义控件库] 模仿UWP的ProgressRing
1. 为什么需要progressring
虽然我认为这个控件库的控件需要模仿aero2的外观,但总有例外,其中一个就是progressring。progressring是来自uwp的控件,部分代码参考了 。progressring的使用方式运行效果如下:
<kino:progressring isactive="true" height="40" width="40" margin="8" minheight="9" minwidth="9" />
在windows 10中progressring十分常见,而且十分好用。它还支持自适应尺寸,在紧凑的地方使用progressring会给ui增色不少,而且不会显得格格不入:
那为什么不使用progressbar?其中一个原因是progressbar功能太多,而我很多时候只需要一个简单的显示正在等待的元素,另一个原因是条状的progressbar在紧凑的地方不好看,所以才需要结构相对简单的progressring。
2. 基本结构
[templatevisualstate(groupname = visualstates.groupactive, name = visualstates.stateactive)] [templatevisualstate(groupname = visualstates.groupactive, name = visualstates.stateinactive)] public partial class progressring : control { // using a dependencyproperty as the backing store for isactive. this enables animation, styling, binding, etc... public static readonly dependencyproperty isactiveproperty = dependencyproperty.register("isactive", typeof(bool), typeof(progressring), new propertymetadata(false, new propertychangedcallback(isactivechanged))); private bool hasappliedtemplate = false; public progressring() { defaultstylekey = typeof(progressring); } public bool isactive { get { return (bool)getvalue(isactiveproperty); } set { setvalue(isactiveproperty, value); } } public override void onapplytemplate() { base.onapplytemplate(); hasappliedtemplate = true; updatestate(isactive); } private static void isactivechanged(dependencyobject d, dependencypropertychangedeventargs args) { var pr = (progressring)d; var isactive = (bool)args.newvalue; pr.updatestate(isactive); } private void updatestate(bool isactive) { if (hasappliedtemplate) { string state = isactive ? visualstates.stateactive : visualstates.stateinactive; visualstatemanager.gotostate(this, state, true); } } }
progressring的基本代码如上所示,它只包含isactive这个属性,并使用这个属性控制它在active和inactive两种状态之间切换。参考silverlight toolkit,我也把常用的各种visualstate的状态名称作为常量写到一个统一的visualstates
类里:
#region groupactive /// <summary> /// active state. /// </summary> public const string stateactive = "active"; /// <summary> /// inactive state. /// </summary> public const string stateinactive = "inactive"; /// <summary> /// active state group. /// </summary> public const string groupactive = "activestates"; #endregion groupactive
3. 旋转
xaml部分几乎全部照抄uwp的progressring,所以实际运行效果和uwp的progressring很像,区别很小。
通常来说,progressring的active状态持续时间不会太长,而且progressring的尺寸也不会太大,所以progressring的active状态可以说不计成本。active状态下有5个ellipse 不停旋转,或者说做绕着中心点做圆周运动,而为了不需要任何计算圆周中心点的代码,progressring给每个ellipse外面都套上一个canvas,让这整个canvas旋转。xaml大概这样:
<storyboard repeatbehavior="forever" x:key="sb"> <doubleanimationusingkeyframes storyboard.targetname="e1r" begintime="0" storyboard.targetproperty="angle"> <splinedoublekeyframe keytime="0" value="-110" keyspline="0.13,0.21,0.1,0.7" /> <splinedoublekeyframe keytime="0:0:0.433" value="10" keyspline="0.02,0.33,0.38,0.77" /> <splinedoublekeyframe keytime="0:0:1.2" value="93" /> <splinedoublekeyframe keytime="0:0:1.617" value="205" keyspline="0.57,0.17,0.95,0.75" /> <splinedoublekeyframe keytime="0:0:2.017" value="357" keyspline="0,0.19,0.07,0.72" /> <splinedoublekeyframe keytime="0:0:2.783" value="439" /> <splinedoublekeyframe keytime="0:0:3.217" value="585" keyspline="0,0,0.95,0.37" /> </doubleanimationusingkeyframes> </storyboard> <canvas rendertransformorigin=".5,.5" height="100" width="100"> <canvas.rendertransform> <rotatetransform x:name="e1r" /> </canvas.rendertransform> <ellipse x:name="e1" width="20" height="20" fill="mediumpurple" /> </canvas>
然后运行效果这样:
4. 自适应大小
为了让progressring中各个ellipse都可以自适应大小,progressring提供了一个templatesettings
属性,类型为templatesettingvalues
,它里面包含以下记个依赖属性:
public double maxsidelength { get { return (double)getvalue(maxsidelengthproperty); } set { setvalue(maxsidelengthproperty, value); } } public double ellipsediameter { get { return (double)getvalue(ellipsediameterproperty); } set { setvalue(ellipsediameterproperty, value); } } public thickness ellipseoffset { get { return (thickness)getvalue(ellipseoffsetproperty); } set { setvalue(ellipseoffsetproperty, value); } }
xaml中的元素大小及布局绑定到这些属性:
<grid x:name="ring" background="{templatebinding background}" maxwidth="{binding relativesource={relativesource templatedparent}, path=templatesettings.maxsidelength}" maxheight="{binding relativesource={relativesource templatedparent}, path=templatesettings.maxsidelength}" visibility="collapsed" rendertransformorigin=".5,.5" flowdirection="lefttoright"> <canvas rendertransformorigin=".5,.5"> <canvas.rendertransform> <rotatetransform x:name="e1r" /> </canvas.rendertransform> <ellipse x:name="e1" style="{staticresource progressringellipsestyle}" width="{binding relativesource={relativesource templatedparent}, path=templatesettings.ellipsediameter}" height="{binding relativesource={relativesource templatedparent}, path=templatesettings.ellipsediameter}" margin="{binding relativesource={relativesource templatedparent}, path=templatesettings.ellipseoffset}" fill="{templatebinding foreground}" /> </canvas>
每当progressring调用measureoverrride
都重新计算这些值:
protected override system.windows.size measureoverride(system.windows.size availablesize) { var width = 20d; var height = 20d; if (system.componentmodel.designerproperties.getisindesignmode(this) == false) { width = double.isnan(width) == false ? width : availablesize.width; height = double.isnan(height) == false ? height : availablesize.height; } templatesettings = new templatesettingvalues(math.min(width, height)); return base.measureoverride(availablesize); }
public templatesettingvalues(double width) { if (width <= 40) { ellipsediameter = (width / 10) + 1; } else { ellipsediameter = width / 10; } maxsidelength = width - ellipsediameter; ellipseoffset = new system.windows.thickness(0, ellipsediameter * 2.5, 0, 0); }
这样就实现了外观的自适应大小功能。需要注意的是,过去很多人喜欢将这种重新计算大小的操作放到layoutupdated
事件中进行,但layoutupdated
是整个布局的最后一步,这时候如果改变了控件的大小有可能重新触发measure和arrange及layoutupdated
,这很可能引起“布局循环”的异常。正确的做法是将计算尺寸及改变尺寸的操作都放到最初的measureoverride
中。
templatesettings在uwp中很长见到,它的其它用法可以参考这篇文章:了解模板化控件:ui指南
5. 参考
brian dunnington - progressring for windows phone 8
frameworkelement.measureoverride(size) method (system.windows) microsoft docs.html
uielement.invalidatemeasure method (system.windows) microsoft docs
uielement.ismeasurevalid property (system.windows) microsoft docs
uielement.layoutupdated event (system.windows) microsoft docs
6. 源码
推荐阅读
-
[WPF自定义控件库] 模仿UWP的ProgressRing
-
[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用
-
[WPF自定义控件库]好用的VisualTreeExtensions
-
[WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画
-
[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互
-
[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题
-
[WPF自定义控件库]简单的表单布局控件
-
[WPF自定义控件库]使用WindowChrome的问题
-
[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用
-
WPF 如何创建自己的WPF自定义控件库