for WPF developers
Home Profile Tips 全記事一覧

WPF で画面遷移する方法 4

(2017/05/19 16:13:11 created.)

(2017/05/24 16:46:50 modified.)

前回は TabControl によって画面遷移をおこないましたが、遷移アニメーションがなくてさみしくなってしまいました。ここでは TabControl によってアニメーションしながら画面遷移するようにします。

わざわざ記事を分けてしまいましたが、実は結構簡単。一番最初に紹介した TransitionPanel コントロールを使います。もう忘れてしまっていると思うのでコードを再掲します。

TransitionPanel.xaml
  1. <UserControl x:Class="TabSample.Views.TransitionPanel"
  2.          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
  5.          xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
  6.          mc:Ignorable="d" 
  7.          d:DesignWidth="400" d:DesignHeight="600"
  8.          x:Name="root">
  9.     <Grid>
  10.         <ContentControl Content="{Binding ContentA, ElementName=root}">
  11.             <ContentControl.RenderTransform>
  12.                 <TranslateTransform X="{Binding OffsetXA, ElementName=root}" Y="{Binding OffsetYA, ElementName=root}" />
  13.             </ContentControl.RenderTransform>
  14.         </ContentControl>
  15.         <ContentControl Content="{Binding ContentB, ElementName=root}">
  16.             <ContentControl.RenderTransform>
  17.                 <TranslateTransform X="{Binding OffsetXB, ElementName=root}" Y="{Binding OffsetYB, ElementName=root}" />
  18.             </ContentControl.RenderTransform>
  19.         </ContentControl>
  20.     </Grid>
  21. </UserControl>
