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; }
问题根源
- 自定义模板中使用
ScrollViewer并通过MultiBinding绑定Height,手动修改Height会覆盖绑定值,导致后续动画的Tag变化无法更新Height(本地值优先级高于绑定)。 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
相关产品推荐
相关产品推荐

