65.9K
CodeProject 正在变化。 阅读更多。
Home

动画 Expander 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.59/5 (15投票s)

2009 年 4 月 1 日

CPOL

5分钟阅读

viewsIcon

75444

downloadIcon

3144

一个自定义内容控件,可以随着动画展开/折叠。

Sample Image

引言

在 Whitebox Security 开发我们的软件时,我们遇到了一个需求,需要构建一个可以展开和折叠的控件,同时包含不同的内容。

对该控件的要求是

  • 最小化时占用最小屏幕空间。
  • 最大化时,用户可以自定义控件的大小。
  • 如果用户折叠和展开控件,它将扩展到用户设置的最后一次展开的长度(高度或宽度)。
  • 当控件的大小改变时,它会调整屏幕上其他控件的大小。
  • 展开和折叠操作将是动画化的。
  • 控件可以轻松配置为向不同方向展开。

要求

该项目使用 Visual Studio 2008 编写,基于 .NET 3.5 SP1 构建。

先决知识

要使用该控件,您应该具备基本的 WPF 知识。

如果您想了解控件是如何编写的,您应该熟悉

演示应用程序 

演示应用程序由一个使用 Grid 分成四部分的 Window 组成。在屏幕的每个四分之一处,都放置了一个 AnimatingExpanderControl,其中包含一些内容。

这四个控件中的每一个都展示了一种略有不同的配置。一些不同的地方包括

  • 展开方向。
  • 初始状态为折叠或展开。
  • 使用动画进行展开和折叠。
  • 限制内容的宽度/高度的最大值。

使用控件

这是四个控件之一的 XAML

<!--Expand Left-->
<Border
    BorderBrush="Black" 
    BorderThickness="2" 
    Grid.Row="1"
    Grid.Column="1"
    Padding="3">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Viewbox
        Stretch="Uniform"
        Grid.Column="0">
            <Image
            Source="..\Images\cat_rope.gif">
            </Image>
        </Viewbox>
        <local:AnimatingExpanderControl 
        ExpandDirection="Left"
        Style="{StaticResource AnimatingExpanderControlStyle}"
        Grid.Column="1"
        Title="Expand Content Left"
        >
            <Viewbox
            Stretch="Uniform">
                <StackPanel
                Orientation="Vertical">
                    <Button>
                        Content
                    </Button>
                    <TextBlock>
                    Some more content
                </TextBlock>
                    <StackPanel
                    Orientation="Vertical">
                        <RadioButton Content="First Choice"/>
                        <RadioButton Content="Second Choice"/>
                        <RadioButton Content="Third Choice"/>
                    </StackPanel>
                </StackPanel>
            </Viewbox>
        </local:AnimatingExpanderControl>
    </Grid>
</Border>

正如您所见,该控件定义了一个 ViewBox 作为其内容。这是为了让内容根据其所占用的空间进行缩放。该控件的“邻近”内容也放置在 ViewBox 中,原因相同。

控件上可以设置的自定义依赖属性有

  • ExpandDirection - 确定展开的方向。值可以是 UpDownLeftRight
  • IsCollapsed - 一个标志,显示控件初始化时是折叠还是未折叠。
  • InitialExpandedLength - 用户可以设置此项来控制展开器展开的初始值(宽度或高度)。
  • AnimationEnabled - 一个标志,用于启用或禁用控件上的展开和折叠动画。
  • Title - 此属性设置控件的标题,该标题显示在展开/折叠按钮旁边。

在屏幕上动态调整不同组件大小的一种方法是,将内容放置在一个 Grid 中,其中 AnimatingExpanderControl 位于 Width 设置为“Auto”的 Column(或 Row)中,而屏幕的其余内容位于 Width 为“*”的 Column 中。此外,还使用了 ViewBox 来容纳内容。

要限制控件可以展开的最大尺寸,请在控件的 **Content** 上设置 MaxWidth(或 MaxHeight)。在演示中,向右展开的控件上可以看到这一点。