TransitionPanel.xaml.cs
  1. namespace TabSample.Views
  2. {
  3.     using System;
  4.     using System.Windows;
  5.     using System.Windows.Controls;
  6.     using System.Windows.Media.Animation;
  7.  
  8.     /// <summary>
  9.     /// TransitionPanel.xaml の相互作用ロジック
  10.     /// </summary>
  11.     public partial class TransitionPanel : UserControl
  12.     {
  13.         /// <summary>
  14.         /// 新しいインスタンスを生成します。
  15.         /// </summary>
  16.         public TransitionPanel()
  17.         {
  18.             InitializeComponent();
  19.  
  20.             this.Loaded += OnLoaded;
  21.         }
  22.  
  23.         /// <summary>
  24.         /// アニメーションの方向を表します。
  25.         /// </summary>
  26.         public enum TransitDirections
  27.         {
  28.             /// <summary>
  29.             /// 左へ移動します。
  30.             /// </summary>
  31.             ToLeft,
  32.  
  33.             /// <summary>
  34.             /// 右へ移動します。
  35.             /// </summary>
  36.             ToRight,
  37.         }
  38.  
  39.         /// <summary>
  40.         /// 遷移状態を表します。
  41.         /// </summary>
  42.         public enum TransitionStates
  43.         {
  44.             /// <summary>
  45.             /// A が表示されている状態を表します。
  46.             /// </summary>
  47.             DisplayA,
  48.  
  49.             /// <summary>
  50.             /// B が表示されている状態を表します。
  51.             /// </summary>
  52.             DisplayB,
  53.         }
  54.  
  55.         #region Content 依存関係プロパティ
  56.  
  57.         /// <summary>
  58.         /// Content 依存関係プロパティを定義し直します。
  59.         /// </summary>
  60.         public static readonly new DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(TransitionPanel), new UIPropertyMetadata(null, OnConentPropertyChanged));
  61.  
  62.         /// <summary>
  63.         /// コンテンツを取得または設定します。
  64.         /// </summary>
  65.         public new object Content
  66.         {
  67.             get { return GetValue(ContentProperty); }
  68.             set { SetValue(ContentProperty, value); }
  69.         }
  70.  
  71.         #endregion Content 依存関係プロパティ
  72.  
  73.         #region ContentA 依存関係プロパティ
  74.  
  75.         /// <summary>
  76.         /// ContentA 依存関係プロパティのキーを定義します。
  77.         /// </summary>
  78.         private static readonly DependencyPropertyKey ContentAPropertyKey = DependencyProperty.RegisterReadOnly("ContentA", typeof(object), typeof(TransitionPanel), new UIPropertyMetadata(null));
  79.  
  80.         /// <summary>
  81.         /// ContentA 依存関係プロパティを定義します。
  82.         /// </summary>
  83.         public static readonly DependencyProperty ContentAProperty = ContentAPropertyKey.DependencyProperty;
  84.  
  85.         /// <summary>
  86.         /// コンテンツのためのバッファ A を取得します。
  87.         /// </summary>
  88.         public object ContentA
  89.         {
  90.             get { return GetValue(ContentAProperty); }
  91.             private set { SetValue(ContentAPropertyKey, value); }
  92.         }
  93.  
  94.         #endregion ContentA 依存関係プロパティ
  95.  
  96.         #region ContentB 依存関係プロパティ
  97.  
  98.         /// <summary>
  99.         /// ContentB 依存関係プロパティのキーを定義します。
  100.         /// </summary>
  101.         private static readonly DependencyPropertyKey ContentBPropertyKey = DependencyProperty.RegisterReadOnly("ContentB", typeof(object), typeof(TransitionPanel), new UIPropertyMetadata(null));
  102.  
  103.         /// <summary>
  104.         /// ContentB 依存関係プロパティを定義します。
  105.         /// </summary>
  106.         public static readonly DependencyProperty ContentBProperty = ContentBPropertyKey.DependencyProperty;
  107.  
  108.         /// <summary>
  109.         /// コンテンツのためのバッファ B を取得します。
  110.         /// </summary>
  111.         public object ContentB
  112.         {
  113.             get { return GetValue(ContentBProperty); }
  114.             private set { SetValue(ContentBPropertyKey, value); }
  115.         }
  116.  
  117.         #endregion ContentB 依存関係プロパティ
  118.  
  119.         #region State 依存関係プロパティ
  120.  
  121.         /// <summary>
  122.         /// State 依存関係プロパティのキーを定義します。
  123.         /// </summary>
  124.         private static readonly DependencyPropertyKey StatePropertyKey = DependencyProperty.RegisterReadOnly("State", typeof(TransitionStates), typeof(TransitionPanel), new UIPropertyMetadata(TransitionStates.DisplayB));
  125.  
  126.         /// <summary>
  127.         /// State 依存関係プロパティを定義します。
  128.         /// </summary>
  129.         public static readonly DependencyProperty StateProperty = StatePropertyKey.DependencyProperty;
  130.  
  131.         /// <summary>
  132.         /// 遷移状態を取得します。
  133.         /// </summary>
  134.         public TransitionStates State
  135.         {
  136.             get { return (TransitionStates)GetValue(StateProperty); }
  137.             private set { SetValue(StatePropertyKey, value); }
  138.         }
  139.  
  140.         #endregion State 依存関係プロパティ
  141.  
  142.         #region TransitDirection 依存関係プロパティ
  143.  
  144.         /// <summary>
  145.         /// TransitDirection 依存関係プロパティを定義します。
  146.         /// </summary>
  147.         public static readonly DependencyProperty TransitDirectionProperty = DependencyProperty.Register("TransitDirection", typeof(TransitDirections), typeof(TransitionPanel), new UIPropertyMetadata(TransitDirections.ToLeft));
  148.  
  149.         /// <summary>
  150.         /// 画面遷移方向を取得または設定します。
  151.         /// </summary>
  152.         public TransitDirections TransitDirection
  153.         {
  154.             get { return (TransitDirections)GetValue(TransitDirectionProperty); }
  155.             set { SetValue(TransitDirectionProperty, value); }
  156.         }
  157.  
  158.         #endregion TransitDirection 依存関係プロパティ
  159.  
  160.         #region OffsetXA 依存関係プロパティ
  161.  
  162.         /// <summary>
  163.         /// OffsetXA 依存関係プロパティを定義します。
  164.         /// </summary>
  165.         public static readonly DependencyProperty OffsetXAProperty = DependencyProperty.Register("OffsetXA", typeof(double), typeof(TransitionPanel), new UIPropertyMetadata(0.0));
  166.  
  167.         /// <summary>
  168.         /// コンテンツのためのバッファ A の水平方向オフセットを取得または設定します。
  169.         /// </summary>
  170.         public double OffsetXA
  171.         {
  172.             get { return (double)GetValue(OffsetXAProperty); }
  173.             set { SetValue(OffsetXAProperty, value); }
  174.         }
  175.  
  176.         #endregion OffsetXA 依存関係プロパティ
  177.  
  178.         #region OffsetYA 依存関係プロパティ
  179.  
  180.         /// <summary>
  181.         /// OffsetYA 依存関係プロパティを定義します。
  182.         /// </summary>
  183.         public static readonly DependencyProperty OffsetYAProperty = DependencyProperty.Register("OffsetYA", typeof(double), typeof(TransitionPanel), new UIPropertyMetadata(0.0));
  184.  
  185.         /// <summary>
  186.         /// コンテンツのためのバッファ A の垂直方向オフセットを取得または設定します。
  187.         /// </summary>
  188.         public double OffsetYA
  189.         {
  190.             get { return (double)GetValue(OffsetYAProperty); }
  191.             set { SetValue(OffsetYAProperty, value); }
  192.         }
  193.  
  194.         #endregion OffsetYA 依存関係プロパティ
  195.  
  196.         #region OffsetXB 依存関係プロパティ
  197.  
  198.         /// <summary>
  199.         /// OffsetXB 依存関係プロパティを定義します。
  200.         /// </summary>
  201.         public static readonly DependencyProperty OffsetXBProperty = DependencyProperty.Register("OffsetXB", typeof(double), typeof(TransitionPanel), new UIPropertyMetadata(0.0));
  202.  
  203.         /// <summary>
  204.         /// コンテンツのためのバッファ B の水平方向オフセットを取得または設定します。
  205.         /// </summary>
  206.         public double OffsetXB
  207.         {
  208.             get { return (double)GetValue(OffsetXBProperty); }
  209.             set { SetValue(OffsetXBProperty, value); }
  210.         }
  211.  
  212.         #endregion OffsetXB 依存関係プロパティ
  213.  
  214.         #region OffsetYB 依存関係プロパティ
  215.  
  216.         /// <summary>
  217.         /// OffsetYB 依存関係プロパティを定義します。
  218.         /// </summary>
  219.         public static readonly DependencyProperty OffsetYBProperty = DependencyProperty.Register("OffsetYB", typeof(double), typeof(TransitionPanel), new UIPropertyMetadata(0.0));
  220.  
  221.         /// <summary>
  222.         /// コンテンツのためのバッファ B の垂直方向オフセットを取得または設定します。
  223.         /// </summary>
  224.         public double OffsetYB
  225.         {
  226.             get { return (double)GetValue(OffsetYBProperty); }
  227.             set { SetValue(OffsetYBProperty, value); }
  228.         }
  229.  
  230.         #endregion OffsetYB 依存関係プロパティ
  231.  
  232.         #region イベントハンドラ
  233.  
  234.         /// <summary>
  235.         /// Load イベントハンドラ
  236.         /// </summary>
  237.         /// <param name="sender">イベント発行元</param>
  238.         /// <param name="e">イベント引数</param>
  239.         private void OnLoaded(object sender, RoutedEventArgs e)
  240.         {
  241.             var storyboard = new Storyboard();
  242.  
  243.             storyboard.Children = new TimelineCollection()
  244.             {
  245.                 CreateMoveAnimation(TimeZero, TimeZero, this.HorizontalOffset, "OffsetXB"),
  246.             };
  247.  
  248.             storyboard.Begin();
  249.         }
  250.  
  251.         /// <summary>
  252.         /// Content 依存関係プロパティ変更イベントハンドラ
  253.         /// </summary>
  254.         /// <param name="d">イベント発行元</param>
  255.         /// <param name="e">イベント引数</param>
  256.         private static void OnConentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  257.         {
  258.             var control = d as TransitionPanel;
  259.             if (control.IsInitialized) control.SwapDisplay();
  260.         }
  261.  
  262.         #endregion イベントハンドラ
  263.  
  264.         #region ヘルパ
  265.  
  266.         /// <summary>
  267.         /// コンテンツを入れ替えます。
  268.         /// </summary>
  269.         private void SwapDisplay()
  270.         {
  271.             if (this.State == TransitionStates.DisplayA)
  272.             {
  273.                 this.ContentB = this.Content;
  274.                 this.State = TransitionStates.DisplayB;
  275.             }
  276.             else
  277.             {
  278.                 this.ContentA = this.Content;
  279.                 this.State = TransitionStates.DisplayA;
  280.             }
  281.  
  282.             if ((this.ContentA != null) && (this.ContentB != null))
  283.                 StartAnimation();
  284.         }
  285.  
  286.         /// <summary>
  287.         /// 画面遷移を開始します。
  288.         /// </summary>
  289.         private void StartAnimation()
  290.         {
  291.             var storyboard = this.State == TransitionStates.DisplayA ? CreateAnimationBtoA(this.TransitDirection) : CreateAnimationAtoB(this.TransitDirection);
  292.             storyboard.Begin();
  293.         }
  294.  
  295.         /// <summary>
  296.         /// ContentB から ContentA へ遷移するためのストーリーボードを生成します。
  297.         /// </summary>
  298.         /// <param name="direction">遷移する方向を指定します。</param>
  299.         /// <returns>生成したストーリーボードを返します。</returns>
  300.         private Storyboard CreateAnimationBtoA(TransitDirections direction)
  301.         {
  302.             var storyboard = new Storyboard();
  303.  
  304.             storyboard.Children = direction == TransitDirections.ToLeft ?
  305.             new TimelineCollection()
  306.             {
  307.                 CreateMoveAnimation(TimeZero, TimeZero, this.HorizontalOffset, "OffsetXA"),
  308.                 CreateMoveAnimation(TimeZero, AnimationTime, 0, "OffsetXA"),
  309.                 CreateMoveAnimation(TimeZero, AnimationTime, -this.HorizontalOffset, "OffsetXB"),
  310.             } :
  311.             new TimelineCollection()
  312.             {
  313.                 CreateMoveAnimation(TimeZero, TimeZero, -this.HorizontalOffset, "OffsetXA"),
  314.                 CreateMoveAnimation(TimeZero, AnimationTime, 0, "OffsetXA"),
  315.                 CreateMoveAnimation(TimeZero, AnimationTime, this.HorizontalOffset, "OffsetXB"),
  316.             };
  317.  
  318.             return storyboard;
  319.         }
  320.  
  321.         /// <summary>
  322.         /// ContentA から ContentB へ遷移するためのストーリーボードを生成します。
  323.         /// </summary>
  324.         /// <param name="direction">遷移する方向を指定します。</param>
  325.         /// <returns>生成したストーリーボードを返します。</returns>
  326.         private Storyboard CreateAnimationAtoB(TransitDirections direction)
  327.         {
  328.             var storyboard = new Storyboard();
  329.  
  330.             storyboard.Children = direction == TransitDirections.ToLeft ?
  331.             new TimelineCollection()
  332.             {
  333.                 CreateMoveAnimation(TimeZero, TimeZero, this.HorizontalOffset, "OffsetXB"),
  334.                 CreateMoveAnimation(TimeZero, AnimationTime, 0, "OffsetXB"),
  335.                 CreateMoveAnimation(TimeZero, AnimationTime, -this.HorizontalOffset, "OffsetXA"),
  336.             } :
  337.             new TimelineCollection()
  338.             {
  339.                 CreateMoveAnimation(TimeZero, TimeZero, -this.HorizontalOffset, "OffsetXB"),
  340.                 CreateMoveAnimation(TimeZero, AnimationTime, 0, "OffsetXB"),
  341.                 CreateMoveAnimation(TimeZero, AnimationTime, this.HorizontalOffset, "OffsetXA"),
  342.             };
  343.  
  344.             return storyboard;
  345.         }
  346.  
  347.         /// <summary>
  348.         /// Double 型のプロパティに対するアニメーションを生成します。
  349.         /// </summary>
  350.         /// <param name="beginTime">アニメーションの開始時間を指定します。</param>
  351.         /// <param name="duration">アニメーションの実行時間を指定します。</param>
  352.         /// <param name="to">プロパティ値の最終値を指定します。</param>
  353.         /// <param name="targetPropertyName">対象とするプロパティ名を指定します。</param>
  354.         /// <returns>Storyboard の添付プロパティを設定したアニメーションを返します。</returns>
  355.         private DoubleAnimation CreateMoveAnimation(TimeSpan beginTime, TimeSpan duration, double to, string targetPropertyName)
  356.         {
  357.             var animation = new DoubleAnimation()
  358.             {
  359.                 To = to,
  360.                 BeginTime = beginTime,
  361.                 Duration = new Duration(duration),
  362.                 AccelerationRatio = 0.3,
  363.                 DecelerationRatio = 0.3,
  364.             };
  365.             Storyboard.SetTarget(animation, this);
  366.             Storyboard.SetTargetProperty(animation, new PropertyPath(targetPropertyName));
  367.  
  368.             return animation;
  369.         }
  370.  
  371.         #endregion ヘルパ
  372.  
  373.         #region private フィールド
  374.  
  375.         /// <summary>
  376.         /// 時刻ゼロ
  377.         /// </summary>
  378.         private static readonly TimeSpan TimeZero = TimeSpan.FromMilliseconds(0);
  379.  
  380.         /// <summary>
  381.         /// アニメーション時間
  382.         /// </summary>
  383.         private static readonly TimeSpan AnimationTime = TimeSpan.FromMilliseconds(500);
  384.  
  385.         /// <summary>
  386.         /// 水平方向の遷移量
  387.         /// </summary>
  388.         private double HorizontalOffset { get { return this.ActualWidth + 10; } }
  389.  
  390.         #endregion private フィールド
  391.     }
  392. }

