Wpf 如何实现淡入淡出添加/删除的列表项

Wpf 如何实现淡入淡出添加/删除的列表项,wpf,animation,listbox,Wpf,Animation,Listbox,假设我将一个列表框绑定到一个可观察集合,并且我想设置添加/删除列表框项的动画,例如FadeIn/Out,向下/向上滑动等。我如何才能做到这一点?为淡入和淡出创建两层板,并将其值绑定到您为列表框的不透明掩码创建的笔刷上。沿着这条路走下去,你必须包装observetecollection并实现BeforeDelete事件,…然后你可以使用EventTrigger来控制情节提要 这是一个正确的痛苦,虽然。您最好创建一个数据模板并处理框架元素。在事件触发器中加载和框架元素。卸载事件 我在下面为您准备了一

假设我将一个
列表框
绑定到一个
可观察集合
,并且我想设置添加/删除
列表框项
的动画,例如FadeIn/Out,向下/向上滑动等。我如何才能做到这一点?

为淡入和淡出创建两层板,并将其值绑定到您为
列表框的
不透明掩码创建的笔刷上。沿着这条路走下去,你必须包装
observetecollection
并实现BeforeDelete事件,…然后你可以使用
EventTrigger
来控制情节提要

这是一个正确的痛苦,虽然。您最好创建一个
数据模板
并处理
框架元素。在
事件触发器
中加载
框架元素。卸载
事件

我在下面为您准备了一个快速样品。您必须自己整理删除代码,但我相信您能胜任

    <ListBox>
        <ListBox.ItemsSource>
            <x:Array Type="sys:String">
                <sys:String>One</sys:String>
                <sys:String>Two</sys:String>
                <sys:String>Three</sys:String>
                <sys:String>Four</sys:String>
                <sys:String>Five</sys:String>
            </x:Array>
        </ListBox.ItemsSource>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding}"
                           Opacity="0">
                    <TextBlock.Triggers>
                        <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                                     Duration="00:00:02"
                                                     From="0"
                                                     To="1" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                        <EventTrigger RoutedEvent="FrameworkElement.Unloaded">
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                                     Duration="00:00:02"
                                                     From="1"
                                                     To="0" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </TextBlock.Triggers>
                </TextBlock>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

一个
两个
三
四
五

HTH,如果不重新写入
项控件
基本实现,则Stimul8d

淡出可能是不可能的。问题在于,当
ItemsControl
从集合中接收到
INotifyCollectionChanged
事件时,它会立即(在深层私有代码中)将项容器标记为不可见(
IsVisible
是一个只读属性,它从隐藏缓存获取其值,因此无法访问)

您可以通过以下方式轻松实现淡入:

public class FadingListBox : ListBox
{
    protected override void PrepareContainerForItemOverride(
        DependencyObject element, object item)
    {
        var lb = (ListBoxItem)element;
        DoubleAnimation anm = new DoubleAnimation(0, 1, 
            TimeSpan.FromMilliseconds(500));
        lb.BeginAnimation(OpacityProperty, anm);
        base.PrepareContainerForItemOverride(element, item);
    }
}
但是“淡出”等价物永远不起作用,因为容器已经不可见并且无法重置

public class FadingListBox : ListBox
{
    protected override void ClearContainerForItemOverride(
        DependencyObject element, object item)
    {
        var lb = (ListBoxItem) element;
        lb.BringIntoView();
        DoubleAnimation anm = new DoubleAnimation(
            1, 0, TimeSpan.FromMilliseconds(500));
        lb.BeginAnimation(OpacityProperty, anm);
        base.ClearContainerForItemOverride(element, item);
    }
}
即使您有自己的自定义容器生成器,也无法克服此问题

protected override DependencyObject GetContainerForItemOverride()
    {
        return new FadingListBoxItem();
    }

这是有意义的,因为如果容器在它所代表的数据消失后仍然可见,那么理论上你可以点击容器(启动触发器、事件等)并且可能会遇到一些微妙的错误。

对于我来说
FrameworkElement。卸载的
事件不起作用-该项会立即消失。我很难相信多年的WPF经验并没有创造出更漂亮的东西,但看起来唯一可行的方法就是这里描述的一个黑客:?…

在花了几个小时疯狂地搜索谷歌的荒野之后,我想我应该和大家分享一下我是如何解决这个问题的,因为这似乎是一件非常简单的事情,但是WPF却让它变得非常令人沮丧,直到你完全理解动画是如何实现的。一旦你这么做了,你就会意识到FrameworkElement.Unload对于动画来说是一个无用的事件。我在StackOverflow(除其他外)上看到了这个问题的许多版本,有各种各样的黑客方法来解决这个问题。希望我能提供一个最简单的例子,然后你可以想出你的许多目的

我不会展示淡入示例,因为已经有很多使用加载的routed事件的示例介绍了淡入示例。在*@$中,皇家的痛苦在于移除物品,这一点正在逐渐消失

