ListBox
がObservableCollection
にバインドされていて、ListBoxItems
の追加/削除をアニメーション化するとします。 FadeIn/Out、SlideDown/Upなど。どうすればよいですか?
TJ博士の答えは十分に正しいです。そのルートをたどると、ObservableCollection<T>
をラップして、BeforeDeleteイベントを実装する必要があります。次に、EventTrigger
を使用してストーリーボードを制御できます。
しかし、それは正しい痛みです。おそらく、DataTemplate
を作成し、EventTrigger
でFrameworkElement.Loaded
およびFrameworkElement.Unloaded
イベントを処理する方がよいでしょう。
以下に簡単なサンプルをまとめました。削除コードは自分で整理する必要がありますが、それで十分だと思います。
<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
グーグルの荒野を狩りに狂った時間を費やした後、私はこの問題をどのように解決したかを共有する必要があると思います。それは必要なのはかなり簡単なことのようですが、アニメーションがどのように実装されているかを深く理解するまで、WPFは途方もなくイライラします。そうすると、FrameworkElement.Unloadedがアニメーションにとって役に立たないイベントであることがわかります。 StackOverflow全体で(とりわけ)この質問の多くのバージョンを見てきましたが、これを解決するためのあらゆる種類のハックな方法があります。うまくいけば、私はあなたがあなたの多くの目的のために空想することができる最も簡単な例を提供することができます。
すでにLoadedroutedイベントを使用した多くの例でカバーされているため、FadeInの例は示しません。 * @ $の王室の苦痛であるのは、アイテムの削除でフェードアウトしています。
ここでの主な問題は、ストーリーボードをコントロール/データテンプレート/スタイルに配置したときにストーリーボードがどのように奇妙になるかに起因します。 DataContext(したがってオブジェクトのID)をストーリーボードにバインドすることはできません。 Completedイベントは、誰が終了したかがまったくわからない状態で発生します。すべてのデータテンプレートアイテムのコンテナの名前が同じであるため、ビジュアルツリーのダイビングは役に立ちません。確かに、コレクション全体で削除フラグプロパティが設定されているオブジェクトを検索する関数を作成することはできますが、それは醜く正直であり、意図的に作成することを認めたくないものです。また、アニメーションの長さの範囲内で複数のオブジェクトが削除されている場合は機能しません(私の場合です)。同様のことを行い、タイミングの地獄で迷子になるクリーンアップスレッドを作成することもできます。楽しくない。余談です。解決策に移ります。
仮定:
そうすれば、解決策は非常に単純で、苦痛を伴うので、これを解決するために長い時間を費やした場合。
ウィンドウのWindow.Resourcesセクション(DataTemplateの上)でフェードアウトをアニメーション化するストーリーボードを作成します。
(オプション)期間をリソースとして個別に定義して、ハードコーディングをできるだけ回避できるようにします。または、期間をハードコーディングします。
オブジェクトクラスに「Removing」、「isRemoving」、whatevというパブリックブールプロパティを作成します。このフィールドのプロパティ変更イベントを発生させるようにしてください。
「削除」プロパティにバインドするDataTriggerを作成し、Trueでフェードアウトストーリーボードを再生します。
オブジェクトクラスにプライベートDispatcherTimerオブジェクトを作成し、フェードアウトアニメーションと同じ期間の単純なタイマーを実装して、ティックハンドラーのリストからオブジェクトを削除します。
以下にコード例を示します。これにより、すべてを簡単に把握できるようになります。例をできるだけ単純化したので、自分に合った環境に適応させる必要があります。
コードビハインド
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,
}
}
XAML
<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>
万歳!〜
フェードアウトは、ItemsControl
基本実装を書き直さないと不可能になる可能性があります。問題は、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();
}
そして、この種の意味はあります。なぜなら、コンテナが表すデータが消えた後もコンテナが表示されている場合、理論的にはコンテナをクリックして(トリガー、イベントなどを開始する)、いくつかの微妙なバグが発生する可能性があるからです。
受け入れられた回答は、新しいアイテムの追加をアニメーション化するために機能しますが、既存のアイテムを削除するためには機能しません。これは、Unloaded
イベントが発生するまでに、アイテムがすでに削除されているためです。削除を機能させるための鍵は、「削除のマーク」の概念を追加することです。削除のマークを付けるとアニメーションがトリガーされ、アニメーションが完了すると実際の削除がトリガーされます。このアイデアを実装する方法はおそらくたくさんありますが、アタッチされた動作を作成し、ビューモデルを少し調整することで、それを機能させることができました。この動作により、3つの添付プロパティが公開されます。これらはすべて、各ListViewItem
に設定する必要があります。
Storyboard
の「ストーリーボード」。これは、アイテムが削除されたときに実行する実際のアニメーションです。ICommand
の「PerformRemoval」。これは、アニメーションの実行が完了したときに実行されるコマンドです。データバインドされたコレクションから要素を実際に削除するコードを実行する必要があります。bool
の「IsMarkedForRemoval」。リストからアイテムを削除する場合(ボタンクリックハンドラーなど)は、これをtrueに設定します。アタッチされたビヘイビアーは、このプロパティがtrueに変更されるとすぐに、アニメーションを開始します。また、アニメーションのCompleted
イベントが発生すると、Execute
PerformRemoval
コマンドが発生します。ここ は動作と使用例の完全なソースへのリンクです(自分のブログに転送するのが悪い形式の場合は、リンクを削除します。ここにコードを貼り付けますが、かなりです長い。それが違いを生むならば、私は物からお金を受け取らない)。
ふふ。受け入れられた解決策は機能しないので、別のラウンドを試してみましょう;)
ListBox(または他のコントロール)が元のリストから削除したときにビジュアルツリーからアイテムを削除するため、Unloadedイベントを使用できません。したがって、主なアイデアは、提供されたObservableCollectionのシャドウコピーを作成し、それにリストをバインドすることです。
まず第一に-XAML:
<ListBox ItemsSource="{Binding ShadowView}" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Loaded="OnItemViewLoaded">
<TextBlock Text="{Binding}"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
ListBoxを作成し、シャドウコピーにバインドし、IsSynchronizedWithCurrentItemを設定してICollectionView.CurrentItem(非常に便利なインターフェイス)を正しくサポートし、アイテムビューにLoadedイベントを設定します。このイベントハンドラーは、ビュー(アニメーション化される)とアイテム(削除される)を関連付ける必要があります。
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 ();
}
最後になりましたが、ShadowViewSourceクラス(完全ではありませんが、概念実証として機能します):
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 ;
}
}
そして最後の言葉。まず第一にそれは動作します。次へ-このアプローチでは、既存のコードの変更、プロパティの削除などによる回避策などは必要ありません。特に、単一のカスタムコントロールとして実装されている場合はそうです。 ObservableCollectionがあり、アイテムを追加し、削除し、必要なことを実行します。UIは常にこの変更を正しく反映しようとします。
フェードインとフェードアウト用に2つのストーリーボードを作成し、その値をOpacityMask
のListBox
用に作成したブラシにバインドします。
私のために FrameworkElement.Unloaded
イベントは機能しません-アイテムはただちに消えます。 WPFでの長年の経験が何もきれいにならなかったとは信じられませんが、これが機能する唯一の方法は、ここで説明するハックであるように見えます: リストボックスで削除されたアイテムをアニメーション化する ?..