Content プロパティの変化を捕捉して TranslateTransform によって 2 つのコンテンツを水平移動するコントロールでしたね。これを TabControl で使います。

MainView.xaml
  1. <YK:Window x:Class="TabSample.Views.MainView"
  2.            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.            xmlns:YK="clr-namespace:YKToolkit.Controls;assembly=YKToolkit.Controls"
  5.            xmlns:vw="clr-namespace:TabSample.Views"
  6.            Title="MainView"
  7.            Width="300" Height="200">
  8.     <DockPanel>
  9.         <ComboBox x:Name="combobox" DockPanel.Dock="Bottom" SelectedIndex="0">
  10.             <ComboBoxItem>Content01</ComboBoxItem>
  11.             <ComboBoxItem>Content02</ComboBoxItem>
  12.             <ComboBoxItem>Content03</ComboBoxItem>
  13.         </ComboBox>
  14.         <TabControl SelectedIndex="{Binding SelectedIndex, ElementName=combobox}">
  15.             <TabControl.Template>
  16.                 <ControlTemplate TargetType="{x:Type TabControl}">
  17.                     <vw:TransitionPanel Content="{Binding SelectedContent, RelativeSource={RelativeSource TemplatedParent}}" />
  18.                 </ControlTemplate>
  19.             </TabControl.Template>
  20.  
  21.             <TabItem>
  22.                 <Grid DataContext="{Binding Content01ViewModel}">
  23.                     <CheckBox Content="{Binding Caption}" />
  24.                 </Grid>
  25.             </TabItem>
  26.  
  27.             <TabItem>
  28.                 <Grid DataContext="{Binding Content02ViewModel}">
  29.                     <TextBlock Text="{Binding Caption}" />
  30.                 </Grid>
  31.             </TabItem>
  32.  
  33.             <TabItem>
  34.                 <Grid DataContext="{Binding Content03ViewModel}">
  35.                     <TextBlock Text="{Binding Caption}" />
  36.                 </Grid>
  37.             </TabItem>
  38.         </TabControl>
  39.     </DockPanel>
  40. </YK:Window>

そう、TabControl でコンテンツを表示するときに ContentPresenter を指定していましたね。これを TransitionPanel に差し替えるだけです。ただし、SelectedContent で渡ってくる要素は TabItem ではなく、その直下の要素になるため、DataContext を切り替える場合はこの例のように TabItem 直下に Grid など親要素となるパネルを置き、その DataContext を明確に指定するようにします。

こうしてできあがった外観がこちら。

一番最初に紹介した外観とまったく同じものができあがりました。違いは DataTemplate による View と ViewModel の紐付けをしていないところです。また、だからといって App クラスにややこしい GetView() などというメソッドも作っていません。単に MainView.xaml に各コンテンツを配置しているだけでわかりやすくなりました。