这里的主要问题源于当你将故事板放入控件/数据模板/样式时,它们是如何变得怪异的。无法将DataContext(以及对象的ID)绑定到情节提要。“已完成”事件触发时,不知道它是在谁身上完成的。潜水的可视化树是无用的,因为您的所有数据模板项目都有相同的名称为其容器!当然,您可以编写一个函数来搜索整个集合中设置了Remove flag属性的对象,但这很难看,说实话,只是您不想承认是故意写的。如果在动画的长度范围内删除多个对象(这是我的情况),那么它将不起作用。你也可以只写一个清理线程来做类似的事情,然后迷失在时间地狱中。没有乐趣。我离题了。关于解决方案

假设:

  • 您正在使用由一些自定义对象填充的ObservableCollection
  • 您可以使用DataTemplate为这些对象提供自定义外观,因此您需要为其移除设置动画
  • 将ObservableCollection绑定到一个列表框(或类似的简单对象)
  • 您已经在OC中的对象类上实现了InotifyPropertyChange
  • 那么这个解决方案非常简单,非常痛苦,如果你花了很长时间试图解决这个问题

  • 创建一个故事板,在窗口的Window.Resources部分(DataTemplate上方)设置淡出动画

  • (可选)将持续时间单独定义为资源,以便尽可能避免硬编码。或者只是硬编码持续时间

  • 在对象类中创建一个名为“Removing”、“isRemoving”的公共布尔属性。确保为此字段引发属性更改事件

  • 创建一个绑定到“删除”属性的DataTrigger,并在True上播放淡出情节提要

  • 在对象类中创建一个私有Dispatchermer对象,并实现一个简单的计时器,该计时器的持续时间与淡出动画的持续时间相同,并将对象从其标记处理程序的列表中删除

  • 下面是代码示例,其中
    public partial class MainWindow : Window
    {
        public static ObservableCollection<Missiles> MissileRack = new ObservableCollection<Missiles>(); // because who doesn't love missiles? 
        public static Duration FadeDuration; 
    
        // main window constructor
        public MainWindow()
        {
            InitializeComponent();
    
            // somewhere here you'll want to tie the XAML Duration to your code-behind, or if you like ugly messes you can just skip this step and hard code away 
            FadeDuration = (Duration)this.Resources["cnvFadeDuration"];
            // 
            // blah blah
            // 
        }
    
        public void somethread_ShootsMissiles()
        {
            // imagine this is running on your background worker threads (or something like it)
            // however you want to flip the Removing flag on specific objects, once you do, it will fade out nicely
            var missilesToShoot = MissileRack.Where(p => (complicated LINQ search routine).ToList();
            foreach (var missile in missilesToShoot)
            {
                // fire!
                missile.Removing = true;
            }
        }
    }
    
    public class Missiles
    {
        public Missiles()
        {}
    
        public bool Removing
        {
            get { return _removing; }
            set
            {
                _removing = value;
                OnPropertyChanged("Removing"); // assume you know how to implement this
    
                // start timer to remove missile from the rack
                start_removal_timer();
            }
        }
        private bool _removing = false;
    
        private DispatcherTimer remove_timer;
        private void start_removal_timer()
        {
            remove_timer = new DispatcherTimer();
            // because we set the Interval of the timer to the same length as the animation, we know the animation will finish running before remove is called. Perfect. 
            remove_timer.Interval = MainWindow.TrackFadeDuration.TimeSpan; // I'm sure you can find a better way to share if you don't like global statics, but I am lazy
            remove_timer.Tick += new EventHandler(remove_timer_Elapsed);
            remove_timer.Start();
        }
    
        // use of DispatcherTimer ensures this handler runs on the GUI thread for us
        // this handler is now effectively the "Storyboard Completed" event
        private void remove_timer_Elapsed(object sender, EventArgs e)
        {
            // this is the only operation that matters for this example, feel free to fancy this line up on your own
            MainWindow.MissileRack.Remove(this); // normally this would cause your object to just *poof* before animation has played, but thanks to timer, 
        }
    
    }
    
    <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Test" Height="300" Width="300">
        <Window.Resources>
            <Duration x:Key="cnvFadeDuration">0:0:0.3</Duration> <!-- or hard code this if you really must -->
            <Storyboard x:Key="cnvFadeOut" >
                <DoubleAnimation Storyboard.TargetName="cnvMissile"
                                          Storyboard.TargetProperty="Opacity" 
                                          From="1" To="0" Duration="{StaticResource cnvFadeDuration}"
                                          />
            </Storyboard>
    
            <DataTemplate x:Key="MissileTemplate">
                <Canvas x:Name="cnvMissile">
                    <!-- bunch of pretty missile graphics go here -->
                </Canvas>
    
                <DataTemplate.Triggers>
                    <DataTrigger Binding="{Binding Path=Removing}" Value="true" >
                        <DataTrigger.EnterActions>
                            <!-- you could actually just plop the storyboard right here instead of calling it as a resource, whatever suits your needs really -->
                            <BeginStoryboard Storyboard="{StaticResource cnvFadeOut}"  /> 
                        </DataTrigger.EnterActions>
                    </DataTrigger>
                </DataTemplate.Triggers>
            </DataTemplate>
        </Window.Resources>
        <Grid>
            <ListBox /> <!-- do your typical data binding and junk -->
        </Grid>
    </Window>
    
    <ListBox ItemsSource="{Binding ShadowView}" IsSynchronizedWithCurrentItem="True">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Border Loaded="OnItemViewLoaded">
                    <TextBlock Text="{Binding}"/>
                </Border>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    
    private void OnItemViewLoaded (object sender, RoutedEventArgs e)
    {
        var fe = (FrameworkElement) sender ;
        var dc = (DependencyObject) fe.DataContext ;
    
        dc.SetValue (ShadowViewSource.ViewProperty, fe) ;
    }
    
    private readonly ShadowViewSource m_shadow ;
    
    public ICollectionView ShadowView => m_shadow.View ;
    
    public MainWindow ()
    {
        m_collection = new ObservableCollection<...> () ;
    
        m_view = CollectionViewSource.GetDefaultView (m_collection) ;
        m_shadow = new ShadowViewSource (m_view) ;
    
        InitializeComponent ();
    }
    
    using System ;
    using System.Collections.Generic ;
    using System.Collections.ObjectModel ;
    using System.Collections.Specialized ;
    using System.ComponentModel ;
    using System.Linq ;
    using System.Windows ;
    using System.Windows.Data ;
    using System.Windows.Media.Animation ;
    
    namespace ShadowView
    {
        public class ShadowViewSource
        {
            public static readonly DependencyProperty ViewProperty = DependencyProperty.RegisterAttached ("View", typeof (FrameworkElement), typeof (ShadowViewSource)) ;
    
            private readonly ICollectionView m_sourceView ;
            private readonly IEnumerable<object> m_source ;
    
            private readonly ICollectionView m_view ;
            private readonly ObservableCollection<object> m_collection ;
    
            public ShadowViewSource (ICollectionView view)
            {
                var sourceChanged = view.SourceCollection as INotifyCollectionChanged ;
                if (sourceChanged == null)
                    throw new ArgumentNullException (nameof (sourceChanged)) ;
    
                var sortChanged = view.SortDescriptions as INotifyCollectionChanged ;
                if (sortChanged == null)
                    throw new ArgumentNullException (nameof (sortChanged)) ;
    
                m_source = view.SourceCollection as IEnumerable<object> ;
                if (m_source == null)
                    throw new ArgumentNullException (nameof (m_source)) ;
    
                m_sourceView = view ;
    
                m_collection = new ObservableCollection<object> (m_source) ;
                m_view = CollectionViewSource.GetDefaultView (m_collection) ;
                m_view.MoveCurrentTo (m_sourceView.CurrentItem) ;
    
                m_sourceView.CurrentChanged += OnSourceCurrentChanged ;
                m_view.CurrentChanged += OnViewCurrentChanged ;
    
                sourceChanged.CollectionChanged += OnSourceCollectionChanged ;
                sortChanged.CollectionChanged += OnSortChanged ;
            }
    
            private void OnSortChanged (object sender, NotifyCollectionChangedEventArgs e)
            {
                using (m_view.DeferRefresh ())
                {
                    var sd = m_view.SortDescriptions ;
                    sd.Clear () ;
                    foreach (var desc in m_sourceView.SortDescriptions)
                        sd.Add (desc) ;
                }
            }
    
            private void OnSourceCollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
            {
                var toAdd    = m_source.Except (m_collection) ;
                var toRemove = m_collection.Except (m_source) ;
    
                foreach (var obj in toAdd)
                    m_collection.Add (obj) ;
    
                foreach (DependencyObject obj in toRemove)
                {
                    var view = (FrameworkElement) obj.GetValue (ViewProperty) ;
    
                    var begintime = 1 ;
                    var sb = new Storyboard { BeginTime = TimeSpan.FromSeconds (begintime) } ;
                    sb.Completed += (s, ea) => m_collection.Remove (obj) ;
    
                    var fade = new DoubleAnimation (1, 0, new Duration (TimeSpan.FromMilliseconds (500))) ;
                    Storyboard.SetTarget (fade, view) ;
                    Storyboard.SetTargetProperty (fade, new PropertyPath (UIElement.OpacityProperty)) ;
                    sb.Children.Add (fade) ;
    
                    var size = new DoubleAnimation (view.ActualHeight, 0, new Duration (TimeSpan.FromMilliseconds (250))) ;
                    Storyboard.SetTarget (size, view) ;
                    Storyboard.SetTargetProperty (size, new PropertyPath (FrameworkElement.HeightProperty)) ;
                    sb.Children.Add (size) ;
                    size.BeginTime = fade.Duration.TimeSpan ;
    
                    sb.Begin () ;
                }
            }
    
            private void OnViewCurrentChanged (object sender, EventArgs e)
            {
                m_sourceView.MoveCurrentTo (m_view.CurrentItem) ;
            }
    
            private void OnSourceCurrentChanged (object sender, EventArgs e)
            {
                m_view.MoveCurrentTo (m_sourceView.CurrentItem) ;
            }
    
            public ICollectionView View => m_view ;
        }
    }