• [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

  • 相关阅读:
    Android Studio中无法找到android.os.SystemProperties解决办法
    Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks
    神经网络中的常用表示方式
    Leetcode 223. 矩形面积
    Leetcode 836. 矩形重叠
    Mysql-索引
    Mysql表操作
    Mysql-概念
    利用mnist数据集进行深度神经网络
    剑指-面试题-07.重建二叉树
  • 原文地址:https://www.cnblogs.com/dino623/p/Custom_Expander.html
Copyright © 2020-2023  润新知