You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF嵌套自定义Expander时父控件高度无法自动收缩的问题

WPF嵌套自定义Expander UserControl高度无法自动收缩的解决方案

问题描述

为WPF的Expander控件设计了自定义模板并封装为UserControl(UC3),单独使用正常,但嵌套同类型UC3作为内容时,子Expander收缩后父UC的高度无法自动减小。尝试监听子Expander的Collapsed事件手动调整父控件高度,仅首次生效,再次展开后高度不再变化。

相关代码

App.xaml

<Application x:Class="SampleExpander.App"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib"
         xmlns:local ="clr-namespace:SampleExpander"
         StartupUri="MainWindow.xaml">
<Application.Resources>
    <ControlTemplate x:Key="AnimatedExpanderButtonTemp2" TargetType="{x:Type ToggleButton}">
        <Border x:Name="ExpanderButtonBorder" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Rectangle Fill="Transparent" Grid.ColumnSpan="2"/>
                <Ellipse Name="Circle" Grid.Column="0" Stroke="Transparent" Width="40" Height="40" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                <Image x:Name="Arrow" Grid.Column="0" Source="Resources/ArrowDown.png" Width="30" Height="30" RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center">
                    <Image.RenderTransform>
                        <RotateTransform Angle="270"/>
                    </Image.RenderTransform>
                </Image>
                <ContentPresenter x:Name="HeaderContent" Grid.Column="1" Margin="4,0,0,0" ContentSource="Content" HorizontalAlignment="Stretch"/>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsChecked" Value="True">
                <Trigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(Image.RenderTransform).(RotateTransform.Angle)" To="360" Duration="0:0:0.2"/>
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.EnterActions>
                <Trigger.ExitActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(Image.RenderTransform).(RotateTransform.Angle)" To="270" Duration="0:0:0.2"/>
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.ExitActions>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

    <local:MultiplyConverter x:Key="multiplyConverter" />
   
    <ControlTemplate x:Key="RevealExpanderTemp2" TargetType="{x:Type Expander}">
        <DockPanel>
            <ToggleButton x:Name="ExpanderButton" DockPanel.Dock="Top" Template="{StaticResource AnimatedExpanderButtonTemp2}" Content="{TemplateBinding Header}" IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" OverridesDefaultStyle="True" Padding="1.5,0"/>
            <ScrollViewer x:Name="ExpanderContentScrollView" DockPanel.Dock="Bottom" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Bottom">
                <ScrollViewer.Tag>
                    <sys:Double>0.0</sys:Double>
                </ScrollViewer.Tag>
                <ScrollViewer.Height>
                    <MultiBinding Converter="{StaticResource multiplyConverter}">
                        <Binding Path="ActualHeight" ElementName="ExpanderContent"/>
                        <Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
                    </MultiBinding>
                </ScrollViewer.Height>
                <ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
            </ScrollViewer>
        </DockPanel>
        <ControlTemplate.Triggers>
            <Trigger Property="IsExpanded" Value="True">
                <Trigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="ExpanderContentScrollView" Storyboard.TargetProperty="Tag" To="1" Duration="0:0:0.2"/>
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.EnterActions>
                <Trigger.ExitActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="ExpanderContentScrollView" Storyboard.TargetProperty="Tag" To="0" Duration="0:0:0.2"/>
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.ExitActions>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</Application.Resources>
</Application>

MultiplyConverter

public class MultiplyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        double result = 1.0;
        for (int i = 0; i < values.Length; i++)
        {
            if (values[i] is double)
                result *= (double)values[i];
        }

        return result;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new Exception("Not implemented");
    }
}

UC3.xaml

<UserControl x:Class="SampleExpander.UC3"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:SampleExpander"
         mc:Ignorable="d" >
<Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Stretch" VerticalAlignment="Top">
    <Expander Margin="10" Template="{StaticResource RevealExpanderTemp2}" OverridesDefaultStyle="True" x:Name="expTest" >
        <Expander.Header>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="40"/>
                </Grid.ColumnDefinitions>
                <TextBlock x:Name="txtDescription" Grid.Column="0" FontSize="18" Foreground="Black" VerticalAlignment="Center"/>
                <TextBlock x:Name="txtContent" Grid.Column="1" FontSize="18" Margin="10,0,10,0" Foreground="Black" VerticalAlignment="Center"/>                    
                <Button x:Name="btnRemove" Grid.Column="2" Width="40" Height="40" Background="#F8CECC" Content="X" FontWeight="Normal" FontSize="18" BorderBrush="#B95753" Visibility="Collapsed">
                    <Button.Resources>
                        <Style TargetType="Border">
                            <Setter Property="CornerRadius" Value="3"/>
                        </Style>
                    </Button.Resources>
                </Button>
            </Grid>
        </Expander.Header>
    </Expander>
</Border>    
</UserControl>

UC3.xaml.cs

