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

[WPF自定义控件库]自定义Expander

程序员文章站 2022-04-15 08:16:22
1. 前言 上一篇文章介绍了使用Resizer实现Expander简单的动画效果,运行效果也还好,不过只有展开/折叠而缺少了淡入/淡出的动画(毕竟Resizer模仿Expander只是附带的功能)。这篇继续Measure的话题,自定义了一个带有动画的ExtendedExpander。 2. Exte ......

1. 前言

上一篇文章介绍了使用resizer实现expander简单的动画效果,运行效果也还好,不过只有展开/折叠而缺少了淡入/淡出的动画(毕竟resizer模仿expander只是附带的功能)。这篇继续measure的话题,自定义了一个带有动画的extendedexpander。

2. extendedexpander的需求

使用resizer实现的简易expander没办法在折叠时做淡出动画,因为controltemplate中的expandsite在collapsed状态下直接设置为隐藏。一个稍微好看些的expander的状态改变动画要满足下面的需求:

  • 拉伸
  • 淡入淡出
  • 上面两个效果都可以用xaml定义

最终运行效果如下:

[WPF自定义控件库]自定义Expander

3. 实现思路

模仿silverlighttoolkit,我也用一个带有percentage属性的expandablecontentcontrol控件控制expander内容的拉伸。(顺便一提,silverlighttoolkit的expander没有拉伸动画,expandablecontentcontrol用在accordionitem里面)。expandablecontentcontrol的percentage属性控制这个控件的展开的百分比,1为完全展开,0为完全折叠。

在controltemplate中使用visualstate控制expanded/collapsed的动画。vusialstate.storyboard控制visualstate的最终值,过渡动画由visualstategroup.transitions控制,这在以前的 这篇文章 中有介绍过:

<border borderbrush="{templatebinding borderbrush}" borderthickness="{templatebinding borderthickness}" background="{templatebinding background}" cornerradius="3" snapstodevicepixels="true">
    <visualstatemanager.visualstategroups>
        <visualstategroup x:name="expansionstates">
            <visualstategroup.transitions>
                <visualtransition generatedduration="0:0:0.3">
                    <visualtransition.generatedeasingfunction>
                        <quarticease easingmode="easeout"/>
                    </visualtransition.generatedeasingfunction>
                </visualtransition>
            </visualstategroup.transitions>
            <visualstate x:name="expanded"/>
            <visualstate x:name="collapsed">
                <storyboard>
                    <doubleanimationusingkeyframes storyboard.targetproperty="(uielement.opacity)" storyboard.targetname="expandablecontentcontrol">
                        <easingdoublekeyframe keytime="0" value="0"/>
                    </doubleanimationusingkeyframes>
                    <doubleanimationusingkeyframes storyboard.targetproperty="percentage" storyboard.targetname="expandablecontentcontrol">
                        <easingdoublekeyframe keytime="0" value="0"/>
                    </doubleanimationusingkeyframes>
                </storyboard>
            </visualstate>
        </visualstategroup>
    </visualstatemanager.visualstategroups>
    <dockpanel>
        <togglebutton x:name="headersite" contenttemplate="{templatebinding headertemplate}" contenttemplateselector="{templatebinding headertemplateselector}" content="{templatebinding header}" dockpanel.dock="top" foreground="{templatebinding foreground}" fontweight="{templatebinding fontweight}" focusvisualstyle="{staticresource expanderheaderfocusvisual}" fontstyle="{templatebinding fontstyle}" fontstretch="{templatebinding fontstretch}" fontsize="{templatebinding fontsize}" fontfamily="{templatebinding fontfamily}" horizontalcontentalignment="{templatebinding horizontalcontentalignment}" ischecked="{binding isexpanded, mode=twoway, relativesource={relativesource templatedparent}}" margin="1" minwidth="0" minheight="0" padding="{templatebinding padding}" style="{staticresource expanderdownheaderstyle}" verticalcontentalignment="{templatebinding verticalcontentalignment}"/>
        <primitives:expandablecontentcontrol x:name="expandablecontentcontrol" horizontalcontentalignment="{templatebinding horizontalcontentalignment}" verticalcontentalignment="{templatebinding verticalcontentalignment}"
                                             margin="{templatebinding padding}" cliptobounds="true">
            <contentpresenter x:name="expandsite" dockpanel.dock="bottom" focusable="false" horizontalalignment="{templatebinding horizontalcontentalignment}"  verticalalignment="{templatebinding verticalcontentalignment}"/>
        </primitives:expandablecontentcontrol>
    </dockpanel>
</border>
<controltemplate.triggers>
    <trigger property="isexpanded" value="false">
        <setter property="ishittestvisible" targetname="expandablecontentcontrol" value="false"/>
    </trigger>
    ...
</controltemplate.triggers>

这样expander及它的controltemplate只做了最少的改动就实现了动画效果。主要的代码逻辑都交给expandablecontentcontrol。

4. 实现expandablecontentcontrol

expandablecontentcontrol派生自contentcontrol,它的percentage属性的定义如下:

public static readonly dependencyproperty percentageproperty =
    dependencyproperty.register(nameof(percentage),
                                typeof(double),
                                typeof(expandablecontentcontrol),
                                new frameworkpropertymetadata(1d, frameworkpropertymetadataoptions.affectsmeasure));

