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

1. 前言

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

2. ExtendedExpander的需求

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

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

最终运行效果如下:

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变化。

最终效果如下:

6. 结语

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

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

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

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

其它控件库的AccordionItem也可以实现类似的功能,可以当作Expander来用,例如Silverlight Toolkit,移植起来应该也不复杂。

另外有没有从上面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

原文地址:https://www.cnblogs.com/dino623/p/Custom_Expander.html

时间: 2024-07-31 12:30:27

[WPF自定义控件库]自定义Expander的相关文章

[WPF自定义控件库]使用WindowChrome自定义RibbonWindow

原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自定义RibbonWindow则不一样: 如果程序使用了自定义样式的Window,为了统一外观需要把RibbonWindow一起修改样式. 为了解决RibbonWindow的BUG. 如上图所示,在Windows 10 上运行打开RibbonWindow,可以看到标题栏的内容(包括分隔符)没有居中对齐

[WPF自定义控件库]为Form和自定义Window添加FunctionBar

1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按钮的大小,而我喜欢更进一步将"确定"."取消"或其它按钮封装进一个自定义控件里. 这篇文章介绍了另一种ItemsControl的实现方式,并使用它为表单及自定义Window添加常用的按钮及其它功能. 2. 为Form添加FunctionBar 本来打算派生自ToolBa

[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互

1. 前言 WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观.例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现: protected override void OnMouseEnter(MouseEventArgs e) { base.OnMouseEnter(e); Background = new SolidColorBrush(Colors.Blue); } 但一般没人会这么做,因为这样做代码和UI过于耦合,难以扩展.正确的做法应该是使用代码告诉C

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用"素颜"的外观,然后再提供一些主题包.这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分人来说模仿原生的主题也比自己设计一套好看的UI容易得多. WPF有以下几种原生主题: 主题文件 桌面主题 Classic.xaml Windows XP 操作系统上的经典 Windows 外观(Windows 95.Windows 98 和 Windows 2000). Luna.NormalColor.xa

【2016-11-11】【坚持学习】【Day24】【WPF 自定义控件 附加属性 自定义事件】

UserControl ,自定义控件. 这里刚刚想到一个问题.什么时候应该用usercontrol 定义一个控件.什么时候应该重写控件的template和样式,实现新效果. 引用一下人家的话:http://www.cnblogs.com/denghejun/p/3671061.html 我的理解: Usercontrol应该是一个带有功能,带有行为的控件.而一些简单的模型,样式效果,应该都可以用Template实现.它大部分是给呈现效果服务,当然,它可以带有很多事件触发器. 另外,在一些复杂的功

[WPF自定义控件库] 让Form在加载后自动获得焦点

1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录"对话框加载后"用户名"应该马上获得焦点,用户只需输入用户名,点击Tab,再输入密码,点击回车就完成了登录操作. 在WPF中要让一个控件在加载时获得焦点应该很简单,只需要在Loaded事件后调用Focus()就行了.但有时表单是动态添加的,或者第一个表单元素会根据某些条件显示或隐藏,这时很难简单地让第一个控件获得焦点. 为了实现这个功能我创建了一个叫F

[WPF自定义控件库] 关于ScrollViewr和滚动轮劫持(scroll-wheel-hijack)

1. 什么是滚动轮劫持 这篇文章介绍一个很简单的继承自ScrollViewer的控件: public class ExtendedScrollViewer : ScrollViewer { protected override void OnMouseWheel(MouseWheelEventArgs e) { if (ViewportHeight + VerticalOffset >= ExtentHeight && e.Delta <= 0) return; if (Ver

WPF自定义控件与样式(13)-自定义窗体Window &amp; 自适应内容大小消息框MessageBox

一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 自定义Window窗体样式: 基于自定义窗体实现自定义MessageBox消息提示框: 二.自定义Window窗体样式 自定义的Window窗体效果:   因为WPF默认的窗体比较简陋,大都需要自己实现Window窗体样式效果,基本思路很简单: 第一步:干掉默认样式:WindowStyle = Windo

WPF自定义控件与样式(7)-列表控件DataGrid与ListView自定义样式

一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: DataGrid自定义样式: ListView自定义样式: 二.DataGrid自定义样式 DataGrid是常用的数据列表显示控件,先看看实现的效果(动态图,有点大): DataGrid控件样式结构包括以下几个部分: 列头header样式 调整列头宽度的列分割线样式 行样式 行头调整高度样式 行头部样式