在 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?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM