WPF 只保留一个 TreeViewItem 如果它被选中展开,折叠其他的除非它是 Selected TreeViewItem 的祖先
WPF Keep only one TreeViewItem expanded if it is selected, collapse the others unless it is the ancestor of the Selected TreeViewItem
我有一个带有自定义样式的 TreeViewItem,我希望它具有如下所示的行为,因此我想将它设置为我的 WPF 应用程序中的侧边菜单。问题是我只想保留一个展开的 TreeViewItem,而其他的则折叠,只要它们不是 selected 的祖先。
例如,如果我 select,Cat,在 Mammal 中,Mammal 显然也应该扩展,但不是 Insects。如果我 select 非洲,非洲和大陆必须扩张,而美洲,欧洲和亚洲必须崩溃。
还发生了一些奇怪的事情,SelectedItemChanged 事件没有触发,所以当 select 创建一个新的 TreeViewItem 时 IsSelected 属性 没有改变,所以我保留了我应用于 TreeViewItem 的样式。
自定义样式
<Style x:Key="TreeViewExpanderHeaderStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="{TemplateBinding Background}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="1" Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
VerticalAlignment="Center"
Margin="0,0,16,0" />
<ToggleButton Grid.Column="2"
VerticalAlignment="Center"
Foreground="{TemplateBinding Foreground}" x:Name="Expander" Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}}"
IsChecked="{Binding Path=IsChecked, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Style>
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="Transparent">
<Path Data="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" Opacity=".38" x:Name="ExpandPath" RenderTransformOrigin="0.5,0.5" Height="24" Width="24" Fill="{TemplateBinding Foreground}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MyTreeViewItemFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Gray"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Padding" Value="8" />
<Setter Property="FocusVisualStyle" Value="{StaticResource MyTreeViewItemFocusVisual}"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SelectionStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.6"/>
</VisualStateGroup.Transitions>
<VisualState Name="Selected">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="HeaderSite"
Storyboard.TargetProperty="Opacity"
To="0.18" Duration="0"/>
</Storyboard>
</VisualState>
<VisualState Name="Unselected"/>
</VisualStateGroup>
<VisualStateGroup x:Name="ExpansionStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0" To="Expanded">
<VisualTransition.GeneratedEasingFunction>
<CubicEase EasingMode="EaseOut"/>
</VisualTransition.GeneratedEasingFunction>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="1" Duration="0:0:0.3"/>
</Storyboard>
</VisualTransition>
<VisualTransition GeneratedDuration="0" To="Collapsed">
<VisualTransition.GeneratedEasingFunction>
<CubicEase EasingMode="EaseOut"/>
</VisualTransition.GeneratedEasingFunction>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0.3" Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="0" Duration="0:0:0.3"/>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Expanded">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="1" Duration="0"/>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Collapsed">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="0" Duration="0"/>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<DockPanel Background="{TemplateBinding Background}">
<ToggleButton Name="HeaderSite"
DockPanel.Dock="Top"
BorderThickness="0" Cursor="Hand"
IsChecked="{Binding Path=IsExpanded,RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource TreeViewExpanderHeaderStyle}"
Opacity=".87"
Foreground="{TemplateBinding Foreground}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"/>
<Border Name="ContentSite" DockPanel.Dock="Bottom">
<StackPanel x:Name="ItemsPanel" Margin="10 0 0 0">
<StackPanel.Height>
<MultiBinding Converter="{StaticResource MathMlpMultipleConverter}">
<Binding ElementName="ItemsHost" Path="ActualHeight"/>
<Binding ElementName="ItemsHost" Path="Opacity"/>
</MultiBinding>
</StackPanel.Height>
<ItemsPresenter x:Name="ItemsHost" VerticalAlignment="Top" Opacity="0" Visibility="Collapsed"/>
</StackPanel>
</Border>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="TextElement.Foreground" Value="Red"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Opacity" Value=".56"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
</Style.Triggers>
</Style>
和 TreeView
<TreeView SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeViewItem Header="Animals">
<TreeViewItem Header="Mammals">
<TreeViewItem Header="Cat"/>
<TreeViewItem Header="Dog"/>
<TreeViewItem Header="Horse"/>
</TreeViewItem>
<TreeViewItem Header="Insects">
<TreeViewItem Header="Fly"/>
<TreeViewItem Header="Wasp"/>
<TreeViewItem Header="Bee"/>
</TreeViewItem>
</TreeViewItem>
<TreeViewItem Header="Continents">
<TreeViewItem Header="Africa">
<TreeViewItem Header="Angola"/>
<TreeViewItem Header="Congo"/>
<TreeViewItem Header="Egypth"/>
<TreeViewItem Header="S. Africa"/>
</TreeViewItem>
<TreeViewItem Header="America">
<TreeViewItem Header="USA"/>
<TreeViewItem Header="Canada"/>
<TreeViewItem Header="Mexico"/>
<TreeViewItem Header="Brazil"/>
</TreeViewItem>
<TreeViewItem Header="Europe">
<TreeViewItem Header="UK"/>
<TreeViewItem Header="Spain"/>
<TreeViewItem Header="France"/>
<TreeViewItem Header="Italy"/>
</TreeViewItem>
<TreeViewItem Header="Asia">
<TreeViewItem Header="China"/>
<TreeViewItem Header="Korea"/>
<TreeViewItem Header="Japan"/>
<TreeViewItem Header="Viet Nam"/>
</TreeViewItem>
</TreeViewItem>
</TreeView>
我推荐使用这个简单的代码片段,一个简单的全局样式,可以转换为 Behavior
<Window.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="Expanded" Handler="TreeViewItem_Expanded"/>
</Style>
</Window.Resources>
以及 EventSetter
的处理程序
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
var trvi = sender as TreeViewItem;
//Using pattern matching
//If parent is an ItemsControl and it's the current expanded item
//That's a generic way to achieve such behavior
if(trvi is {Parent: ItemsControl parent, IsExpanded: true })
foreach (TreeViewItem item in parent.Items)
item.IsExpanded = false;
}
更新
由于您要求在 select 另一个分支之后折叠整个分支,您应该递归地遍历并折叠节点
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
var trvi = sender as TreeViewItem;
if (trvi is { Parent: ItemsControl parent, IsExpanded: true })
foreach (TreeViewItem item in parent.Items)
if (item != trvi && item.IsExpanded)
CollapseBranch(item);
}
private static void CollapseBranch(TreeViewItem trvi)
{
trvi.IsExpanded = false;
foreach (TreeViewItem item in trvi.Items)
CollapseBranch(item);
}
我有一个带有自定义样式的 TreeViewItem,我希望它具有如下所示的行为,因此我想将它设置为我的 WPF 应用程序中的侧边菜单。问题是我只想保留一个展开的 TreeViewItem,而其他的则折叠,只要它们不是 selected 的祖先。 例如,如果我 select,Cat,在 Mammal 中,Mammal 显然也应该扩展,但不是 Insects。如果我 select 非洲,非洲和大陆必须扩张,而美洲,欧洲和亚洲必须崩溃。 还发生了一些奇怪的事情,SelectedItemChanged 事件没有触发,所以当 select 创建一个新的 TreeViewItem 时 IsSelected 属性 没有改变,所以我保留了我应用于 TreeViewItem 的样式。
自定义样式
<Style x:Key="TreeViewExpanderHeaderStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="{TemplateBinding Background}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="1" Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
VerticalAlignment="Center"
Margin="0,0,16,0" />
<ToggleButton Grid.Column="2"
VerticalAlignment="Center"
Foreground="{TemplateBinding Foreground}" x:Name="Expander" Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}}"
IsChecked="{Binding Path=IsChecked, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Style>
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="Transparent">
<Path Data="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" Opacity=".38" x:Name="ExpandPath" RenderTransformOrigin="0.5,0.5" Height="24" Width="24" Fill="{TemplateBinding Foreground}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MyTreeViewItemFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Gray"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Padding" Value="8" />
<Setter Property="FocusVisualStyle" Value="{StaticResource MyTreeViewItemFocusVisual}"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SelectionStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.6"/>
</VisualStateGroup.Transitions>
<VisualState Name="Selected">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="HeaderSite"
Storyboard.TargetProperty="Opacity"
To="0.18" Duration="0"/>
</Storyboard>
</VisualState>
<VisualState Name="Unselected"/>
</VisualStateGroup>
<VisualStateGroup x:Name="ExpansionStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0" To="Expanded">
<VisualTransition.GeneratedEasingFunction>
<CubicEase EasingMode="EaseOut"/>
</VisualTransition.GeneratedEasingFunction>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="1" Duration="0:0:0.3"/>
</Storyboard>
</VisualTransition>
<VisualTransition GeneratedDuration="0" To="Collapsed">
<VisualTransition.GeneratedEasingFunction>
<CubicEase EasingMode="EaseOut"/>
</VisualTransition.GeneratedEasingFunction>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0.3" Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="0" Duration="0:0:0.3"/>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Expanded">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="1" Duration="0"/>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Collapsed">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="0" Duration="0"/>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<DockPanel Background="{TemplateBinding Background}">
<ToggleButton Name="HeaderSite"
DockPanel.Dock="Top"
BorderThickness="0" Cursor="Hand"
IsChecked="{Binding Path=IsExpanded,RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource TreeViewExpanderHeaderStyle}"
Opacity=".87"
Foreground="{TemplateBinding Foreground}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"/>
<Border Name="ContentSite" DockPanel.Dock="Bottom">
<StackPanel x:Name="ItemsPanel" Margin="10 0 0 0">
<StackPanel.Height>
<MultiBinding Converter="{StaticResource MathMlpMultipleConverter}">
<Binding ElementName="ItemsHost" Path="ActualHeight"/>
<Binding ElementName="ItemsHost" Path="Opacity"/>
</MultiBinding>
</StackPanel.Height>
<ItemsPresenter x:Name="ItemsHost" VerticalAlignment="Top" Opacity="0" Visibility="Collapsed"/>
</StackPanel>
</Border>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="TextElement.Foreground" Value="Red"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Opacity" Value=".56"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
</Style.Triggers>
</Style>
和 TreeView
<TreeView SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeViewItem Header="Animals">
<TreeViewItem Header="Mammals">
<TreeViewItem Header="Cat"/>
<TreeViewItem Header="Dog"/>
<TreeViewItem Header="Horse"/>
</TreeViewItem>
<TreeViewItem Header="Insects">
<TreeViewItem Header="Fly"/>
<TreeViewItem Header="Wasp"/>
<TreeViewItem Header="Bee"/>
</TreeViewItem>
</TreeViewItem>
<TreeViewItem Header="Continents">
<TreeViewItem Header="Africa">
<TreeViewItem Header="Angola"/>
<TreeViewItem Header="Congo"/>
<TreeViewItem Header="Egypth"/>
<TreeViewItem Header="S. Africa"/>
</TreeViewItem>
<TreeViewItem Header="America">
<TreeViewItem Header="USA"/>
<TreeViewItem Header="Canada"/>
<TreeViewItem Header="Mexico"/>
<TreeViewItem Header="Brazil"/>
</TreeViewItem>
<TreeViewItem Header="Europe">
<TreeViewItem Header="UK"/>
<TreeViewItem Header="Spain"/>
<TreeViewItem Header="France"/>
<TreeViewItem Header="Italy"/>
</TreeViewItem>
<TreeViewItem Header="Asia">
<TreeViewItem Header="China"/>
<TreeViewItem Header="Korea"/>
<TreeViewItem Header="Japan"/>
<TreeViewItem Header="Viet Nam"/>
</TreeViewItem>
</TreeViewItem>
</TreeView>
我推荐使用这个简单的代码片段,一个简单的全局样式,可以转换为 Behavior
<Window.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="Expanded" Handler="TreeViewItem_Expanded"/>
</Style>
</Window.Resources>
以及 EventSetter
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
var trvi = sender as TreeViewItem;
//Using pattern matching
//If parent is an ItemsControl and it's the current expanded item
//That's a generic way to achieve such behavior
if(trvi is {Parent: ItemsControl parent, IsExpanded: true })
foreach (TreeViewItem item in parent.Items)
item.IsExpanded = false;
}
更新
由于您要求在 select 另一个分支之后折叠整个分支,您应该递归地遍历并折叠节点
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
var trvi = sender as TreeViewItem;
if (trvi is { Parent: ItemsControl parent, IsExpanded: true })
foreach (TreeViewItem item in parent.Items)
if (item != trvi && item.IsExpanded)
CollapseBranch(item);
}
private static void CollapseBranch(TreeViewItem trvi)
{
trvi.IsExpanded = false;
foreach (TreeViewItem item in trvi.Items)
CollapseBranch(item);
}