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

如何让TreeViewItem的MultiTrigger仅在两个属性均变化时触发?

解决WPF MultiTrigger仅在两个属性同时变为true时触发的问题

你的问题很典型——WPF的MultiTrigger默认逻辑是只要所有条件都满足(不管是先后达到还是同时达到),就会触发EnterActions。所以当IsExpanded已经是true,再把IsSelected设为true时,两个条件都满足了,自然会触发动画。要实现“仅当两个属性同时从非true变为true”才触发,我们需要跟踪属性的状态变化,而不是只看当前状态。

下面提供两种可行的方案:


方案一:使用附加行为(Attached Behavior)(推荐,声明式+可复用)

附加行为是WPF中实现自定义交互逻辑的最佳实践之一,它可以保持XAML的整洁,同时逻辑可复用。

1. 创建附加行为类

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;

public static class ExpandSelectedBehavior
{
    // 附加属性:是否启用该行为
    public static readonly DependencyProperty EnableProperty =
        DependencyProperty.RegisterAttached("Enable", typeof(bool), typeof(ExpandSelectedBehavior), 
            new PropertyMetadata(false, OnEnableChanged));

    private static readonly Dictionary<TreeViewItem, (bool Expanded, bool Selected)> _previousStates = new();

    public static bool GetEnable(DependencyObject obj)
    {
        return (bool)obj.GetValue(EnableProperty);
    }

    public static void SetEnable(DependencyObject obj, bool value)
    {
        obj.SetValue(EnableProperty, value);
    }

    private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not TreeViewItem item) return;

        if ((bool)e.NewValue)
        {
            // 订阅属性变更事件
            item.Loaded += Item_Loaded;
            item.Unloaded += Item_Unloaded;
            item.PropertyChanged += Item_PropertyChanged;
        }
        else
        {
            // 取消订阅
            item.Loaded -= Item_Loaded;
            item.Unloaded -= Item_Unloaded;
            item.PropertyChanged -= Item_PropertyChanged;
            _previousStates.Remove(item);
        }
    }

    private static void Item_Loaded(object sender, RoutedEventArgs e)
    {
        var item = sender as TreeViewItem;
        if (item == null) return;
        // 初始化记录初始状态
        _previousStates[item] = (item.IsExpanded, item.IsSelected);
    }

    private static void Item_Unloaded(object sender, RoutedEventArgs e)
    {
        var item = sender as TreeViewItem;
        if (item == null) return;
        _previousStates.Remove(item);
    }

    private static void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        var item = sender as TreeViewItem;
        if (item == null || !_previousStates.ContainsKey(item)) return;

        var prevState = _previousStates[item];
        var currentExpanded = item.IsExpanded;
        var currentSelected = item.IsSelected;

        // 判断:当前两个属性都是true,且之前两个都是false(即同时变为true)
        if (currentExpanded && currentSelected && !prevState.Expanded && !prevState.Selected)
        {
            // 找到并启动动画
            var itemsHost = item.Template.FindName("ItemsHost", item) as ItemsPresenter;
            if (itemsHost != null)
            {
                var storyboard = new Storyboard();
                var scaleAnim = new DoubleAnimation
                {
                    From = 0,
                    To = 1,
                    Duration = new Duration(TimeSpan.FromSeconds(0.2)),
                    FillBehavior = FillBehavior.HoldEnd
                };
                Storyboard.SetTargetName(scaleAnim, "ItemsHost");
                Storyboard.SetTargetProperty(scaleAnim, 
                    new PropertyPath("(ItemsPresenter.LayoutTransform).(ScaleTransform.ScaleY)"));
                storyboard.Children.Add(scaleAnim);
                storyboard.Begin(itemsHost);
                // 同时设置ItemsHost可见
                itemsHost.Visibility = Visibility.Visible;
            }
        }

        // 更新记录的状态
        _previousStates[item] = (currentExpanded, currentSelected);
    }
}

2. 在XAML中使用该行为

首先确保你的TreeViewItem模板里有ItemsHost这个ItemsPresenter,然后添加行为:

<TreeViewItem>
    <TreeViewItem.Style>
        <Style TargetType="TreeViewItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="TreeViewItem">
                        <!-- 你的模板内容,确保包含名为ItemsHost的ItemsPresenter -->
                        <ItemsPresenter x:Name="ItemsHost" Visibility="Collapsed">
                            <ItemsPresenter.LayoutTransform>
                                <ScaleTransform ScaleY="0"/>
                            </ItemsPresenter.LayoutTransform>
                        </ItemsPresenter>
                        <!-- 其他模板元素 -->
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <!-- 启用附加行为 -->
            <Setter Property="local:ExpandSelectedBehavior.Enable" Value="True"/>
        </Style>
    </TreeViewItem.Style>
</TreeViewItem>

方案二:代码后台直接处理(简单快速,适合单个控件)

如果你的场景只针对少数几个控件,不想写附加行为,可以直接在代码后台订阅PropertyChanged事件:

1. XAML中给TreeViewItem命名

<TreeViewItem x:Name="MyTreeViewItem">
    <TreeViewItem.Template>
        <ControlTemplate TargetType="TreeViewItem">
            <ItemsPresenter x:Name="ItemsHost" Visibility="Collapsed">
                <ItemsPresenter.LayoutTransform>
                    <ScaleTransform ScaleY="0"/>
                </ItemsPresenter.LayoutTransform>
            </ItemsPresenter>
            <!-- 其他模板元素 -->
        </ControlTemplate>
    </TreeViewItem.Template>
</TreeViewItem>

2. 代码后台逻辑

private (bool Expanded, bool Selected) _prevState;

public MainWindow()
{
    InitializeComponent();
    // 初始化状态
    _prevState = (MyTreeViewItem.IsExpanded, MyTreeViewItem.IsSelected);
    // 订阅属性变更事件
    MyTreeViewItem.PropertyChanged += MyTreeViewItem_PropertyChanged;
}

private void MyTreeViewItem_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (e.PropertyName != nameof(TreeViewItem.IsExpanded) && e.PropertyName != nameof(TreeViewItem.IsSelected))
        return;

    var currentExpanded = MyTreeViewItem.IsExpanded;
    var currentSelected = MyTreeViewItem.IsSelected;

    // 判断是否同时从false变为true
    if (currentExpanded && currentSelected && !_prevState.Expanded && !_prevState.Selected)
    {
        var itemsHost = MyTreeViewItem.Template.FindName("ItemsHost", MyTreeViewItem) as ItemsPresenter;
        if (itemsHost != null)
        {
            itemsHost.Visibility = Visibility.Visible;
            var storyboard = new Storyboard();
            var scaleAnim = new DoubleAnimation(0, 1, TimeSpan.FromSeconds(0.2))
            {
                FillBehavior = FillBehavior.HoldEnd
            };
            Storyboard.SetTargetProperty(scaleAnim, 
                new PropertyPath("(ItemsPresenter.LayoutTransform).(ScaleTransform.ScaleY)"));
            storyboard.Children.Add(scaleAnim);
            storyboard.Begin(itemsHost);
        }
    }

    // 更新状态
    _prevState = (currentExpanded, currentSelected);
}

关键逻辑说明

两种方案的核心都是跟踪属性的历史状态,只有当:

  1. 当前IsExpanded和IsSelected都为true
  2. 上一次记录的状态中,两个属性都为false

才会触发动画和可见性设置,完美符合你“仅在两个属性都变为true时触发”的需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:02:42