public partial class UC3 : UserControl
{
    public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description", typeof(string), typeof(UC3), new PropertyMetadata(PropChanged));
    public static readonly DependencyProperty ContentCapturesProperty = DependencyProperty.Register("ContentCaptures", typeof(string), typeof(UC3), new PropertyMetadata(PropChanged));        
    public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(UC3), new PropertyMetadata(PropChanged));

    public event EventHandler Remove;

    public string Description
    {
        get { return (string)GetValue(DescriptionProperty); }
        set { SetValue(DescriptionProperty, value); }
    }

    public string ContentCaptures
    {
        get { return (string)GetValue(ContentCapturesProperty); }
        set { SetValue(ContentCapturesProperty, value); }
    }        

    public object Content
    {
        get { return (object)GetValue(ContentProperty); }
        set { SetValue(ContentProperty, value); }
    }

    public UC3()
    {
        InitializeComponent();
        
        btnRemove.Click += BtnRemove_Click;
    }

    private void BtnRemove_Click(object sender, RoutedEventArgs e)
    {
        Remove?.Invoke(this, null);
    }

    public static void PropChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        switch (e.Property.Name)
        {
            case "Description":
                ((UC3)sender).txtDescription.Text = e.NewValue.ToString();
                break;
            case "ContentCaptures":
                ((UC3)sender).txtContent.Text = e.NewValue.ToString();
                break;                                        
            case "Content":
                ((UC3)sender).expTest.Content = e.NewValue;
                break;
            default:
                break;
        }
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show(Description);
    }
}

MainWindow.xaml

<StackPanel Orientation="Vertical">        
    <local:UC3 Description="This is a description" ContentCaptures="(Test)" Margin="10" x:Name="ucTest">
        <local:UC3 Description="4 Packungen" Margin="10">
            <StackPanel Orientation="Vertical" Margin="10">
                <Label Content="1. QCBDRa1xyzgdfhnfgdfsfewrt44523412edafsf" />
                <Label Content="2. QCBDRa1xyzgdfhnfgdfsfewrt44523ddx2edasf" />
                <Label Content="3. QCBDRa1xyzgdfhnfgdfsfewrt4453423112sasf" />
                <Label Content="4. QCBDRa1xyzgdfhrrrasdewrt4998854511fddsd" />
            </StackPanel>
        </local:UC3>
        
    </local:UC3>
</StackPanel>

尝试的解决代码

private void SubExpander_Collapsed(object sender, RoutedEventArgs e)
{            
    var collpasedTemplate = ((Expander)sender).Template;
    var collapsedScrollViewer = (ScrollViewer)collpasedTemplate.FindName("ExpanderContentScrollView", (Expander)sender);
    double oldHeight = collapsedScrollViewer.ActualHeight;

    var template = expTest.Template;
    var scrollViewer = (ScrollViewer)template.FindName("ExpanderContentScrollView", expTest);
    
    scrollViewer.Height -= oldHeight;
}

问题根源

  1. 自定义模板中使用ScrollViewer并通过MultiBinding绑定Height,手动修改Height会覆盖绑定值,导致后续动画的Tag变化无法更新Height(本地值优先级高于绑定)。
  2. ScrollViewer的固定Height会让父容器无法感知子控件的尺寸变化,导致父UC高度无法自动调整。

解决方案

1. 修改Expander自定义模板

移除ScrollViewer,改用Border包裹内容区域,直接对Border的Height做动画,绑定内容的实际高度,确保每次展开都能获取最新的内容尺寸:

<Application.Resources>
    <!-- 保留AnimatedExpanderButtonTemp2模板不变 -->
    <ControlTemplate x:Key="AnimatedExpanderButtonTemp2" TargetType="{x:Type ToggleButton}">
        <!-- 原代码不变 -->
    </ControlTemplate>
   
    <!-- 修改后的Expander模板 -->
    <ControlTemplate x:Key="RevealExpanderTemp2" TargetType="{x:Type Expander}">
        <DockPanel>
            <ToggleButton x:Name="ExpanderButton" DockPanel.Dock="Top" Template="{StaticResource AnimatedExpanderButtonTemp2}" Content="{TemplateBinding Header}" IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" OverridesDefaultStyle="True" Padding="1.5,0"/>
            <!-- 用Border替代ScrollViewer,控制Height动画 -->
            <Border x:Name="ExpanderContentContainer" DockPanel.Dock="Bottom" Height="0" HorizontalAlignment="Stretch">
                <ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
            </Border>
        </DockPanel>
        <ControlTemplate.Triggers>
            <Trigger Property="IsExpanded" Value="True">
                <Trigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <!-- 动画到内容的实际高度 -->
                            <DoubleAnimation Storyboard.TargetName="ExpanderContentContainer" Storyboard.TargetProperty="Height" 
                                             To="{Binding ActualHeight, ElementName=ExpanderContent}" 
                                             Duration="0:0:0.2"/>
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.EnterActions>
                <Trigger.ExitActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <!-- 收缩到0 -->
                            <DoubleAnimation Storyboard.TargetName="ExpanderContentContainer" Storyboard.TargetProperty="Height" 
                                             To="0" 
                                             Duration="0:0:0.2"/>
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.ExitActions>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</Application.Resources>

2. 移除多余代码

  • 删除MultiplyConverter类(不再需要)。
  • 移除手动调整高度的SubExpander_Collapsed事件处理代码。

3. 确保UC3的布局属性

UC3的根Border已设置VerticalAlignment="Top",确保它的高度随内容自动调整,无需额外修改。

效果说明

修改后,子Expander收缩时,其内容区域高度变为0,父UC的内容区域会感知到子控件的尺寸变化,在父Expander展开时自动重新计算高度,嵌套的UC3可以正确地随子控件的展开/收缩自动调整高度,解决了手动调整仅首次生效的问题。

内容的提问来源于stack exchange,提问作者nicetomitja

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 23:50:00