控件的结构

<Border BorderBrush="{TemplateBinding BorderBrush}"
    BorderThickness="{TemplateBinding BorderThickness}"
    Background="{TemplateBinding Background}"
    CornerRadius="3"
    SnapsToDevicePixels="true">
    <DockPanel 
        Name="TemplateDockPanel"
        DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
        <Thumb 
           DockPanel.Dock="Top"
           Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
           BorderBrush="White"
           x:Name="PART_Thumb"
           Cursor="SizeNS"
           DragDelta="GridSplitter_DragDelta" 
           HorizontalAlignment="Stretch"
           VerticalAlignment="Bottom"
           Margin="5,0,5,5" 
           IsEnabled="{Binding ElementName=PART_CheckBox, Path=IsChecked, Mode=OneWay}"
           Style="{StaticResource GridSplitterStyle}"/>
        <ContentPresenter 
            DockPanel.Dock="Bottom"
            Margin="{TemplateBinding Padding}"
            x:Name="PART_Content"/>
        <CheckBox 
            x:Name="PART_CheckBox" 
            Checked="BottomCB_Checked" 
            Unchecked="BottomCB_UnChecked"
            Style="{StaticResource CheckBox_Expander}" 
            Margin="1" 
            Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Title}"
            Foreground="{TemplateBinding Foreground}"
            Padding="3"
            FontFamily="{TemplateBinding FontFamily}"
            FontSize="{TemplateBinding FontSize}"
            FontStyle="{TemplateBinding FontStyle}"
            FontStretch="{TemplateBinding FontStretch}"
            FontWeight="{TemplateBinding FontWeight}">
        </CheckBox>
    </DockPanel>
</Border>

该控件由三个组件组成,放置在 DockPanel 中。这些组件在控件的 ControlTemplate 中设置

  1. 一个 Thumb,模拟一个 GridSplitterGridSplitter 允许通过拖动来调整控件的大小。如果控件折叠,GridSplitter 将被禁用。
  2. 控件的内容,通过 ContentPresenter 显示。
  3. 一个 CheckBox,模拟一个带箭头的按钮。按下时,控件将展开/折叠,箭头方向会改变。控件的 Title 依赖属性显示在 CheckBox 旁边。

一些基本样式已应用于 ThumbCheckbox,这里不详述。

此外,在 ControlTemplate 中还设置了一些触发器

