当前位置: 移动技术网 > IT编程>开发语言>.net > [WPF自定义控件库] 模仿UWP的ProgressRing

[WPF自定义控件库] 模仿UWP的ProgressRing

2019年10月21日  | 移动技术网IT编程  | 我要评论

天骄2活力版新手卡,李想简介,对讲设备

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. 源码

kino.toolkit.wpf_progressring at master

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网