どこかの投稿サイトで、
ListBox コントロールに動的にアイテムを追加したとき、
アニメーションするようにしたいけどどうすればいいの?
みたいな質問が飛んでいて、
こうやったらできるんじゃないかな~と思っていたものを
実際に組んでみたので紹介しようと思います。
ListBox コントロールなどの ItemsControl 派生のコントロールは、
指定されたアイテムを並べるとき、
コンテナと呼ばれるコントロールに各アイテムを入れて表示しています。
ここでは、そのコンテナをカスタムコントロールにして、
そのコントロール内でアニメーション処理させます。
というわけでカスタムコントロール AnimatedContainer クラスを作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:AnimatedListBoxItem"> <Style TargetType="{x:Type local:AnimatedContainer}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:AnimatedContainer}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <StackPanel> <Button x:Name="PART_DeleteButton" /> <ContentPresenter /> </StackPanel> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary> |
ほぼ自動生成されたコードのままですが、
Border コントロールの中に Button をひとつと、
指定されたコンテンツを表示させるための ContentPresenter を置いてあります。
非常に雑な置き方ですが、
このカスタムコントロールの配置はなんでもいいです。
というのは、実際の配置はこのカスタムコントロールを使う側で
Template を指定してユーザ側で指定してもらうからです。
さておき、
コードビハインドを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
namespace AnimatedListBoxItem { using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media.Animation; /// <summary> /// アニメーションを含むコンテナを表します。 /// </summary> [TemplatePart(Name = PART_DeleteButton, Type = typeof(Button))] public class AnimatedContainer : ContentControl { #region TemplatePart /// <summary> /// 削除ボタンに対する名前 /// </summary> private const string PART_DeleteButton = "PART_DeleteButton"; private Button _deleteButton; /// <summary> /// 削除ボタンを取得または設定します。 /// </summary> private Button DeleteButton { get { return this._deleteButton; } set { if (this._deleteButton != null) { this._deleteButton.Click -= OnDeleteButtonClick; } this._deleteButton = value; if (this._deleteButton != null) { this._deleteButton.Click += OnDeleteButtonClick; } } } /// <summary> /// テンプレートを適用します。 /// </summary> public override void OnApplyTemplate() { base.OnApplyTemplate(); this.DeleteButton = this.Template.FindName(PART_DeleteButton, this) as Button; } #endregion TemplatePart #region コンストラクタ /// <summary> /// 静的なコンストラクタを表します。 /// </summary> static AnimatedContainer() { DefaultStyleKeyProperty.OverrideMetadata(typeof(AnimatedContainer), new FrameworkPropertyMetadata(typeof(AnimatedContainer))); } /// <summary> /// 新しいインスタンスを生成します。 /// </summary> public AnimatedContainer() { this.Opacity = 0; #region InAnimation 初期化 this._heightInAnimation = new DoubleAnimation() { From = 0, Duration = TimeSpan.FromMilliseconds(200), }; Storyboard.SetTargetProperty(this._heightInAnimation, new PropertyPath("Height")); this._widthInAnimation = new DoubleAnimation() { From = 0, Duration = TimeSpan.FromMilliseconds(200), }; Storyboard.SetTargetProperty(this._widthInAnimation, new PropertyPath("Width")); this._opacityInAnimation = new DoubleAnimation() { From = 0, To = 1, BeginTime = TimeSpan.FromMilliseconds(240), Duration = TimeSpan.FromMilliseconds(200), }; Storyboard.SetTargetProperty(this._opacityInAnimation, new PropertyPath("Opacity")); this._inStoryboard = new Storyboard(); this._inStoryboard.Completed += (_, __) => this._isAnimated = null; #endregion InAnimation 初期化 #region OutAnimation 初期化 this._heightOutAnimation = new DoubleAnimation() { To = 0, BeginTime = TimeSpan.FromMilliseconds(240), Duration = TimeSpan.FromMilliseconds(200), }; Storyboard.SetTargetProperty(this._heightOutAnimation, new PropertyPath("Height")); this._widthOutAnimation = new DoubleAnimation() { To = 0, BeginTime = TimeSpan.FromMilliseconds(240), Duration = TimeSpan.FromMilliseconds(200), }; Storyboard.SetTargetProperty(this._widthOutAnimation, new PropertyPath("Width")); this._opacityOutAnimation = new DoubleAnimation() { From = 1, To = 0, Duration = TimeSpan.FromMilliseconds(200), }; Storyboard.SetTargetProperty(this._opacityOutAnimation, new PropertyPath("Opacity")); this._outStoryboard = new Storyboard(); this._outStoryboard.Completed += (_, __) => { this._isAnimated = null; if (this.DeletedCommand != null) { if (DeletedCommand.CanExecute(this.DataContext)) { DeletedCommand.Execute(this.DataContext); } } }; #endregion OutAnimation 初期化 this.SizeChanged += OnSizeChanged; } #endregion コンストラクタ #region DeletedCommand 依存関係プロパティ /// <summary> /// DeletedCommand 依存関係プロパティを定義します。 /// </summary> public static DependencyProperty DeletedCommandProperty = DependencyProperty.Register("DeletedCommand", typeof(ICommand), typeof(AnimatedContainer), new PropertyMetadata(null)); /// <summary> /// 削除ボタンクリック後に実行されるコマンドを取得または設定します。 /// </summary> public ICommand DeletedCommand { get { return (ICommand)GetValue(DeletedCommandProperty); } set { SetValue(DeletedCommandProperty, value); } } #endregion DeletedCommand 依存関係プロパティ #region Direction 依存関係プロパティ public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register("Direction", typeof(SizeToContent), typeof(AnimatedContainer), new PropertyMetadata(SizeToContent.Height)); public SizeToContent Direction { get { return (SizeToContent)GetValue(DirectionProperty); } set { SetValue(DirectionProperty, value); } } #endregion Direction 依存関係プロパティ #region イベントハンドラ /// <summary> /// 削除ボタンクリックイベントハンドラ /// </summary> /// <param name="sender">イベント発行元</param> /// <param name="e">イベント引数</param> private void OnDeleteButtonClick(object sender, RoutedEventArgs e) { BeginOutAnimation(); } /// <summary> /// SizeChanged イベントハンドラ /// </summary> /// <param name="sender">イベント発行元</param> /// <param name="e">イベント引数</param> private void OnSizeChanged(object sender, SizeChangedEventArgs e) { //if (this._isAnimated == null) //{ // this._isAnimated = false; // return; //} if ((this._isAnimated != null) && this._isAnimated.Value) return; this._heightInAnimation.To = e.NewSize.Height; this._widthInAnimation.To = e.NewSize.Width; BeginInAnimation(); } #endregion イベントハンドラ #region アニメーション /// <summary> /// 表示開始アニメーションを開始します。 /// </summary> private void BeginInAnimation() { this._inStoryboard.Children.Clear(); switch (this.Direction) { case SizeToContent.Height: this._inStoryboard.Children.Add(this._heightInAnimation); break; case SizeToContent.Manual: break; case SizeToContent.Width: this._inStoryboard.Children.Add(this._widthInAnimation); break; case SizeToContent.WidthAndHeight: this._inStoryboard.Children.Add(this._heightInAnimation); this._inStoryboard.Children.Add(this._widthInAnimation); break; } this._inStoryboard.Children.Add(this._opacityInAnimation); this._isAnimated = true; this.BeginStoryboard(this._inStoryboard); } /// <summary> /// 非表示アニメーションを開始します。 /// </summary> private void BeginOutAnimation() { this._outStoryboard.Children.Clear(); switch (this.Direction) { case SizeToContent.Height: this._outStoryboard.Children.Add(this._heightOutAnimation); break; case SizeToContent.Manual: break; case SizeToContent.Width: this._outStoryboard.Children.Add(this._widthOutAnimation); break; case SizeToContent.WidthAndHeight: this._outStoryboard.Children.Add(this._heightOutAnimation); this._outStoryboard.Children.Add(this._widthOutAnimation); break; } this._outStoryboard.Children.Add(this._opacityOutAnimation); this._isAnimated = true; this.BeginStoryboard(this._outStoryboard); } #endregion アニメーション #region private フィールド /// <summary> /// アニメーション中かどうかを判別します。 /// </summary> private bool? _isAnimated; /// <summary> /// 表示されるときのアニメーション用ストーリーボード /// </summary> private Storyboard _inStoryboard; /// <summary> /// 表示されるときの高さアニメーション /// </summary> private DoubleAnimation _heightInAnimation; /// <summary> /// 表示されるときの幅アニメーション /// </summary> private DoubleAnimation _widthInAnimation; /// <summary> /// 表示されるときの透明度アニメーション /// </summary> private DoubleAnimation _opacityInAnimation; /// <summary> /// 非表示になるときのアニメーション用ストーリーボード /// </summary> private Storyboard _outStoryboard; /// <summary> /// 非表示になるときの高さアニメーション /// </summary> private DoubleAnimation _heightOutAnimation; /// <summary> /// 非表示になるときの幅アニメーション /// </summary> private DoubleAnimation _widthOutAnimation; /// <summary> /// 非表示になるときの透明度アニメーション /// </summary> private DoubleAnimation _opacityOutAnimation; #endregion private フィールド } } |
いきなり長いコードですが、
ポイントはサイズ変更イベントハンドラの OnSizeChanged メソッドで
このコントロールが表示されるときにフェードインさせるアニメーションを実行しています。
また、PART_DeleteButton と名付けられたボタンがクリックされたタイミングで
このコントロールを非表示とするようにフェードアウトさせるアニメーションを実行しています。
さらに、フェードアウトのアニメーションが終了したら
コールバックとして DeletedCommand プロパティに指定されたコマンドを実行するようにしています。
ListBox コントロールにアイテムを追加/削除することを考えてみましょう。
追加するときはとにかく Add すればコンテナがひとつ増え、
始めに OnSizeChanged が呼ばれることになります。
では、逆に削除するときはどうでしょうか。
いきなり ItemsSource のコレクションから Remove などで削除してしまうと、
フェードアウトさせたいコンテナ自体がいきなり消えてしまいます。
したがって、コレクションから削除する前にフェードアウトのアニメーションを実行し、
そのアニメーションが完了してからコレクションから削除しなければいけません。
そんなわけで、削除するときは DeleteCommand プロパティを利用して
コレクションからアイテムを Remove すればいいのです。
また、そのようにできるように上記のコードでは、
DeleteCommand.Execute(this.DataContext) のように、
AnimatedContainer コントロールの DataContext を
パラメータとしてコマンドを実行しています。
これで処理を渡された方はどのアイテムを削除すれば良いかが判断できるようになっています。
そんなわけでこのカスタムコントロールを使う側の XAML と ViewModel です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<Window x:Class="AnimatedListBoxItem.Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:AnimatedListBoxItem" Title="MainView" Height="300" Width="300"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <StackPanel> <Button Content="Add" Command="{Binding AddCommand}" /> <TextBlock Text="{Binding Count, StringFormat='{}アイテムが {0} 個登録されています。'}" /> </StackPanel> <ListBox Grid.Row="1" ItemsSource="{Binding StringCollection}"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ContentControl}"> <Border Background="{TemplateBinding Background}"> <local:AnimatedContainer DeletedCommand="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" Direction="Height"> <local:AnimatedContainer.Template> <ControlTemplate TargetType="{x:Type local:AnimatedContainer}"> <StackPanel Orientation="Horizontal"> <Button x:Name="PART_DeleteButton" Content="Delete" Margin="2" /> <TextBlock Text="{Binding .}" VerticalAlignment="Center" /> </StackPanel> </ControlTemplate> </local:AnimatedContainer.Template> </local:AnimatedContainer> </Border> </ControlTemplate> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="LightGray" /> </Trigger> <Trigger Property="IsSelected" Value="True"> <Setter Property="Background" Value="Plum" /> </Trigger> </Style.Triggers> </Style> </ListBox.ItemContainerStyle> </ListBox> </Grid> </Window> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
namespace AnimatedListBoxItem.ViewModels { using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; internal class MainViewModel : INotifyPropertyChanged { private ObservableCollection<string> _stringCollection = new ObservableCollection<string>(); /// <summary> /// コレクションデータを取得します。 /// </summary> public ObservableCollection<string> StringCollection { get { return this._stringCollection; } } /// <summary> /// コレクションデータ数を取得します。 /// </summary> public int Count { get { return this.StringCollection.Count; } } private DelegateCommand _addCommand; /// <summary> /// コレクション追加コマンドを取得します。 /// </summary> public DelegateCommand AddCommand { get { return this._addCommand ?? (this._addCommand = new DelegateCommand(_ => { this._counter++; Add(string.Format("私がアイテム No.{0} だ。", this._counter)); })); } } private DelegateCommand _deleteCommand; /// <summary> /// コレクション削除コマンドを取得します。 /// </summary> public DelegateCommand DeleteCommand { get { return this._deleteCommand ?? (this._deleteCommand = new DelegateCommand(p => { Delete(p as string); })); } } /// <summary> /// カウンタ /// </summary> private int _counter; /// <summary> /// 乱数発生器 /// </summary> private Random _random = new Random(); /// <summary> /// コレクションにアイテムを追加します。 /// </summary> /// <param name="item">追加するアイテムを指定します。</param> private void Add(string item) { this.StringCollection.Insert(this._random.Next(0, this.StringCollection.Count), item); RaisePropertyChanged("Count"); } /// <summary> /// コレクションからアイテムを削除します。 /// </summary> /// <param name="item"></param> private void Delete(string item) { this.StringCollection.Remove(item); RaisePropertyChanged("Count"); } #region INotifyPropertyChanged のメンバ /// <summary> /// プロパティ値変更時に発生します。 /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// PropertyChanged イベントを発行します。 /// </summary> /// <param name="propertyName">プロパティ値が変更されたプロパティ名を指定します。</param> private void RaisePropertyChanged([CallerMemberName]string propertyName = null) { var h = this.PropertyChanged; if (h != null) h(this, new PropertyChangedEventArgs(propertyName)); } #endregion INotifyPropertyChanged のメンバ } } |
念のため ICommand インターフェースを実装した DelegateCommand クラスのソースも。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
namespace AnimatedListBoxItem { using System; using System.Windows.Input; internal class DelegateCommand : ICommand { private Action<object> _execute; private Func<object, bool> _canExecute; public DelegateCommand(Action<object> execute) : this(execute, null) { } public DelegateCommand(Action<object> execute, Func<object, bool> canExecute) { this._execute = execute; this._canExecute = canExecute; } public bool CanExecute(object parameter) { return this._canExecute != null ? this._canExecute(parameter) : true; } public event EventHandler CanExecuteChanged; public void RaiseCanExecuteChanged() { var h = this.CanExecuteChanged; if (h != null) h(this, EventArgs.Empty); } public void Execute(object parameter) { if (this._execute != null) { this._execute(parameter); } } } } |
今回は ListBox コントロールを対象としましたが、
他の ItemsControl クラス派生のコントロール、
例えば TreeView とかDataGrid とかでも
応用が利くんじゃないかなぁと思っています。
アニメーションは凝ると楽しいけど、
実用的なものを考えようとすると難しいなぁ。
というわけで今日はこの辺で。