<ControlTemplate.Triggers>
    <!--Trigger for up direction-->
    <Trigger 
        Property="ExpandDirection"
        Value="Up">
        <Setter Property="DockPanel.Dock"
            Value="Top"
            TargetName="PART_Thumb"/>
        <Setter Property="DockPanel.Dock"
            Value="Bottom"
            TargetName="PART_Content"/>
        <Setter Property="HorizontalAlignment"
                Value="Stretch"
                TargetName="PART_Thumb"/>
        <Setter Property="VerticalAlignment"
                Value="Bottom"
                TargetName="PART_Thumb"/>
        <Setter Property="Height"
                Value="5"
                TargetName="PART_Thumb"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"
                TargetName="PART_CheckBox"/>
    </Trigger>
    <!--Trigger for down direction-->
    <Trigger 
        Property="ExpandDirection"
        Value="Down">
        <Setter Property="DockPanel.Dock"
            Value="Bottom"
            TargetName="PART_Thumb"/>
        <Setter Property="DockPanel.Dock"
            Value="Top"
            TargetName="PART_Content"/>
        <Setter Property="HorizontalAlignment"
                Value="Stretch"
                TargetName="PART_Thumb"/>
        <Setter Property="VerticalAlignment"
                Value="Bottom"
                TargetName="PART_Thumb"/>
        <Setter Property="Height"
                Value="5"
                TargetName="PART_Thumb"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"
                TargetName="PART_CheckBox"/>
        <Setter Property="Style"
                TargetName="PART_CheckBox"
                Value="{StaticResource CheckBoxUpSideDown_Expander}"
                />
    </Trigger>
    <!--Trigger for left direction-->
    <Trigger 
        Property="ExpandDirection"
        Value="Left">
        <Setter Property="DockPanel.Dock"
            Value="Left"
            TargetName="PART_Thumb"/>
        <Setter Property="DockPanel.Dock"
            Value="Right"
            TargetName="PART_Content"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"
                TargetName="PART_Thumb"/>
        <Setter Property="VerticalAlignment"
                Value="Stretch"
                TargetName="PART_Thumb"/>
        <Setter Property="Cursor"
                Value="SizeWE"
                TargetName="PART_Thumb"/>
        <Setter Property="Width"
                Value="5"
                TargetName="PART_Thumb"/>
        <Setter Property="LayoutTransform"
                TargetName="PART_CheckBox"
                >
            <Setter.Value>
                <RotateTransform Angle="-90"/>
            </Setter.Value>
            
        </Setter>
        <Setter Property="VerticalAlignment"
                Value="Center"
                TargetName="PART_CheckBox"/>
    </Trigger>

    <!--Trigger for right direction-->
    <Trigger 
        Property="ExpandDirection"
        Value="Right">
        <Setter Property="DockPanel.Dock"
            Value="Right"
            TargetName="PART_Thumb"/>
        <Setter Property="DockPanel.Dock"
            Value="Left"
            TargetName="PART_Content"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"
                TargetName="PART_Thumb"/>
        <Setter Property="VerticalAlignment"
                Value="Stretch"
                TargetName="PART_Thumb"/>
        <Setter Property="Cursor"
                Value="SizeWE"
                TargetName="PART_Thumb"/>
        <Setter Property="Width"
                Value="5"
                TargetName="PART_Thumb"/>
        <Setter Property="LayoutTransform"
                TargetName="PART_CheckBox"
                >
            <Setter.Value>
                <RotateTransform Angle="90"/>
            </Setter.Value>
            
        </Setter>
        <Setter Property="VerticalAlignment"
                Value="Center"
                TargetName="PART_CheckBox"/>
    </Trigger>
    <Trigger Property="IsEnabled"
         Value="false">
        <Setter Property="Foreground"
            Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
    </Trigger>
</ControlTemplate.Triggers>

这些触发器基本上会根据控件的 ExpandDirection 依赖属性设置不同的属性。

代码隐藏部分

这是处理用户在控件折叠时单击展开/折叠按钮(实际上是 CheckBox)的代码

private void BottomCB_Checked(object sender, RoutedEventArgs e) {
    //determine the DP that needs to be changed
    DependencyProperty actualContentLengthDP;
    this.DetermineLengthDPToChange(out actualContentLengthDP);
    
    //change the determined DP with animation
    if (AnimationEnabled) {
        this.ExpandWithAnimation(actualContentLengthDP);
    }
    //change the determined DP without animation
    else {
        this.ActualContent.SetValue(actualContentLengthDP, this.ExpandedValue);
    }
    this.IsCollapsed = false;
}

如果禁用动画,实际内容的高度(或宽度)将设置为 ExpandedValue。如果这是控件第一次展开,ExpandedValue 将从 InitialExpandedLength 依赖属性获取。否则,使用的值是控件通过“GridSplitter”调整到的最后一次高度(宽度)。

下面的代码处理使用动画时的展开

private void ExpandWithAnimation(
        DependencyProperty actualContentLengthDP
) {
    Storyboard sb = new Storyboard();
    DoubleAnimation expandAnimation = new DoubleAnimation();
    expandAnimation.From = 0;
    //Content is collapsed. Expand to last saved this.ExpandedValue.
    expandAnimation.To = this.ExpandedValue;
    expandAnimation.Duration = new TimeSpan(0, 0, 0, 0, _animationDuration);
    sb.Children.Add(expandAnimation);
    sb.FillBehavior = FillBehavior.Stop;
    this.BeginChangeSizeAnimation(
        sb, 
        expandAnimation,
        actualContentLengthDP
    );    
}