frameworkpropertymetadataoptions用于定义依赖属性的行为,其中affectsmeasure的意思是依赖属性的值改变时要求重新measure,既然measure了arrange也会发生,所以这个affectsmeasure其实就是要求重新执行两步布局。功能和上一篇文章介绍的invalidatemeasure差不多。

在measureoverride里根据percentage告诉父元素自己需要多大的空间,那么使用动画操作percentage属性就可以实现拉伸效果:

protected override size measureoverride(size constraint)
{
    int count = visualchildrencount;
    size childconstraint = new size(double.positiveinfinity, double.positiveinfinity);
    uielement child = (count > 0) ? getvisualchild(0) as uielement : null;
    var result = new size();
    if (child != null)
    {
        child.measure(childconstraint);
        result = child.desiredsize;
    }

    return new size(result.width * percentage, result.height * percentage);
}

最后,因为没有使用arrange限制子元素的大小,子元素的ui一定会超出范围,所以要overrid getlayoutclip 函数控制当子元素超出自身大小时是否显示超出的部分,可以用cliptobounds属性控制。

protected override geometry getlayoutclip(size layoutslotsize)
{
    if (cliptobounds)
        return new rectanglegeometry(new rect(rendersize));
    else
        return null;
}

之后只要把expandablecontentcontrol放到expander的controltemplate中就大功告成了。

5. 模仿accordion

因为实现起来太简单,内容太少,所以顺便提一下怎么模仿accordion。

accordion通常被翻译为手风琴?通常也就程序的左侧导航菜单会用到,用expandablecontentcontrol也可以简单地模仿如下:

private void onloaded(object sender, routedeventargs e)
{
    var expanders = new list<kinoexpander>();
    expander firstexpander = null;
    for (int i = 0; i < 10; i++)
    {
        var expander = new kinoexpander() { header = "this is accordionitem " + i };
        if (i == 0)
            firstexpander = expander;

        grid.setrow(expander, i);
        var panel = new stackpanel();
        panel.children.add(new checkbox { content = "calendar" });
        panel.children.add(new checkbox { content = "中国节假日" });
        panel.children.add(new checkbox { content = "birthdays" });
        expander.content = panel;
        menuroot.children.add(expander);
        menuroot.rowdefinitions.add(new rowdefinition { height = new gridlength(1, gridunittype.auto) });
        int index = i;
        expander.expanded += (s, args) =>
        {

            var lastexpander = expanders.where(p => p.isexpanded && p != s).firstordefault();
            if (lastexpander != null)
                lastexpander.isexpanded = false;

            menuroot.rowdefinitions[index].height = new gridlength(1, gridunittype.star);
        };

        expander.collapsed += (s, args) =>
          {
              if (expanders.any(p => p.isexpanded) == false)
              {
                  expander.isexpanded = true;
                  return;
              }

              menuroot.rowdefinitions[index].height = new gridlength(1, gridunittype.auto);
          };
        expanders.add(expander);
    }


    firstexpander.isexpanded = true;
}

menuroot是一个空的grid,上面这段代码用于控制menuroot的rowdefinitions根据当前选中的expander变化。

最终效果如下:

[WPF自定义控件库]自定义Expander

6. 结语

虽然实现了expander,但我想这种方式会影响到expander中scrollviewer的计算,所以最好还是不要把scrollviewer放进expander。

写完这篇文章才发觉可能把这篇和上一篇调换下比较好,因为这篇的measure的用法更简单。

其实有不少方案可以实现,但为了介绍measure搞到有点舍近求远了。例如直接用layouttransform就挺好的。

[WPF自定义控件库]自定义Expander

不过这种动画效果不怎么好看,所以很多控件库基本上都实现了自己的带动画的expander控件,例如telerik开源了ui for uwp控件库,里面的radexpandercontrol是个漂亮优雅的方案,应该可以轻易地移植到wpf(不过某些情况运行起来卡卡的)。

[WPF自定义控件库]自定义Expander

其它控件库的accordionitem也可以实现类似的功能,可以当作expander来用,例如silverlight toolkit,移植起来应该也不复杂。

[WPF自定义控件库]自定义Expander

另外有没有从上面extendedexpander的controltemplate感受到不换行的xaml有多烦?blend产生的样式默认就是这样的。extendedexpander的xaml没有使用之前的每个属性一行的方式写,这样的好处是很容易看清楚结构,但在分辨率不高的显示器,或者在github上根本看不到后面的属性,很容易因为看不到添加在最后的属性犯错(而且我的博客园主题,代码框里还没有滚动条)。使用哪种格式化见仁见智,这篇文章的样式因为是从别的地方复制的,既然保持了原格式就顺便用来讲解一下格式的这个问题,正好headersite的togglebutton几乎是presentationframework.aero2主题里最长的一行,感受一下这有多欢乐。最终选择使用哪种方式视乎团队人员的显示器有多大,但为了博客里看起来方便我会尽量选择每个属性一行的格式。

7. 参考

expander 概述 _ microsoft docs

customizing wpf expander with controltemplate - codeproject

frameworkpropertymetadataoptions enum (system.windows) _ microsoft docs

frameworkelement.measureoverride(size) method (system.windows) microsoft docs.html

8. 源码

kino.toolkit.wpf_expander at master