• 在 UWP 中实现 Expander 控件


        WPF 中的 Expander 控件在 Windows 10 SDK 中并不提供,本文主要说明,如何在 UWP 中创建这样一个控件。其效果如下图:

        首先,分析该控件需要的一些特性,它应该至少包括如下三个属性:

    •     Content: 最重要的属性,设置该属性,可以使 Expander 控件显示其内容;
    •     Header: 控件的 Header; 
    •     IsExpand: 当前是否展开。

        接下来是定义其 UI,在这里使用 Grid,添加两行,一行显示 Header,一行显示 Content,当 IsExpand 属性为 false 时,只要将 Content 那一行隐藏即可;此外,还需要一个 ToggleButton 用于控制该控件的展开与关闭。

        OK。思路弄清楚后,开始实践,    

    1. 创建控件,并添加属性

        在项目中添加一个 Templated Control(模板化控件),名称为 Expander。为其添加三个依赖属性,代码如下:

       public static readonly DependencyProperty ContentProperty =
            DependencyProperty.Register("Content", typeof(object), typeof(Expander), new PropertyMetadata(null));
    
            public static readonly DependencyProperty HeaderProperty =
                DependencyProperty.Register("Header", typeof(object), typeof(Expander), new PropertyMetadata(null));
    
            public static readonly DependencyProperty IsExpandProperty =
                DependencyProperty.Register("IsExpand", typeof(bool), typeof(Expander), new PropertyMetadata(true));
    
            /// <summary>
            /// 控件的内容
            /// </summary>
            public object Content
            {
                get { return (object)GetValue(ContentProperty); }
                set { SetValue(ContentProperty, value); }
            }
    
            /// <summary>
            /// 控件的标题
            /// </summary>
            public object Header
            {
                get { return (object)GetValue(HeaderProperty); }
                set { SetValue(HeaderProperty, value); }
            }
    
            /// <summary>
            /// 返回或设置控件是否展开
            /// </summary>
            public bool IsExpand
            {
                get { return (bool)GetValue(IsExpandProperty); }
                set { SetValue(IsExpandProperty, value); }
            }

    2. 定义UI

        在 Generic.xaml 中,找到 <Style TargetType="controls:Expander"> 节点,添加如下代码:

        <Style TargetType="controls:Expander">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="controls:Expander">
                        <Grid x:Name="grid"
                              Background="{TemplateBinding Background}"
                              BorderBrush="{TemplateBinding BorderBrush}"
                              BorderThickness="{TemplateBinding BorderThickness}">
                             <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="*" />
                            </Grid.RowDefinitions>
                            <StackPanel Orientation="Horizontal">
                                <ToggleButton x:Name="toggleButton"
                                              Width="32"
                                              Height="32"
                                              Margin="0,0,4,0"
                                              BorderThickness="0"
                                              IsChecked="{Binding IsExpand,
                                                                  RelativeSource={RelativeSource Mode=TemplatedParent},
                                                                  Mode=TwoWay}">
                                    <Path x:Name="arrow"
                                          Width="16"
                                          Height="16"
                                          Data="M15.289001,0L20.484007,0 31.650999,15.953003 29.055021,19.658005 20.415007,32 15.35501,32 15.289001,31.906998 24.621,18.572998 0,18.572998 0,13.326004 24.621,13.326004z"
                                          Fill="#DDFFFFFF"
                                          RenderTransformOrigin="0.5,0.5"
                                          Stretch="Uniform">
                                     </Path>
                                </ToggleButton>
                                <ContentPresenter VerticalAlignment="Center" Content="{TemplateBinding Header}" />
                            </StackPanel>
                            <ContentPresenter Grid.Row="1"
                                            HorizontalContentAlignment="Stretch"
                                            VerticalContentAlignment="Stretch"
                                            Content="{TemplateBinding Content}"
                                            Visibility="{Binding IsExpand,
                                                                 RelativeSource={RelativeSource Mode=TemplatedParent},
                                                                 Converter={StaticResource BooleanToVisibilityConverter},
                                                                 Mode=TwoWay}" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        可以看出:
        a) ToggleButton 的 IsChecked 属性绑定了控件的 IsExpand 属性, 绑定表达式 {Binding IsExpand,RelativeSource={RelativeSource Mode=TemplatedParent}} 是 {TemplateBinding IsExpand} 的另一种写法,在这种写法中,我们可以添加 Binding 对象的其它属性,如这里的 Mode=TwoWay,这样可以实现 ToggleButton 与 控件的 IsExpand 属性彼此互相的控制;
        b) ContentControl 的 Visibility 与 a) 同理,略微复杂的是,这里用了一个 Converter,用于在 Bool 和 Visibility 枚举之间转换;
        c) 我们为 ToggleButton 控件的 Content 属性设置了一个 Path 用来形象地表达 Expander 当前的状态。

    3. 定义 VisualState

        我们为该控件定义两个 VisualState,分别代表正常状态和展开状态,即 Normal 与 Expanded,通过切换这两种状态可以完成该控件的UI变化,这里主要是对 ToggleButton 的 Content 进行动画设置。

        在 Path 中为其添加 RotateTransform,代码如下:

                          <Path x:Name="arrow"
                                          Width="16"
                                          Height="16"
                                          Data="M15.289001,0L20.484007,0 31.650999,15.953003 29.055021,19.658005 20.415007,32 15.35501,32 15.289001,31.906998 24.621,18.572998 0,18.572998 0,13.326004 24.621,13.326004z"
                                          Fill="#DDFFFFFF"
                                          RenderTransformOrigin="0.5,0.5"
                                          Stretch="Uniform">
                                        <Path.RenderTransform>
                                            <RotateTransform x:Name="pathRotate" />
                                        </Path.RenderTransform>
                                    </Path>

        在 Grid 中添加 VisualState,代码如下:

                        <Grid x:Name="grid"
                              Background="{TemplateBinding Background}"
                              BorderBrush="{TemplateBinding BorderBrush}"
                              BorderThickness="{TemplateBinding BorderThickness}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition From="Normal"
                                                          GeneratedDuration="0:0:0.2"
                                                          To="Expanded" />
                                        <VisualTransition From="Expanded"
                                                          GeneratedDuration="0:0:0.2"
                                                          To="Normal" />
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="Normal">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0:0:0"
                                                             Storyboard.TargetName="pathRotate"
                                                             Storyboard.TargetProperty="Angle"
                                                             To="0">
                                                <DoubleAnimation.EasingFunction>
                                                    <QuinticEase EasingMode="EaseOut" />
                                                </DoubleAnimation.EasingFunction>
                                            </DoubleAnimation>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Expanded">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0:0:0"
                                                             Storyboard.TargetName="pathRotate"
                                                             Storyboard.TargetProperty="Angle"
                                                             To="90">
                                                <DoubleAnimation.EasingFunction>
                                                    <QuinticEase EasingMode="EaseIn" />
                                                </DoubleAnimation.EasingFunction>
                                            </DoubleAnimation>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            ...

        这里,我们可以看到,除了两个 VisualState 外,我们还定义了两个 VisualTransition,用来设置切换此两种状态时的过度时间。

        提示:关于 Content 区域的隐藏与显示,也可以通过在 VisualState 添加动画来控制,不过在上面的代码中,我们利用了 ToggleButton 以及它的 IsCheced 属性来控制其显示与隐藏,较为简洁地了实现这一功能。

        接下来,我们需要在代码中来控制何时在这两种状态间切换,在 Expander.cs 中添加如下代码:

           private ToggleButton button;
    
            protected override void OnApplyTemplate()
            {
                base.OnApplyTemplate();
                button = GetTemplateChild("toggleButton") as ToggleButton;
                button.Loaded += (s, e) => { ChangeControlState(false); };
                button.Checked += (s, e) => { ChangeControlState(); };
                button.Unchecked += (s, e) => { ChangeControlState(); };
            }
    
            /// <summary>
            /// 改变控件的 VisualState
            /// </summary>
            /// <param name="useTransition">是否使用 VisualTransition,默认使用</param>
            private void ChangeControlState(bool useTransition = true)
            {
                if (button.IsChecked.Value)
                {
                    VisualStateManager.GoToState(this, "Expanded", useTransition);
                }
                else
                {
                    VisualStateManager.GoToState(this, "Normal", useTransition);
                }
            }

        可以看出,我们为 ToggleButton 添加事件响应来切换状态。之所以在 Load 时也来改检查并更改状态,是因为,如果在使 Expander 控件时,如果为它设置 IsExpand 为 true 时,那么加载时,会及时更新控件状态为 Expanded ,否则将默认为 Normal。

        最后,我们为控件添加一个 ContentPropertyAttribute,并设置其 Name 为 Content,这样,该控件的 Content 属性就作为此控件的内容属性(ContentPropery)。简言之,可以省去 <xxx:Expander.Content> 这个节点,类似在 Button 中直接添加其 Content 一样。代码如下:

        [ContentProperty(Name = "Content")]
        public sealed class Expander : Control

        至此,一个 Expander 控件就完成了,至于你还有额外、其它的需求(如样式的修改等),则可在此基础上进行修改。

        如果你有什么更好的建议或其它观点,请留言,互相交流。

     源代码下载

    参考资料:

    What's the difference between ContentControl and ContentPresenter?

    Difference between ContentControl, ContentPresenter, ContentTemplate and ControlTemplate?  (Bob_Bao's answer)

    What is ContentPropertyAttribute?

    作者:WPInfo

    本文系作者原创,欢迎转载;如需转载,请注明出处。

    公众号:.NET之窗 (WinDotNET),更多原创、优质技术文章,欢迎扫码关注。

  • 相关阅读:
    【刷题】BZOJ 3626 [LNOI2014]LCA
    【刷题】UOJ #207 共价大爷游长沙
    【刷题】COGS 2701 动态树
    【刷题】BZOJ 4530 [Bjoi2014]大融合
    【刷题】BZOJ 2959 长跑
    【刷题】BZOJ 1969 [Ahoi2005]LANE 航线规划
    【刷题】BZOJ 4998 星球联盟
    【刷题】BZOJ 1977 [BeiJing2010组队]次小生成树 Tree
    【刷题】BZOJ 4817 [Sdoi2017]树点涂色
    获取元素 在网页中的 坐标
  • 原文地址:https://www.cnblogs.com/wpinfo/p/5007923.html
Copyright © 2020-2023  润新知