private void BeginChangeSizeAnimation(
    Storyboard sb, 
    DoubleAnimation lengthAnimation,
    DependencyProperty actualContentLengthDP
) {
    //execute Animation
    Storyboard.SetTargetProperty(lengthAnimation, 
             new PropertyPath(actualContentLengthDP.Name));
    if (null != this.ActualContent) {
        sb.Completed += delegate {
            this.ActualContent.SetValue(actualContentLengthDP, 
                                        lengthAnimation.To.Value);
        };
        sb.Begin(this.ActualContent);
    }
}

这一次,通过使用简单的 DoubleAnimation 将内容的高度/宽度设置为展开值。动画完成后,一个委托会将实际内容宽度/高度的值设置为 .To 值。

一旦控件展开,用户就可以调整其大小。这是通过一个 Thumb 来实现的,它模拟了一个 GridSplitter。为了确保 Thumb 在控件折叠时无法拖动,请回顾“控件结构”部分中的以下一行

IsEnabled="{Binding ElementName=PART_CheckBox, Path=IsChecked, Mode=OneWay}"

ThumbIsEnabled 属性绑定到 CheckBoxIsChecked 依赖属性。这意味着只有当 CheckBox IsChecked(即控件已展开)时,ThumbIsEnabled

这是 thumb 拖动事件的处理程序

private void GridSplitter_DragDelta(object sender, DragDeltaEventArgs e) {
    
    if (null != this.ActualContent) {
        //set this.ExpandedValue to new dragged value, then resize the content.
        //if new value is smaller then zero, or bigger the Content's Max Length,
        //reset length accordingly
        switch (this.ExpandDirection) {
            case ExpandDirection.Down:
                this.ExpandedValue = this.ActualContent.Height + e.VerticalChange;
                this.CheckExpandedValue(this.ActualContent.MaxHeight);
                this.ActualContent.Height = this.ExpandedValue; 
                break;
            case ExpandDirection.Up:
                this.ExpandedValue = this.ActualContent.Height - e.VerticalChange;
                this.CheckExpandedValue(this.ActualContent.MaxHeight);
                this.ActualContent.Height = this.ExpandedValue; 
                break;
            case ExpandDirection.Left:
                this.ExpandedValue = this.ActualContent.Width - e.HorizontalChange;
                this.CheckExpandedValue(this.ActualContent.MaxWidth);
                this.ActualContent.Width = this.ExpandedValue;
                break;
            case ExpandDirection.Right:
                this.ExpandedValue = this.ActualContent.Width + e.HorizontalChange;
                this.CheckExpandedValue(this.ActualContent.MaxWidth);
                this.ActualContent.Width = this.ExpandedValue;
                break;
        }
    }
}

该方法首先确定垂直或水平的变化,并相应地设置 ExpandedValue。然后它验证新的 ExpandedValue 是否合法。接下来,它将控件内容的 HeightWidth 属性设置为新的 ExpandedValue。下次控件折叠然后再次展开时,它将展开到保存的 ExpandedValue

这是检查 ExpandedValue 是否合法并重置其合法性的代码

private void CheckExpandedValue(double MaxLength) {
    if (this.ExpandedValue < 0) {
        this.ExpandedValue = 0;
    }
    if (this.ExpandedValue > MaxLength) {
        this.ExpandedValue = MaxLength;
    }
}

您有什么看法?

这是我为 CodeProject 撰写的第一个文章。如果您觉得这篇文章有帮助,请告诉我您的想法。

致谢

在编写 ExpandDirection 的触发器时,我受到了一些微软原始 WPF 控件模板的启发。

© . All rights reserved.